Skip to content

Commit

Permalink
[ML] Prompt the user to delete alerting rules upon the anomaly detect…
Browse files Browse the repository at this point in the history
…ion job deletion (elastic#176049)

## Summary

Closes elastic#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

<img width="1360" alt="image"
src="https://github.com/elastic/kibana/assets/5236598/3d22b9a6-9203-4583-b117-dfcd9087f373">

### 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)
  • Loading branch information
darnautov authored and fkanout committed Feb 7, 2024
1 parent ce1bdc7 commit 70968d0
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,10 +40,10 @@ interface Props {
export const DeleteJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction, refreshJobs }) => {
const [deleting, setDeleting] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [jobIds, setJobIds] = useState<string[]>([]);
const [adJobs, setAdJobs] = useState<MlSummaryJob[]>([]);
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') {
Expand All @@ -58,13 +58,22 @@ export const DeleteJobModal: FC<Props> = ({ 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);
Expand All @@ -74,14 +83,15 @@ export const DeleteJobModal: FC<Props> = ({ 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;
Expand Down Expand Up @@ -143,13 +153,28 @@ export const DeleteJobModal: FC<Props> = ({ 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 ? (
<>
<EuiSpacer size={'s'} />
<EuiSwitch
label={i18n.translate(
'xpack.ml.jobsList.resetJobModal.deleteAlertingRules',
{
defaultMessage: 'Delete alerting rules',
}
)}
checked={deleteAlertingRules}
onChange={(e) => setDeleteAlertingRules(e.target.checked)}
/>
</>
) : null}
</EuiText>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const ResetJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction, r
<EuiSpacer />
<EuiSwitch
label={i18n.translate('xpack.ml.jobsList.resetJobModal.deleteUserAnnotations', {
defaultMessage: 'Delete annotations.',
defaultMessage: 'Delete annotations',
})}
checked={deleteUserAnnotations}
onChange={(e) => setDeleteUserAnnotations(e.target.checked)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
export function resetJobs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/ml/public/application/services/job_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>({
path: `${ML_INTERNAL_BASE_PATH}/jobs/delete_jobs`,
method: 'POST',
Expand Down
29 changes: 28 additions & 1 deletion x-pack/plugins/ml/server/models/job_service/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MlAnomalyDetectionAlertParams>({
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 =
Expand Down
12 changes: 8 additions & 4 deletions x-pack/plugins/ml/server/routes/job_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 70968d0

Please sign in to comment.