[Response Ops][Alerting] Alert Deletion - Persist settings (#211488)

## Summary

https://github.com/elastic/kibana/issues/209258 updates the settings
endpoint to also be able to get/set the alert deletion settings. The
alert deletion setting should make use of this new endpoint to load its
initial data and store any user update.

> [!WARNING]
> This will be merged into a feature branch.


## QA:
Activate the feature flag
```
# config/kibana.dev.yml
xpack.trigger_actions_ui.enableExperimental: ['alertDeletionSettingsEnabled']
```
Follow these steps:
- Go to rules
- Click on settings
- Change the alert deletion settings
- Click on save
- Reload and check the settings kept the values

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julian Gernun 2025-03-19 20:09:14 +01:00 committed by GitHub
parent 445646d31d
commit 8928dbbdef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 780 additions and 96 deletions

View file

@ -10,6 +10,7 @@
import { HttpSetup } from '@kbn/core/public';
import type { AsApiContract } from '@kbn/actions-types';
import type { RulesSettingsAlertDeletion } from '@kbn/alerting-types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
const transformAlertsDeletionSettingsResponse = ({
active_alerts_deletion_threshold: activeAlertsDeletionThreshold,
@ -31,19 +32,10 @@ const transformAlertsDeletionSettingsResponse = ({
updatedBy,
});
export const fetchAlertsDeletionSettings = async ({ http }: { http: HttpSetup }) => {
// TODO: https://github.com/elastic/kibana/issues/209258
const res = {
is_active_alerts_deletion_enabled: false,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 0,
inactive_alerts_deletion_threshold: 90,
created_at: String(new Date().valueOf),
updated_at: String(new Date().valueOf),
created_by: null,
updated_by: null,
};
export const fetchAlertDeletionSettings = async ({ http }: { http: HttpSetup }) => {
const res = await http.get<AsApiContract<RulesSettingsAlertDeletion>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_alert_deletion`
);
return transformAlertsDeletionSettingsResponse(res);
};

View file

@ -7,5 +7,5 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { fetchAlertsDeletionSettings } from './fetch_alerts_deletion_settings';
export { updateAlertsDeletionSettings } from './update_alerts_deletion_settings';
export { fetchAlertDeletionSettings } from './fetch_alert_deletion_settings';
export { updateAlertDeletionSettings } from './update_alert_deletion_settings';

View file

@ -0,0 +1,65 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { HttpSetup } from '@kbn/core/public';
import type { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
import type {
RulesSettingsAlertDeletion,
RulesSettingsAlertDeletionProperties,
} from '@kbn/alerting-types/rule_settings';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
const rewriteBodyRes: RewriteRequestCase<RulesSettingsAlertDeletion> = ({
is_active_alerts_deletion_enabled: isActiveAlertsDeletionEnabled,
is_inactive_alerts_deletion_enabled: isInactiveAlertsDeletionEnabled,
active_alerts_deletion_threshold: activeAlertsDeletionThreshold,
inactive_alerts_deletion_threshold: inactiveAlertsDeletionThreshold,
created_by: createdBy,
updated_by: updatedBy,
created_at: createdAt,
updated_at: updatedAt,
}: any) => ({
isActiveAlertsDeletionEnabled,
isInactiveAlertsDeletionEnabled,
activeAlertsDeletionThreshold,
inactiveAlertsDeletionThreshold,
createdBy,
updatedBy,
createdAt,
updatedAt,
});
export const updateAlertDeletionSettings = async ({
http,
alertDeletionSettings,
}: {
http: HttpSetup;
alertDeletionSettings: RulesSettingsAlertDeletionProperties;
}) => {
let body: string;
try {
body = JSON.stringify({
is_active_alerts_deletion_enabled: alertDeletionSettings.isActiveAlertsDeletionEnabled,
is_inactive_alerts_deletion_enabled: alertDeletionSettings.isInactiveAlertsDeletionEnabled,
active_alerts_deletion_threshold: alertDeletionSettings.activeAlertsDeletionThreshold,
inactive_alerts_deletion_threshold: alertDeletionSettings.inactiveAlertsDeletionThreshold,
});
} catch (e) {
throw new Error(`Unable to parse alert deletion settings update params: ${e}`);
}
const response = await http.post<AsApiContract<RulesSettingsAlertDeletion>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_alert_deletion`,
{
body,
}
);
return rewriteBodyRes(response);
};

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { HttpSetup } from '@kbn/core/public';
import type { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
import type {
RulesSettingsAlertDeletion,
RulesSettingsAlertDeletionProperties,
} from '@kbn/alerting-types/rule_settings';
const rewriteBodyRes: RewriteRequestCase<RulesSettingsAlertDeletion> = ({ ...rest }: any) => ({
...rest,
});
export const updateAlertsDeletionSettings = async ({
http,
alertsDeletionSettings,
}: {
http: HttpSetup;
alertsDeletionSettings: RulesSettingsAlertDeletionProperties;
}) => {
// TODO: https://github.com/elastic/kibana/issues/209258
const response: AsApiContract<RulesSettingsAlertDeletion> = await new Promise((resolve) => {
resolve({
is_active_alerts_deletion_enabled: alertsDeletionSettings.isActiveAlertsDeletionEnabled,
is_inactive_alerts_deletion_enabled: alertsDeletionSettings.isInactiveAlertsDeletionEnabled,
active_alerts_deletion_threshold: alertsDeletionSettings.activeAlertsDeletionThreshold,
inactive_alerts_deletion_threshold: alertsDeletionSettings.inactiveAlertsDeletionThreshold,
created_by: null,
updated_by: null,
created_at: '2021-08-25T14:00:00.000Z',
updated_at: '2021-08-25T14:00:00.000Z',
});
});
return rewriteBodyRes(response);
};

View file

@ -14,4 +14,4 @@ export * from './fetch_connectors';
export * from './fetch_connector_types';
export * from './fetch_rule_type_aad_template_fields';
export * from './fetch_ui_health_status';
export * from './fetch_alerts_deletion_settings';
export * from './alert_deletion_settings';

View file

@ -10,7 +10,7 @@
import { useQuery } from '@tanstack/react-query';
import type { HttpStart } from '@kbn/core-http-browser';
import type { RulesSettingsAlertDeletion } from '@kbn/alerting-types/rule_settings';
import { fetchAlertsDeletionSettings } from '../apis/fetch_alerts_deletion_settings';
import { fetchAlertDeletionSettings } from '../apis/alert_deletion_settings';
interface Props {
http: HttpStart;
@ -21,7 +21,7 @@ export const useFetchAlertsDeletionSettings = (props: Props) => {
const { http, enabled, onSuccess } = props;
const queryFn = () => {
return fetchAlertsDeletionSettings({ http });
return fetchAlertDeletionSettings({ http });
};
const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({

View file

@ -5,14 +5,24 @@
* 2.0.
*/
export { updateQueryDelaySettingsBodySchema } from './schemas/latest';
export {
updateQueryDelaySettingsBodySchema,
updateAlertDeletionSettingsBodySchema,
} from './schemas/latest';
export type {
UpdateQueryDelaySettingsRequestBody,
UpdateQueryDelaySettingsResponse,
UpdateAlertDeletionSettingsRequestBody,
UpdateAlertDeletionSettingsResponse,
} from './types/latest';
export { updateQueryDelaySettingsBodySchema as updateQueryDelaySettingsBodySchemaV1 } from './schemas/v1';
export {
updateQueryDelaySettingsBodySchema as updateQueryDelaySettingsBodySchemaV1,
updateAlertDeletionSettingsBodySchema as updateAlertDeletionSettingsBodySchemaV1,
} from './schemas/v1';
export type {
UpdateQueryDelaySettingsRequestBody as UpdateQueryDelaySettingsRequestBodyV1,
UpdateQueryDelaySettingsResponse as UpdateQueryDelaySettingsResponseV1,
UpdateAlertDeletionSettingsRequestBody as UpdateAlertDeletionSettingsRequestBodyV1,
UpdateAlertDeletionSettingsResponse as UpdateAlertDeletionSettingsResponseV1,
} from './types/v1';

View file

@ -10,3 +10,33 @@ import { schema } from '@kbn/config-schema';
export const updateQueryDelaySettingsBodySchema = schema.object({
delay: schema.number(),
});
export const updateAlertDeletionSettingsBodySchema = schema.object({
is_active_alerts_deletion_enabled: schema.boolean({
meta: {
description: 'Enable deletion of active alerts when set to true',
},
}),
active_alerts_deletion_threshold: schema.number({
min: 1,
max: 1000,
meta: {
description:
'Threshold (in days) for deleting active alerts older than this value, applies only when deletion is enabled',
},
}),
is_inactive_alerts_deletion_enabled: schema.boolean({
meta: {
description:
'Enable deletion of inactive alerts (recovered/closed/untracked) when set to true',
},
}),
inactive_alerts_deletion_threshold: schema.number({
min: 1,
max: 1000,
meta: {
description:
'Threshold (in days) for deleting inactive alerts (recovered/closed/untracked) older than this value, applies only when deletion is enabled',
},
}),
});

View file

@ -6,11 +6,24 @@
*/
import type { TypeOf } from '@kbn/config-schema';
import type { queryDelaySettingsResponseSchemaV1 } from '../../../response';
import type { updateQueryDelaySettingsBodySchemaV1 } from '..';
import type {
queryDelaySettingsResponseSchemaV1,
alertDeletionSettingsResponseSchemaV1,
} from '../../../response';
import type {
updateQueryDelaySettingsBodySchemaV1,
updateAlertDeletionSettingsBodySchemaV1,
} from '..';
export type UpdateQueryDelaySettingsRequestBody = TypeOf<
typeof updateQueryDelaySettingsBodySchemaV1
>;
export type UpdateQueryDelaySettingsResponse = TypeOf<typeof queryDelaySettingsResponseSchemaV1>;
export type UpdateAlertDeletionSettingsRequestBody = TypeOf<
typeof updateAlertDeletionSettingsBodySchemaV1
>;
export type UpdateAlertDeletionSettingsResponse = TypeOf<
typeof alertDeletionSettingsResponseSchemaV1
>;

View file

@ -7,6 +7,10 @@
export { queryDelaySettingsResponseSchema } from './schemas/latest';
export type { QueryDelaySettingsResponse } from './types/latest';
export { alertDeletionSettingsResponseSchema } from './schemas/latest';
export type { AlertDeletionSettingsResponse } from './types/latest';
export { queryDelaySettingsResponseSchema as queryDelaySettingsResponseSchemaV1 } from './schemas/v1';
export type { QueryDelaySettingsResponse as QueryDelaySettingsResponseV1 } from './types/v1';
export { alertDeletionSettingsResponseSchema as alertDeletionSettingsResponseSchemaV1 } from './schemas/v1';
export type { AlertDeletionSettingsResponse as AlertDeletionSettingsResponseV1 } from './types/v1';

View file

@ -18,3 +18,18 @@ export const queryDelaySettingsResponseBodySchema = schema.object({
export const queryDelaySettingsResponseSchema = schema.object({
body: queryDelaySettingsResponseBodySchema,
});
export const alertDeletionSettingsResponseBodySchema = schema.object({
active_alerts_deletion_threshold: schema.number({ min: 1, max: 1000 }),
is_active_alerts_deletion_enabled: schema.boolean(),
inactive_alerts_deletion_threshold: schema.number({ min: 1, max: 1000 }),
is_inactive_alerts_deletion_enabled: schema.boolean(),
created_at: schema.string(),
created_by: schema.nullable(schema.string()),
updated_at: schema.string(),
updated_by: schema.nullable(schema.string()),
});
export const alertDeletionSettingsResponseSchema = schema.object({
body: alertDeletionSettingsResponseBodySchema,
});

View file

@ -6,6 +6,7 @@
*/
import type { TypeOf } from '@kbn/config-schema';
import type { queryDelaySettingsResponseSchemaV1 } from '..';
import type { alertDeletionSettingsResponseSchemaV1, queryDelaySettingsResponseSchemaV1 } from '..';
export type QueryDelaySettingsResponse = TypeOf<typeof queryDelaySettingsResponseSchemaV1>;
export type AlertDeletionSettingsResponse = TypeOf<typeof alertDeletionSettingsResponseSchemaV1>;

View file

@ -46,6 +46,8 @@ import { bulkDisableRulesRoute } from './rule/apis/bulk_disable/bulk_disable_rul
import { cloneRuleRoute } from './rule/apis/clone/clone_rule_route';
import { getFlappingSettingsRoute } from './get_flapping_settings';
import { updateFlappingSettingsRoute } from './update_flapping_settings';
import { getAlertDeletionSettingsRoute } from './rules_settings/apis/get/get_alert_deletion_settings';
import { updateAlertDeletionSettingsRoute } from './rules_settings/apis/update/update_alert_deletion_settings';
import { getRuleTagsRoute } from './rule/apis/tags/get_rule_tags';
import { getScheduleFrequencyRoute } from './rule/apis/get_schedule_frequency';
import { bulkUntrackAlertsRoute } from './rule/apis/bulk_untrack';
@ -166,6 +168,8 @@ export function defineRoutes(opts: RouteOptions) {
getActionErrorLogRoute(router, licenseState);
getFlappingSettingsRoute(router, licenseState);
updateFlappingSettingsRoute(router, licenseState);
getAlertDeletionSettingsRoute(router, licenseState);
updateAlertDeletionSettingsRoute(router, licenseState);
runSoonRoute(router, licenseState);
healthRoute(router, licenseState, encryptedSavedObjects);
getGlobalExecutionKPIRoute(router, licenseState);

View file

@ -0,0 +1,81 @@
/*
* 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 { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../../lib/license_state.mock';
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
import type { RulesSettingsClientMock } from '../../../../rules_settings/rules_settings_client.mock';
import { rulesSettingsClientMock } from '../../../../rules_settings/rules_settings_client.mock';
import { getAlertDeletionSettingsRoute } from './get_alert_deletion_settings';
let rulesSettingsClient: RulesSettingsClientMock;
jest.mock('../../../../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
rulesSettingsClient = rulesSettingsClientMock.create();
});
describe('getAlertDeletionSettingsRoute', () => {
test('gets alert deletion settings', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getAlertDeletionSettingsRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config).toMatchInlineSnapshot(`
Object {
"options": Object {
"access": "internal",
},
"path": "/internal/alerting/rules/settings/_alert_deletion",
"security": Object {
"authz": Object {
"requiredPrivileges": Array [
"read-alert-deletion-settings",
],
},
},
"validate": false,
}
`);
(rulesSettingsClient.alertDeletion().get as jest.Mock).mockResolvedValue({
isActiveAlertsDeletionEnabled: true,
isInactiveAlertsDeletionEnabled: false,
activeAlertsDeletionThreshold: 10,
inactiveAlertsDeletionThreshold: 90,
createdBy: 'test name',
updatedBy: 'test name',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const [context, req, res] = mockHandlerArguments({ rulesSettingsClient }, {}, ['ok']);
await handler(context, req, res);
expect(rulesSettingsClient.alertDeletion().get).toHaveBeenCalledTimes(1);
expect(res.ok).toHaveBeenCalledWith({
body: expect.objectContaining({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 10,
inactive_alerts_deletion_threshold: 90,
created_by: 'test name',
updated_by: 'test name',
created_at: expect.any(String),
updated_at: expect.any(String),
}),
});
});
});

View 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 type { IRouter } from '@kbn/core/server';
import type { ILicenseState } from '../../../../lib';
import type { AlertingRequestHandlerContext } from '../../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
import { verifyAccessAndContext } from '../../../lib';
import { API_PRIVILEGES } from '../../../../../common';
import { transformAlertDeletionSettingsToResponseV1 } from '../../transforms';
export const getAlertDeletionSettingsRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_alert_deletion`,
validate: false,
security: {
authz: {
requiredPrivileges: [`${API_PRIVILEGES.READ_ALERT_DELETION_SETTINGS}`],
},
},
options: {
access: 'internal',
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesSettingsClient = (await context.alerting).getRulesSettingsClient();
const alertDeletionSettings = await rulesSettingsClient.alertDeletion().get();
return res.ok(transformAlertDeletionSettingsToResponseV1(alertDeletionSettings));
})
)
);
};

View file

@ -0,0 +1,104 @@
/*
* 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 { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../../lib/license_state.mock';
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
import type { RulesSettingsClientMock } from '../../../../rules_settings/rules_settings_client.mock';
import { rulesSettingsClientMock } from '../../../../rules_settings/rules_settings_client.mock';
import { updateAlertDeletionSettingsRoute } from './update_alert_deletion_settings';
let rulesSettingsClient: RulesSettingsClientMock;
jest.mock('../../../../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
rulesSettingsClient = rulesSettingsClientMock.create();
});
const mockAlertDeletionSettings = {
isActiveAlertsDeletionEnabled: true,
isInactiveAlertsDeletionEnabled: true,
activeAlertsDeletionThreshold: 90,
inactiveAlertsDeletionThreshold: 60,
createdBy: 'test name',
updatedBy: 'test name',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
describe('updateAlertDeletionSettingsRoute', () => {
test('updates alert deletion settings', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
updateAlertDeletionSettingsRoute(router, licenseState);
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(
`"/internal/alerting/rules/settings/_alert_deletion"`
);
expect(config.options).toMatchInlineSnapshot(`
Object {
"access": "internal",
}
`);
(rulesSettingsClient.alertDeletion().get as jest.Mock).mockResolvedValue(
mockAlertDeletionSettings
);
(rulesSettingsClient.alertDeletion().update as jest.Mock).mockResolvedValue(
mockAlertDeletionSettings
);
const updateResult = {
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: true,
active_alerts_deletion_threshold: 90,
inactive_alerts_deletion_threshold: 60,
};
const [context, req, res] = mockHandlerArguments(
{ rulesSettingsClient },
{
body: updateResult,
},
['ok']
);
await handler(context, req, res);
expect(rulesSettingsClient.alertDeletion().update).toHaveBeenCalledTimes(1);
expect((rulesSettingsClient.alertDeletion().update as jest.Mock).mock.calls[0])
.toMatchInlineSnapshot(`
Array [
Object {
"activeAlertsDeletionThreshold": 90,
"inactiveAlertsDeletionThreshold": 60,
"isActiveAlertsDeletionEnabled": true,
"isInactiveAlertsDeletionEnabled": true,
},
]
`);
expect(res.ok).toHaveBeenCalledWith({
body: expect.objectContaining({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: true,
active_alerts_deletion_threshold: 90,
inactive_alerts_deletion_threshold: 60,
created_by: 'test name',
updated_by: 'test name',
created_at: expect.any(String),
updated_at: expect.any(String),
}),
});
});
});

View file

@ -0,0 +1,49 @@
/*
* 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 { IRouter } from '@kbn/core/server';
import { updateAlertDeletionSettingsBodySchemaV1 } from '../../../../../common/routes/rules_settings/apis/update';
import type { ILicenseState } from '../../../../lib';
import { verifyAccessAndContext } from '../../../lib';
import type { AlertingRequestHandlerContext } from '../../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
import { API_PRIVILEGES } from '../../../../../common';
import {
transformAlertDeletionSettingsRequestV1,
transformAlertDeletionSettingsToResponseV1,
} from '../../transforms';
export const updateAlertDeletionSettingsRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_alert_deletion`,
validate: {
body: updateAlertDeletionSettingsBodySchemaV1,
},
security: {
authz: {
requiredPrivileges: [`${API_PRIVILEGES.WRITE_ALERT_DELETION_SETTINGS}`],
},
},
options: {
access: 'internal',
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesSettingsClient = (await context.alerting).getRulesSettingsClient();
const body = transformAlertDeletionSettingsRequestV1(req.body);
const updatedAlertDeletionSettings = await rulesSettingsClient.alertDeletion().update(body);
return res.ok(transformAlertDeletionSettingsToResponseV1(updatedAlertDeletionSettings));
})
)
);
};

View file

@ -6,5 +6,9 @@
*/
export { transformQueryDelaySettingsToResponse } from './transform_query_delay_settings_to_response/latest';
export { transformAlertDeletionSettingsToResponse } from './transform_alert_deletion_settings_to_response/latest';
export { transformAlertDeletionSettingsRequest } from './transform_alert_deletion_settings_request/latest';
export { transformQueryDelaySettingsToResponse as transformQueryDelaySettingsToResponseV1 } from './transform_query_delay_settings_to_response/v1';
export { transformAlertDeletionSettingsToResponse as transformAlertDeletionSettingsToResponseV1 } from './transform_alert_deletion_settings_to_response/latest';
export { transformAlertDeletionSettingsRequest as transformAlertDeletionSettingsRequestV1 } from './transform_alert_deletion_settings_request/latest';

View file

@ -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 * from './v1';

View file

@ -0,0 +1,25 @@
/*
* 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 { RewriteRequestCase } from '@kbn/actions-types';
import type { RulesSettingsAlertDeletionProperties } from '@kbn/alerting-types';
export const transformAlertDeletionSettingsRequest: RewriteRequestCase<
RulesSettingsAlertDeletionProperties
> = ({
active_alerts_deletion_threshold: activeAlertsDeletionThreshold,
is_active_alerts_deletion_enabled: isActiveAlertsDeletionEnabled,
inactive_alerts_deletion_threshold: inactiveAlertsDeletionThreshold,
is_inactive_alerts_deletion_enabled: isInactiveAlertsDeletionEnabled,
}) => {
return {
activeAlertsDeletionThreshold,
isActiveAlertsDeletionEnabled,
inactiveAlertsDeletionThreshold,
isInactiveAlertsDeletionEnabled,
};
};

View file

@ -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 * from './v1';

View file

@ -0,0 +1,26 @@
/*
* 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 { RulesSettingsAlertDeletion } from '@kbn/alerting-types';
import type { AlertDeletionSettingsResponseV1 } from '../../../../../common/routes/rules_settings/response';
export const transformAlertDeletionSettingsToResponse = (
settings: RulesSettingsAlertDeletion
): AlertDeletionSettingsResponseV1 => {
return {
body: {
active_alerts_deletion_threshold: settings.activeAlertsDeletionThreshold,
is_active_alerts_deletion_enabled: settings.isActiveAlertsDeletionEnabled,
inactive_alerts_deletion_threshold: settings.inactiveAlertsDeletionThreshold,
is_inactive_alerts_deletion_enabled: settings.isInactiveAlertsDeletionEnabled,
created_at: settings.createdAt,
created_by: settings.createdBy,
updated_at: settings.updatedAt,
updated_by: settings.updatedBy,
},
};
};

View file

@ -75,7 +75,8 @@
"@kbn/core-saved-objects-base-server-internal",
"@kbn/core-security-server-mocks",
"@kbn/core-http-server-utils",
"@kbn/response-ops-rule-params"
"@kbn/response-ops-rule-params",
"@kbn/actions-types"
],
"exclude": [
"target/**/*"

View file

@ -21,8 +21,8 @@ import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_setti
import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
import { updateQueryDelaySettings } from '../../lib/rule_api/update_query_delay_settings';
import { getIsExperimentalFeatureEnabled } from '../../../common/get_experimental_features';
import { updateAlertsDeletionSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_deletion_settings/update_alerts_deletion_settings';
import { fetchAlertsDeletionSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_deletion_settings/fetch_alerts_deletion_settings';
import { updateAlertDeletionSettings } from '@kbn/alerts-ui-shared/src/common/apis/alert_deletion_settings/update_alert_deletion_settings';
import { fetchAlertDeletionSettings } from '@kbn/alerts-ui-shared/src/common/apis/alert_deletion_settings/fetch_alert_deletion_settings';
jest.mock('../../../common/lib/kibana');
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
@ -42,15 +42,15 @@ jest.mock('../../../common/get_experimental_features', () => ({
}));
jest.mock(
'@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_deletion_settings/fetch_alerts_deletion_settings',
'@kbn/alerts-ui-shared/src/common/apis/alert_deletion_settings/fetch_alert_deletion_settings',
() => ({
fetchAlertsDeletionSettings: jest.fn(),
fetchAlertDeletionSettings: jest.fn(),
})
);
jest.mock(
'@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_deletion_settings/update_alerts_deletion_settings',
'@kbn/alerts-ui-shared/src/common/apis/alert_deletion_settings/update_alert_deletion_settings',
() => ({
updateAlertsDeletionSettings: jest.fn(),
updateAlertDeletionSettings: jest.fn(),
})
);
@ -82,13 +82,12 @@ const updateQueryDelaySettingsMock = updateQueryDelaySettings as unknown as jest
const getIsExperimentalFeatureEnabledMock = getIsExperimentalFeatureEnabled as jest.MockedFunction<
typeof getIsExperimentalFeatureEnabled
>;
const updateAlertsDeletionSettingsMock =
updateAlertsDeletionSettings as unknown as jest.MockedFunction<
typeof updateAlertsDeletionSettings
>;
const updateAlertDeletionSettingsMock =
updateAlertDeletionSettings as unknown as jest.MockedFunction<typeof updateAlertDeletionSettings>;
const fetchAlertsDeletionSettingsMock =
fetchAlertsDeletionSettings as unknown as jest.MockedFunction<typeof fetchAlertsDeletionSettings>;
const fetchAlertDeletionSettingsMock = fetchAlertDeletionSettings as unknown as jest.MockedFunction<
typeof fetchAlertDeletionSettings
>;
const mockFlappingSetting: RulesSettingsFlapping = {
enabled: true,
@ -189,7 +188,7 @@ describe('rules_settings_modal', () => {
updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
updateQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
fetchAlertsDeletionSettingsMock.mockResolvedValue(mockAlertDeletionSetting);
fetchAlertDeletionSettingsMock.mockResolvedValue(mockAlertDeletionSetting);
getIsExperimentalFeatureEnabledMock.mockReturnValue(true);
});
@ -273,7 +272,7 @@ describe('rules_settings_modal', () => {
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(updateFlappingSettingsMock).not.toHaveBeenCalled();
expect(updateAlertsDeletionSettingsMock).not.toHaveBeenCalled();
expect(updateAlertDeletionSettingsMock).not.toHaveBeenCalled();
expect(modalProps.onSave).not.toHaveBeenCalled();
expect(screen.queryByTestId('centerJustifiedSpinner')).toBe(null);
@ -283,7 +282,7 @@ describe('rules_settings_modal', () => {
expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1);
expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
expect(fetchAlertsDeletionSettingsMock).toHaveBeenCalledTimes(1);
expect(fetchAlertDeletionSettingsMock).toHaveBeenCalledTimes(1);
});
test('should prevent statusChangeThreshold from being greater than lookBackWindow', async () => {
@ -507,7 +506,7 @@ describe('rules_settings_modal', () => {
jest.clearAllMocks();
user = userEvent.setup();
getIsExperimentalFeatureEnabledMock.mockReturnValue(true);
updateAlertsDeletionSettingsMock.mockResolvedValue(mockAlertDeletionSetting);
updateAlertDeletionSettingsMock.mockResolvedValue(mockAlertDeletionSetting);
getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
});
@ -535,9 +534,9 @@ describe('rules_settings_modal', () => {
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
});
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(updateAlertsDeletionSettingsMock).toHaveBeenCalledWith(
expect(updateAlertDeletionSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({
alertsDeletionSettings: {
alertDeletionSettings: {
isActiveAlertsDeletionEnabled: true,
isInactiveAlertsDeletionEnabled: true,
activeAlertsDeletionThreshold: 5,
@ -551,7 +550,7 @@ describe('rules_settings_modal', () => {
});
test('should show error message if it fails', async () => {
fetchAlertsDeletionSettingsMock.mockRejectedValue('failed!');
fetchAlertDeletionSettingsMock.mockRejectedValue('failed!');
render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();

View file

@ -119,7 +119,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
useResettableState<RulesSettingsQueryDelayProperties>();
const [
alertsDeletionSettings,
alertDeletionSettings,
hasAlertsDeletionChanged,
setAlertsDeletionSettings,
resetAlertsDeletionSettings,
@ -161,7 +161,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
http,
enabled: isVisible && isAlertsDeletionSettingsEnabled,
onSuccess: (fetchedSettings) => {
if (!alertsDeletionSettings) {
if (!alertDeletionSettings) {
setAlertsDeletionSettings(
{
isActiveAlertsDeletionEnabled: fetchedSettings.isActiveAlertsDeletionEnabled,
@ -236,11 +236,11 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
}
if (setting === 'alertDeletion') {
if (!alertsDeletionSettings) {
if (!alertDeletionSettings) {
return;
}
const newSettings = {
...alertsDeletionSettings,
...alertDeletionSettings,
[key]: value,
};
setAlertsDeletionSettings(newSettings);
@ -258,8 +258,8 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
setQueryDelaySettings(queryDelaySettings!, true);
}
if (canWriteAlertsDeletionSettings && hasAlertsDeletionChanged) {
updatedSettings.alertDeletion = alertsDeletionSettings;
setAlertsDeletionSettings(alertsDeletionSettings!, true);
updatedSettings.alertDeletion = alertDeletionSettings;
setAlertsDeletionSettings(alertDeletionSettings!, true);
}
mutate(updatedSettings);
@ -304,7 +304,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
<EuiSpacer />
<RulesSettingsAlertsDeletionSection
onChange={(key, value) => handleSettingsChange('alertDeletion', key, value)}
settings={alertsDeletionSettings}
settings={alertDeletionSettings}
canWrite={canWriteAlertsDeletionSettings}
hasError={hasAlertsDeletionError}
/>

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { useMutation } from '@tanstack/react-query';
import type { RulesSettingsProperties } from '@kbn/alerting-plugin/common';
import { updateAlertsDeletionSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_deletion_settings';
import { updateAlertDeletionSettings } from '@kbn/alerts-ui-shared/src/common/apis/alert_deletion_settings';
import { useKibana } from '../../common/lib/kibana';
import { updateFlappingSettings } from '../lib/rule_api/update_flapping_settings';
import { updateQueryDelaySettings } from '../lib/rule_api/update_query_delay_settings';
@ -39,7 +39,7 @@ export const useUpdateRuleSettings = (props: UseUpdateRuleSettingsProps) => {
if (settings.alertDeletion) {
updates.push(
updateAlertsDeletionSettings({ http, alertsDeletionSettings: settings.alertDeletion })
updateAlertDeletionSettings({ http, alertDeletionSettings: settings.alertDeletion })
);
}

View file

@ -0,0 +1,78 @@
/*
* 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 expect from '@kbn/expect';
import { DEFAULT_ALERT_DELETION_SETTINGS } from '@kbn/alerting-plugin/common';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function getAlertDeletionSettingsTests({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('getAlertDeletionSettings', () => {
beforeEach(async () => {
await resetRulesSettings(supertestWithoutAuth, 'space1');
await resetRulesSettings(supertestWithoutAuth, 'space2');
});
after(async () => {
await resetRulesSettings(supertestWithoutAuth, 'space1');
await resetRulesSettings(supertestWithoutAuth, 'space2');
});
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle get alert deletion request appropriately', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_alert_deletion`)
.auth(user.username, user.password);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all_with_restricted_fixture at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message:
'API [GET /internal/alerting/rules/settings/_alert_deletion] is unauthorized for user, this action is granted by the Kibana privileges [read-alert-deletion-settings]',
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.is_active_alerts_deletion_enabled).to.eql(
DEFAULT_ALERT_DELETION_SETTINGS.isActiveAlertsDeletionEnabled
);
expect(response.body.is_inactive_alerts_deletion_enabled).to.eql(
DEFAULT_ALERT_DELETION_SETTINGS.isInactiveAlertsDeletionEnabled
);
expect(response.body.active_alerts_deletion_threshold).to.eql(
DEFAULT_ALERT_DELETION_SETTINGS.activeAlertsDeletionThreshold
);
expect(response.body.inactive_alerts_deletion_threshold).to.eql(
DEFAULT_ALERT_DELETION_SETTINGS.inactiveAlertsDeletionThreshold
);
expect(response.body.updated_by).to.be.a('string');
expect(response.body.created_by).to.be.a('string');
expect(Date.parse(response.body.created_at)).to.be.greaterThan(0);
expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});
}

View file

@ -34,6 +34,8 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./get_query_delay_settings'));
loadTestFile(require.resolve('./update_query_delay_settings'));
loadTestFile(require.resolve('./resolve'));
loadTestFile(require.resolve('./get_alert_deletion_settings'));
loadTestFile(require.resolve('./update_alert_deletion_settings'));
});
});
}

View file

@ -0,0 +1,168 @@
/*
* 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 expect from '@kbn/expect';
import { UserAtSpaceScenarios, Superuser } from '../../../scenarios';
import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function updateAlertDeletionSettingsTest({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('updateAlertDeletionSettings', () => {
afterEach(async () => {
await resetRulesSettings(supertestWithoutAuth, 'space1');
await resetRulesSettings(supertestWithoutAuth, 'space2');
});
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle update alert deletion settings request appropriately', async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_alert_deletion`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 70,
inactive_alerts_deletion_threshold: 50,
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
case 'space_1_all_with_restricted_fixture at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message:
'API [POST /internal/alerting/rules/settings/_alert_deletion] is unauthorized for user, this action is granted by the Kibana privileges [write-alert-deletion-settings]',
statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.is_active_alerts_deletion_enabled).to.eql(true);
expect(response.body.is_inactive_alerts_deletion_enabled).to.eql(false);
expect(response.body.active_alerts_deletion_threshold).to.eql(70);
expect(response.body.inactive_alerts_deletion_threshold).to.eql(50);
expect(Date.parse(response.body.created_at)).to.be.greaterThan(0);
expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
it('should error if provided with invalid inputs', async () => {
let response = await supertestWithoutAuth
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_alert_deletion`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 2000,
inactive_alerts_deletion_threshold: 50,
})
.expect(400);
expect(response.body.message).to.eql(
'[request body.active_alerts_deletion_threshold]: Value must be equal to or lower than [1000].'
);
response = await supertestWithoutAuth
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_alert_deletion`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 70,
inactive_alerts_deletion_threshold: 2000,
})
.expect(400);
expect(response.body.message).to.eql(
'[request body.inactive_alerts_deletion_threshold]: Value must be equal to or lower than [1000].'
);
response = await supertestWithoutAuth
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_alert_deletion`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 0,
inactive_alerts_deletion_threshold: 20,
})
.expect(400);
expect(response.body.message).to.eql(
'[request body.active_alerts_deletion_threshold]: Value must be equal to or greater than [1].'
);
response = await supertestWithoutAuth
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_alert_deletion`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: false,
active_alerts_deletion_threshold: 20,
inactive_alerts_deletion_threshold: 0,
})
.expect(400);
expect(response.body.message).to.eql(
'[request body.inactive_alerts_deletion_threshold]: Value must be equal to or greater than [1].'
);
});
describe('updateAlertDeletionSettings for other spaces', () => {
it('should update specific isolated settings depending on space', async () => {
// Update the rules setting in space1
const postResponse = await supertestWithoutAuth
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/settings/_alert_deletion`)
.set('kbn-xsrf', 'foo')
.auth(Superuser.username, Superuser.password)
.send({
is_active_alerts_deletion_enabled: true,
is_inactive_alerts_deletion_enabled: true,
active_alerts_deletion_threshold: 10,
inactive_alerts_deletion_threshold: 100,
});
expect(postResponse.statusCode).to.eql(200);
expect(postResponse.body.is_active_alerts_deletion_enabled).to.eql(true);
expect(postResponse.body.is_inactive_alerts_deletion_enabled).to.eql(true);
expect(postResponse.body.active_alerts_deletion_threshold).to.eql(10);
expect(postResponse.body.inactive_alerts_deletion_threshold).to.eql(100);
// Get the rules settings in space2
const getResponse = await supertestWithoutAuth
.get(`${getUrlPrefix('space2')}/internal/alerting/rules/settings/_alert_deletion`)
.auth(Superuser.username, Superuser.password);
expect(getResponse.statusCode).to.eql(200);
expect(getResponse.body.is_active_alerts_deletion_enabled).to.eql(false);
expect(getResponse.body.is_active_alerts_deletion_enabled).to.eql(false);
expect(getResponse.body.active_alerts_deletion_threshold).to.eql(90);
expect(getResponse.body.inactive_alerts_deletion_threshold).to.eql(90);
});
});
});
}