From 70968d095da8cf6b1628ecb8974b42fb0bf659ed Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 2 Feb 2024 18:18:58 +0100 Subject: [PATCH] [ML] Prompt the user to delete alerting rules upon the anomaly detection job deletion (#176049) ## Summary Closes #174513 - Adds a control to the job deletion dialog for deleting associated alerting rules - Update the delete Kibana endpoint with a flag to delete alerting rules image ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../delete_job_modal/delete_job_modal.tsx | 41 +++++++++++++++---- .../reset_job_modal/reset_job_modal.tsx | 2 +- .../jobs/jobs_list/components/utils.d.ts | 1 + .../jobs/jobs_list/components/utils.js | 4 +- .../application/services/job_service.js | 4 +- .../services/ml_api_service/jobs.ts | 4 +- .../ml/server/models/job_service/jobs.ts | 29 ++++++++++++- .../plugins/ml/server/routes/job_service.ts | 12 ++++-- .../routes/schemas/job_service_schema.ts | 1 + 9 files changed, 78 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx index d840d2e016d5e..71a6216cbf5df 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useState, useEffect, useCallback } from 'react'; +import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, @@ -40,10 +40,10 @@ interface Props { export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, refreshJobs }) => { const [deleting, setDeleting] = useState(false); const [modalVisible, setModalVisible] = useState(false); - const [jobIds, setJobIds] = useState([]); + const [adJobs, setAdJobs] = useState([]); const [canDelete, setCanDelete] = useState(false); - const [hasManagedJob, setHasManagedJob] = useState(false); const [deleteUserAnnotations, setDeleteUserAnnotations] = useState(false); + const [deleteAlertingRules, setDeleteAlertingRules] = useState(false); useEffect(() => { if (typeof setShowFunction === 'function') { @@ -58,13 +58,22 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, }, []); const showModal = useCallback((jobs: MlSummaryJob[]) => { - setJobIds(jobs.map(({ id }) => id)); - setHasManagedJob(jobs.some((job) => isManagedJob(job))); + setAdJobs(jobs); setModalVisible(true); setDeleting(false); setDeleteUserAnnotations(false); }, []); + const { jobIds, hasManagedJob, hasAlertingRules } = useMemo(() => { + return { + jobIds: adJobs.map(({ id }) => id), + hasManagedJob: adJobs.some((job) => isManagedJob(job)), + hasAlertingRules: adJobs.some( + (job) => Array.isArray(job.alertingRules) && job.alertingRules.length > 0 + ), + }; + }, [adJobs]); + const closeModal = useCallback(() => { setModalVisible(false); setCanDelete(false); @@ -74,14 +83,15 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, setDeleting(true); deleteJobs( jobIds.map((id) => ({ id })), - deleteUserAnnotations + deleteUserAnnotations, + deleteAlertingRules ); setTimeout(() => { closeModal(); refreshJobs(); }, DELETING_JOBS_REFRESH_INTERVAL_MS); - }, [jobIds, deleteUserAnnotations, closeModal, refreshJobs]); + }, [jobIds, deleteUserAnnotations, deleteAlertingRules, closeModal, refreshJobs]); if (modalVisible === false || jobIds.length === 0) { return null; @@ -143,13 +153,28 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, label={i18n.translate( 'xpack.ml.jobsList.deleteJobModal.deleteUserAnnotations', { - defaultMessage: 'Delete annotations.', + defaultMessage: 'Delete annotations', } )} checked={deleteUserAnnotations} onChange={(e) => setDeleteUserAnnotations(e.target.checked)} data-test-subj="mlDeleteJobConfirmModalDeleteAnnotationsSwitch" /> + {hasAlertingRules ? ( + <> + + setDeleteAlertingRules(e.target.checked)} + /> + + ) : null} )} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx index 335b19bd3a794..a31cb3cc898ab 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx @@ -129,7 +129,7 @@ export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, r setDeleteUserAnnotations(e.target.checked)} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 8358afc3b0ef8..d3b2b29dafb06 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -12,6 +12,7 @@ export function closeJobs(jobs: Array<{ id: string }>, callback?: () => void): P export function deleteJobs( jobs: Array<{ id: string }>, deleteUserAnnotations?: boolean, + deleteAlertingRules?: boolean, callback?: () => void ): Promise; export function resetJobs( diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index ffa9e8eecf0ee..86ed4a125aead 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -326,10 +326,10 @@ export function resetJobs(jobIds, deleteUserAnnotations, finish = () => {}) { }); } -export function deleteJobs(jobs, deleteUserAnnotations, finish = () => {}) { +export function deleteJobs(jobs, deleteUserAnnotations, deleteAlertingRules, finish = () => {}) { const jobIds = jobs.map((j) => j.id); mlJobService - .deleteJobs(jobIds, deleteUserAnnotations) + .deleteJobs(jobIds, deleteUserAnnotations, deleteAlertingRules) .then((resp) => { showResults(resp, JOB_STATE.DELETED); finish(); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 5f2be59156762..0fc78db628fcb 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -335,8 +335,8 @@ class JobService { return ml.jobs.stopDatafeeds(dIds); } - deleteJobs(jIds, deleteUserAnnotations) { - return ml.jobs.deleteJobs(jIds, deleteUserAnnotations); + deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules) { + return ml.jobs.deleteJobs(jIds, deleteUserAnnotations, deleteAlertingRules); } closeJobs(jIds) { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 0b79dfc2dd990..89d7898a2626d 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -130,8 +130,8 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, - deleteJobs(jobIds: string[], deleteUserAnnotations?: boolean) { - const body = JSON.stringify({ jobIds, deleteUserAnnotations }); + deleteJobs(jobIds: string[], deleteUserAnnotations?: boolean, deleteAlertingRules?: boolean) { + const body = JSON.stringify({ jobIds, deleteUserAnnotations, deleteAlertingRules }); return httpService.http({ path: `${ML_INTERNAL_BASE_PATH}/jobs/delete_jobs`, method: 'POST', diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 669b1d4b737d7..87a719816896a 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -84,10 +84,37 @@ export function jobsProvider( }); } - async function deleteJobs(jobIds: string[], deleteUserAnnotations = false) { + async function deleteJobs( + jobIds: string[], + deleteUserAnnotations = false, + deleteAlertingRules = false + ) { const results: Results = {}; const datafeedIds = await getDatafeedIdsByJobId(); + if (deleteAlertingRules && rulesClient) { + // Check what jobs have associated alerting rules + const anomalyDetectionAlertingRules = await rulesClient.find({ + options: { + filter: `alert.attributes.alertTypeId:${ML_ALERT_TYPES.ANOMALY_DETECTION}`, + perPage: 10000, + }, + }); + + const jobIdsSet = new Set(jobIds); + const ruleIds: string[] = anomalyDetectionAlertingRules.data + .filter((rule) => { + return jobIdsSet.has(rule.params.jobSelection.jobIds[0]); + }) + .map((rule) => rule.id); + + if (ruleIds.length > 0) { + await rulesClient.bulkDeleteRules({ + ids: ruleIds, + }); + } + } + for (const jobId of jobIds) { try { const datafeedResp = diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index b7fad0ef66ce2..ef4ed3670ab66 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -141,11 +141,15 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { tags: ['access:ml:canDeleteJob'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => { try { - const { deleteJobs } = jobServiceProvider(client, mlClient); - const { jobIds, deleteUserAnnotations } = request.body; - const resp = await deleteJobs(jobIds, deleteUserAnnotations); + const alerting = await context.alerting; + const rulesClient = alerting?.getRulesClient(); + const { deleteJobs } = jobServiceProvider(client, mlClient, rulesClient); + + const { jobIds, deleteUserAnnotations, deleteAlertingRules } = request.body; + + const resp = await deleteJobs(jobIds, deleteUserAnnotations, deleteAlertingRules); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index ea9c95d4ad375..008016a211630 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -70,6 +70,7 @@ export const deleteJobsSchema = schema.object({ /** List of job IDs. */ jobIds: schema.arrayOf(schema.string()), deleteUserAnnotations: schema.maybe(schema.boolean()), + deleteAlertingRules: schema.maybe(schema.boolean()), }); export const optionalJobIdsSchema = schema.object({