[ML] Adding close jobs menu item (#20927)

* [ML] Adding close jobs menu item

* changing icon

* updates based on review

* adding extra check for close failure

* adding extra guard against missing response
This commit is contained in:
James Gowdy 2018-07-18 17:47:44 +01:00 committed by GitHub
parent 4445c58988
commit 3c74d30bee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 5 deletions

View file

@ -10,8 +10,10 @@ import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import {
stopDatafeeds,
cloneJob,
closeJobs,
isStartable,
isStoppable,
isClosable,
} from '../utils';
export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showStartDatafeedModal, refreshJobs) {
@ -20,6 +22,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
const canDeleteJob = checkPermission('canDeleteJob');
const canUpdateDatafeed = checkPermission('canUpdateDatafeed');
const canStartStopDatafeed = (checkPermission('canStartStopDatafeed') && mlNodesAvailable());
const canCloseJob = (checkPermission('canCloseJob') && mlNodesAvailable());
return [
{
@ -42,6 +45,16 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
stopDatafeeds([item], refreshJobs);
closeMenu(true);
}
}, {
name: 'Close job',
description: 'Close job',
icon: 'cross',
enabled: () => (canCloseJob),
available: (item) => (isClosable([item])),
onClick: (item) => {
closeJobs([item], refreshJobs);
closeMenu(true);
}
}, {
name: 'Clone job',
description: 'Clone job',

View file

@ -20,9 +20,12 @@ import {
} from '@elastic/eui';
import {
closeJobs,
stopDatafeeds,
isStartable,
isStoppable } from '../utils';
isStoppable,
isClosable,
} from '../utils';
export class MultiJobActionsMenu extends Component {
constructor(props) {
@ -34,6 +37,7 @@ export class MultiJobActionsMenu extends Component {
this.canDeleteJob = checkPermission('canDeleteJob');
this.canStartStopDatafeed = (checkPermission('canStartStopDatafeed') && mlNodesAvailable());
this.canCloseJob = (checkPermission('canCloseJob') && mlNodesAvailable());
}
onButtonClick = () => {
@ -74,6 +78,19 @@ export class MultiJobActionsMenu extends Component {
)
];
if(isClosable(this.props.jobs)) {
items.push(
<EuiContextMenuItem
key="close job"
icon="cross"
disabled={(this.canCloseJob === false)}
onClick={() => { closeJobs(this.props.jobs); this.closePopover(); }}
>
Close job{s}
</EuiContextMenuItem>
);
}
if(isStoppable(this.props.jobs)) {
items.push(
<EuiContextMenuItem

View file

@ -10,7 +10,7 @@ import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar
import { mlJobService } from 'plugins/ml/services/job_service';
import { ml } from 'plugins/ml/services/ml_api_service';
import { DATAFEED_STATE } from 'plugins/ml/../common/constants/states';
import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states';
export function loadFullJob(jobId) {
return new Promise((resolve, reject) => {
@ -29,11 +29,15 @@ export function loadFullJob(jobId) {
}
export function isStartable(jobs) {
return (jobs.find(j => j.datafeedState === DATAFEED_STATE.STOPPED) !== undefined);
return jobs.some(j => j.datafeedState === DATAFEED_STATE.STOPPED);
}
export function isStoppable(jobs) {
return (jobs.find(j => j.datafeedState === DATAFEED_STATE.STARTED) !== undefined);
return jobs.some(j => j.datafeedState === DATAFEED_STATE.STARTED);
}
export function isClosable(jobs) {
return jobs.some(j => (j.datafeedState === DATAFEED_STATE.STOPPED) && (j.jobState !== JOB_STATE.CLOSED));
}
export function forceStartDatafeeds(jobs, start, end, finish = () => {}) {
@ -69,7 +73,8 @@ function showResults(resp, action) {
const successes = [];
const failures = [];
for (const d in resp) {
if (resp[d][action] === true || (resp[d][action] === false && resp[d].error.statusCode === 409)) {
if (resp[d][action] === true ||
(resp[d][action] === false && (resp[d].error.statusCode === 409 && action === DATAFEED_STATE.STARTED))) {
successes.push(d);
} else {
failures.push({
@ -90,8 +95,12 @@ function showResults(resp, action) {
} else if (action === DATAFEED_STATE.DELETED) {
actionText = 'delete';
actionTextPT = 'deleted';
} else if (action === JOB_STATE.CLOSED) {
actionText = 'close';
actionTextPT = 'closed';
}
if (successes.length > 1) {
toastNotifications.addSuccess(`${successes.length} jobs ${actionTextPT} successfully`);
} else if (successes.length === 1) {
@ -118,6 +127,20 @@ export function cloneJob(jobId) {
});
}
export function closeJobs(jobs, finish = () => {}) {
const jobIds = jobs.map(j => j.id);
mlJobService.closeJobs(jobIds)
.then((resp) => {
showResults(resp, JOB_STATE.CLOSED);
finish();
})
.catch((error) => {
mlMessageBarService.notify.error(error);
toastNotifications.addDanger(`Jobs failed to close`, error);
finish();
});
}
export function deleteJobs(jobs, finish = () => {}) {
const jobIds = jobs.map(j => j.id);
mlJobService.deleteJobs(jobIds)

View file

@ -927,6 +927,9 @@ class JobService {
return ml.jobs.deleteJobs(jIds);
}
closeJobs(jIds) {
return ml.jobs.closeJobs(jIds);
}
validateDetector(detector) {
return new Promise((resolve, reject) => {

View file

@ -71,6 +71,16 @@ export const jobs = {
});
},
closeJobs(jobIds) {
return http({
url: `${basePath}/jobs/close_jobs`,
method: 'POST',
data: {
jobIds,
}
});
},
jobAuditMessages(jobId, from) {
const jobIdString = (jobId !== undefined) ? `/${jobId}` : '';
const fromString = (from !== undefined) ? `?from=${from}` : '';

View file

@ -47,6 +47,31 @@ export function jobsProvider(callWithRequest) {
return results;
}
async function closeJobs(jobIds) {
const results = {};
for (const jobId of jobIds) {
try {
await callWithRequest('ml.closeJob', { jobId });
results[jobId] = { closed: true };
} catch (error) {
if (error.statusCode === 409 && (error.response && error.response.includes('datafeed') === false)) {
// the close job request may fail (409) if the job has failed or if the datafeed hasn't been stopped.
// if the job has failed we want to attempt a force close.
// however, if we received a 409 due to the datafeed being started we should not attempt a force close.
try {
await callWithRequest('ml.closeJob', { jobId, force: true });
results[jobId] = { closed: true };
} catch (error2) {
results[jobId] = { closed: false, error: error2 };
}
} else {
results[jobId] = { closed: false, error };
}
}
}
return results;
}
async function jobsSummary(jobIds = []) {
const fullJobsList = await createFullJobsList();
const auditMessages = await getAuditMessagesSummary();
@ -276,6 +301,7 @@ export function jobsProvider(callWithRequest) {
return {
forceDeleteJob,
deleteJobs,
closeJobs,
jobsSummary,
createFullJobsList,
getAllGroups,

View file

@ -63,6 +63,22 @@ export function jobServiceRoutes(server, commonRouteConfig) {
}
});
server.route({
method: 'POST',
path: '/api/ml/jobs/close_jobs',
handler(request, reply) {
const callWithRequest = callWithRequestFactory(server, request);
const { closeJobs } = jobServiceProvider(callWithRequest);
const { jobIds } = request.payload;
return closeJobs(jobIds)
.then(resp => reply(resp))
.catch(resp => reply(wrapError(resp)));
},
config: {
...commonRouteConfig
}
});
server.route({
method: 'POST',
path: '/api/ml/jobs/jobs_summary',