[ML] Adding reset anomaly detection jobs link to jobs list (#108039)

* [ML] Adding reset jobs link to jobs list

* fixing types

* updating types

* improving react code

* adding closed job warning callout

* small code changes after review

* updating comment for api docs

* adding canResetJob to security's emptyMlCapabilities

* updating apidoc

* adding blocked to job summary

* udating test

* adding delayed refresh back in

* updating tests

* adding better reverting controls and labels

* fixing bug in delete modal

* updating job task polling for all blocking tasks

* fixing types after es client update

* one other type correction

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
James Gowdy 2021-08-17 19:34:08 +01:00 committed by GitHub
parent c60530f4a6
commit 61097e70c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 637 additions and 93 deletions

View file

@ -0,0 +1,43 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const JOB_ACTION = {
DELETE: 'delete',
RESET: 'reset',
REVERT: 'revert',
} as const;
export type JobAction = typeof JOB_ACTION[keyof typeof JOB_ACTION];
export function getJobActionString(action: JobAction) {
switch (action) {
case JOB_ACTION.DELETE:
return i18n.translate('xpack.ml.models.jobService.deletingJob', {
defaultMessage: 'deleting',
});
case JOB_ACTION.RESET:
return i18n.translate('xpack.ml.models.jobService.resettingJob', {
defaultMessage: 'resetting',
});
case JOB_ACTION.REVERT:
return i18n.translate('xpack.ml.models.jobService.revertingJob', {
defaultMessage: 'reverting',
});
default:
return '';
}
}
export const JOB_ACTION_TASK: Record<string, JobAction> = {
'cluster:admin/xpack/ml/job/delete': JOB_ACTION.DELETE,
'cluster:admin/xpack/ml/job/reset': JOB_ACTION.RESET,
'cluster:admin/xpack/ml/job/model_snapshots/revert': JOB_ACTION.REVERT,
};
export const JOB_ACTION_TASKS = Object.keys(JOB_ACTION_TASK);

View file

@ -8,4 +8,5 @@
export const DEFAULT_REFRESH_INTERVAL_MS = 30000;
export const MINIMUM_REFRESH_INTERVAL_MS = 1000;
export const DELETING_JOBS_REFRESH_INTERVAL_MS = 2000;
export const RESETTING_JOBS_REFRESH_INTERVAL_MS = 1000;
export const PROGRESS_JOBS_REFRESH_INTERVAL_MS = 2000;

View file

@ -10,8 +10,11 @@ import { estypes } from '@elastic/elasticsearch';
export type JobId = string;
export type BucketSpan = string;
// temporary Job override, waiting for es client to have correct types
export type Job = estypes.MlJob;
export type MlJobBlocked = estypes.MlJobBlocked;
export type AnalysisConfig = estypes.MlAnalysisConfig;
export type Detector = estypes.MlDetector;

View file

@ -7,10 +7,11 @@
import { Moment } from 'moment';
import { CombinedJob, CombinedJobWithStats } from './combined_job';
import { MlAnomalyDetectionAlertRule } from '../alerts';
export { Datafeed } from './datafeed';
export { DatafeedStats } from './datafeed_stats';
import type { CombinedJob, CombinedJobWithStats } from './combined_job';
import type { MlAnomalyDetectionAlertRule } from '../alerts';
import type { MlJobBlocked } from './job';
export type { Datafeed } from './datafeed';
export type { DatafeedStats } from './datafeed_stats';
export interface MlSummaryJob {
id: string;
@ -31,7 +32,7 @@ export interface MlSummaryJob {
auditMessage?: Partial<AuditMessage>;
isSingleMetricViewerJob: boolean;
isNotSingleMetricViewerJobMessage?: string;
deleting?: boolean;
blocked?: MlJobBlocked;
latestTimestampSortValue?: number;
earliestStartTimestampMs?: number;
awaitingNodeAssignment: boolean;

View file

@ -40,6 +40,7 @@ export const adminMlCapabilities = {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canResetJob: false,
canUpdateJob: false,
canForecastJob: false,
canCreateDatafeed: false,

View file

@ -48,3 +48,11 @@ export interface BulkCreateResults {
datafeed: { success: boolean; error?: ErrorType };
};
}
export interface ResetJobsResponse {
[jobId: string]: {
reset: boolean;
task?: string;
error?: ErrorType;
};
}

View file

@ -73,5 +73,5 @@ export function isMLResponseError(error: any): error is MLResponseError {
}
export function isBoomError(error: any): error is Boom.Boom {
return error.isBoom === true;
return error?.isBoom === true;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC, useState, useEffect } from 'react';
import React, { FC, useState, useEffect, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiSpacer,
@ -23,8 +23,9 @@ import {
import { deleteJobs } from '../utils';
import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list';
import { DeleteJobCheckModal } from '../../../../components/delete_job_check_modal';
import { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
type ShowFunc = (jobs: Array<{ id: string }>) => void;
type ShowFunc = (jobs: MlSummaryJob[]) => void;
interface Props {
setShowFunction(showFunc: ShowFunc): void;
@ -49,18 +50,18 @@ export const DeleteJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction,
};
}, []);
function showModal(jobs: any[]) {
const showModal = useCallback((jobs: MlSummaryJob[]) => {
setJobIds(jobs.map(({ id }) => id));
setModalVisible(true);
setDeleting(false);
}
}, []);
function closeModal() {
const closeModal = useCallback(() => {
setModalVisible(false);
setCanDelete(false);
}
}, []);
function deleteJob() {
const deleteJob = useCallback(() => {
setDeleting(true);
deleteJobs(jobIds.map((id) => ({ id })));
@ -68,7 +69,7 @@ export const DeleteJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction,
closeModal();
refreshJobs();
}, DELETING_JOBS_REFRESH_INTERVAL_MS);
}
}, [jobIds, refreshJobs]);
if (modalVisible === false || jobIds.length === 0) {
return null;

View file

@ -8,14 +8,24 @@
import { checkPermission } from '../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes';
import { getIndexPatternNames } from '../../../../util/index_utils';
import { JOB_ACTION } from '../../../../../../common/constants/job_actions';
import { stopDatafeeds, cloneJob, closeJobs, isStartable, isStoppable, isClosable } from '../utils';
import {
stopDatafeeds,
cloneJob,
closeJobs,
isStartable,
isStoppable,
isClosable,
isResettable,
} from '../utils';
import { getToastNotifications } from '../../../../util/dependency_cache';
import { i18n } from '@kbn/i18n';
export function actionsMenuContent(
showEditJobFlyout,
showDeleteJobModal,
showResetJobModal,
showStartDatafeedModal,
refreshJobs,
showCreateAlertFlyout
@ -26,6 +36,7 @@ export function actionsMenuContent(
const canUpdateDatafeed = checkPermission('canUpdateDatafeed');
const canStartStopDatafeed = checkPermission('canStartStopDatafeed') && mlNodesAvailable();
const canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable();
const canResetJob = checkPermission('canResetJob') && mlNodesAvailable();
const canCreateMlAlerts = checkPermission('canCreateMlAlerts');
return [
@ -37,7 +48,7 @@ export function actionsMenuContent(
defaultMessage: 'Start datafeed',
}),
icon: 'play',
enabled: (item) => item.deleting !== true && canStartStopDatafeed,
enabled: (item) => isJobBlocked(item) === false && canStartStopDatafeed,
available: (item) => isStartable([item]),
onClick: (item) => {
showStartDatafeedModal([item]);
@ -53,7 +64,7 @@ export function actionsMenuContent(
defaultMessage: 'Stop datafeed',
}),
icon: 'stop',
enabled: (item) => item.deleting !== true && canStartStopDatafeed,
enabled: (item) => isJobBlocked(item) === false && canStartStopDatafeed,
available: (item) => isStoppable([item]),
onClick: (item) => {
stopDatafeeds([item], refreshJobs);
@ -69,7 +80,7 @@ export function actionsMenuContent(
defaultMessage: 'Create alert rule',
}),
icon: 'bell',
enabled: (item) => item.deleting !== true,
enabled: (item) => isJobBlocked(item) === false,
available: () => canCreateMlAlerts,
onClick: (item) => {
showCreateAlertFlyout([item.id]);
@ -85,7 +96,7 @@ export function actionsMenuContent(
defaultMessage: 'Close job',
}),
icon: 'cross',
enabled: (item) => item.deleting !== true && canCloseJob,
enabled: (item) => isJobBlocked(item) === false && canCloseJob,
available: (item) => isClosable([item]),
onClick: (item) => {
closeJobs([item], refreshJobs);
@ -93,6 +104,22 @@ export function actionsMenuContent(
},
'data-test-subj': 'mlActionButtonCloseJob',
},
{
name: i18n.translate('xpack.ml.jobsList.managementActions.resetJobLabel', {
defaultMessage: 'Reset job',
}),
description: i18n.translate('xpack.ml.jobsList.managementActions.resetJobDescription', {
defaultMessage: 'Reset job',
}),
icon: 'refresh',
enabled: (item) => isResetEnabled(item) && canResetJob,
available: (item) => isResettable([item]),
onClick: (item) => {
showResetJobModal([item]);
closeMenu(true);
},
'data-test-subj': 'mlActionButtonResetJob',
},
{
name: i18n.translate('xpack.ml.jobsList.managementActions.cloneJobLabel', {
defaultMessage: 'Clone job',
@ -106,7 +133,7 @@ export function actionsMenuContent(
// the indexPattern the job was created for. An indexPattern could either have been deleted
// since the the job was created or the current user doesn't have the required permissions to
// access the indexPattern.
return item.deleting !== true && canCreateJob;
return isJobBlocked(item) === false && canCreateJob;
},
onClick: (item) => {
const indexPatternNames = getIndexPatternNames();
@ -136,7 +163,7 @@ export function actionsMenuContent(
defaultMessage: 'Edit job',
}),
icon: 'pencil',
enabled: (item) => item.deleting !== true && canUpdateJob && canUpdateDatafeed,
enabled: (item) => isJobBlocked(item) === false && canUpdateJob && canUpdateDatafeed,
onClick: (item) => {
showEditJobFlyout(item);
closeMenu();
@ -162,6 +189,17 @@ export function actionsMenuContent(
];
}
function isResetEnabled(item) {
if (item.blocked === undefined || item.blocked.reason === JOB_ACTION.RESET) {
return true;
}
return false;
}
function isJobBlocked(item) {
return item.blocked !== undefined;
}
function closeMenu(now = false) {
if (now) {
document.querySelector('.euiTable').click();

View file

@ -48,7 +48,7 @@ export function ResultLinks({ jobs }) {
},
})
: undefined;
const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;
const jobActionsDisabled = jobs.length === 1 && jobs[0].blocked !== undefined;
const { createLinkWithUserDefaults } = useCreateADLinks();
const timeSeriesExplorerLink = useMemo(
() => createLinkWithUserDefaults('timeseriesexplorer', jobs),

View file

@ -104,7 +104,7 @@ export class JobsList extends Component {
render() {
const { loading, isManagementTable, spacesApi } = this.props;
const selectionControls = {
selectable: (job) => job.deleting !== true,
selectable: (job) => job.blocked === undefined,
selectableMessage: (selectable, rowItem) =>
selectable === false
? i18n.translate('xpack.ml.jobsList.cannotSelectRowForJobMessage', {
@ -140,7 +140,7 @@ export class JobsList extends Component {
render: (item) => (
<EuiButtonIcon
onClick={() => this.toggleRow(item)}
isDisabled={item.deleting === true}
isDisabled={item.blocked !== undefined}
iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowDown' : 'arrowRight'}
aria-label={
this.state.itemIdToExpandedRowMap[item.id]
@ -337,6 +337,7 @@ export class JobsList extends Component {
actions: actionsMenuContent(
this.props.showEditJobFlyout,
this.props.showDeleteJobModal,
this.props.showResetJobModal,
this.props.showStartDatafeedModal,
this.props.refreshJobs,
this.props.showCreateAlertFlyout

View file

@ -27,6 +27,7 @@ import { JobDetails } from '../job_details';
import { JobFilterBar } from '../job_filter_bar';
import { EditJobFlyout } from '../edit_job_flyout';
import { DeleteJobModal } from '../delete_job_modal';
import { ResetJobModal } from '../reset_job_modal';
import { StartDatafeedModal } from '../start_datafeed_modal';
import { MultiJobActions } from '../multi_job_actions';
import { NewJobButton } from '../new_job_button';
@ -41,7 +42,7 @@ import { RefreshJobsListButton } from '../refresh_jobs_list_button';
import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list';
import { JobListMlAnomalyAlertFlyout } from '../../../../../alerting/ml_alerting_flyout';
let deletingJobsRefreshTimeout = null;
let blockingJobsRefreshTimeout = null;
const filterJobsDebounce = debounce((jobsSummaryList, filterClauses, callback) => {
const ss = filterJobs(jobsSummaryList, filterClauses);
@ -62,7 +63,7 @@ export class JobsListView extends Component {
selectedJobs: [],
itemIdToExpandedRowMap: {},
filterClauses: [],
deletingJobIds: [],
blockingJobIds: [],
jobsAwaitingNodeCount: 0,
};
@ -70,6 +71,7 @@ export class JobsListView extends Component {
this.showEditJobFlyout = () => {};
this.showDeleteJobModal = () => {};
this.showResetJobModal = () => {};
this.showStartDatafeedModal = () => {};
this.showCreateAlertFlyout = () => {};
// work around to keep track of whether the component is mounted
@ -105,7 +107,7 @@ export class JobsListView extends Component {
componentWillUnmount() {
if (this.props.isManagementTable === undefined) {
deletingJobsRefreshTimeout = null;
blockingJobsRefreshTimeout = null;
}
this._isMounted = false;
}
@ -209,6 +211,13 @@ export class JobsListView extends Component {
this.showDeleteJobModal = () => {};
};
setShowResetJobModalFunction = (func) => {
this.showResetJobModal = func;
};
unsetShowResetJobModalFunction = () => {
this.showResetJobModal = () => {};
};
setShowStartDatafeedModalFunction = (func) => {
this.showStartDatafeedModal = func;
};
@ -353,17 +362,17 @@ export class JobsListView extends Component {
});
jobs.forEach((job) => {
if (job.deleting && this.state.itemIdToExpandedRowMap[job.id]) {
if (job.blocked !== undefined && this.state.itemIdToExpandedRowMap[job.id]) {
this.toggleRow(job.id);
}
});
this.isDoneRefreshing();
if (jobsSummaryList.some((j) => j.deleting === true)) {
if (jobsSummaryList.some((j) => j.blocked !== undefined)) {
// if there are some jobs in a deleting state, start polling for
// deleting jobs so we can update the jobs list once the
// deleting tasks are over
this.checkDeletingJobTasks(forceRefresh);
this.checkBlockingJobTasks(forceRefresh);
}
} catch (error) {
console.error(error);
@ -372,18 +381,18 @@ export class JobsListView extends Component {
}
}
async checkDeletingJobTasks(forceRefresh = false) {
async checkBlockingJobTasks(forceRefresh = false) {
if (this._isMounted === false) {
return;
}
const { jobIds: taskJobIds } = await ml.jobs.deletingJobTasks();
const { jobs } = await ml.jobs.blockingJobTasks();
const blockingJobIds = Object.keys(jobs);
const taskListHasChanged =
isEqual(taskJobIds.sort(), this.state.deletingJobIds.sort()) === false;
isEqual(blockingJobIds.sort(), this.state.blockingJobIds.sort()) === false;
this.setState({
deletingJobIds: taskJobIds,
blockingJobIds,
});
// only reload the jobs list if the contents of the task list has changed
@ -392,10 +401,10 @@ export class JobsListView extends Component {
this.refreshJobSummaryList();
}
if (taskJobIds.length > 0 && deletingJobsRefreshTimeout === null) {
deletingJobsRefreshTimeout = setTimeout(() => {
deletingJobsRefreshTimeout = null;
this.checkDeletingJobTasks();
if (blockingJobIds.length > 0 && blockingJobsRefreshTimeout === null) {
blockingJobsRefreshTimeout = setTimeout(() => {
blockingJobsRefreshTimeout = null;
this.checkBlockingJobTasks();
}, DELETING_JOBS_REFRESH_INTERVAL_MS);
}
}
@ -515,6 +524,7 @@ export class JobsListView extends Component {
allJobIds={jobIds}
showStartDatafeedModal={this.showStartDatafeedModal}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showCreateAlertFlyout={this.showCreateAlertFlyout}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
@ -531,6 +541,7 @@ export class JobsListView extends Component {
selectJobChange={this.selectJobChange}
showEditJobFlyout={this.showEditJobFlyout}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showStartDatafeedModal={this.showStartDatafeedModal}
refreshJobs={() => this.refreshJobSummaryList(true)}
jobsViewState={this.props.jobsViewState}
@ -550,6 +561,11 @@ export class JobsListView extends Component {
unsetShowFunction={this.unsetShowDeleteJobModalFunction}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
<ResetJobModal
setShowFunction={this.setShowResetJobModalFunction}
unsetShowFunction={this.unsetShowResetJobModalFunction}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
<StartDatafeedModal
setShowFunction={this.setShowStartDatafeedModalFunction}
unsetShowFunction={this.unsetShowDeleteJobModalFunction}

View file

@ -12,7 +12,14 @@ import React, { Component } from 'react';
import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
import { closeJobs, stopDatafeeds, isStartable, isStoppable, isClosable } from '../utils';
import {
closeJobs,
stopDatafeeds,
isStartable,
isStoppable,
isClosable,
isResettable,
} from '../utils';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -27,6 +34,7 @@ class MultiJobActionsMenuUI extends Component {
this.canDeleteJob = checkPermission('canDeleteJob');
this.canStartStopDatafeed = checkPermission('canStartStopDatafeed') && mlNodesAvailable();
this.canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable();
this.canResetJob = checkPermission('canResetJob') && mlNodesAvailable();
this.canCreateMlAlerts = checkPermission('canCreateMlAlerts');
}
@ -43,7 +51,7 @@ class MultiJobActionsMenuUI extends Component {
};
render() {
const anyJobsDeleting = this.props.jobs.some((j) => j.deleting);
const anyJobsBlocked = this.props.jobs.some((j) => j.blocked !== undefined);
const button = (
<EuiButtonIcon
size="s"
@ -57,7 +65,7 @@ class MultiJobActionsMenuUI extends Component {
)}
color="text"
disabled={
anyJobsDeleting || (this.canDeleteJob === false && this.canStartStopDatafeed === false)
anyJobsBlocked || (this.canDeleteJob === false && this.canStartStopDatafeed === false)
}
data-test-subj="mlADJobListMultiSelectManagementActionsButton"
/>
@ -103,6 +111,27 @@ class MultiJobActionsMenuUI extends Component {
);
}
if (isResettable(this.props.jobs)) {
items.push(
<EuiContextMenuItem
key="reset job"
icon="refresh"
disabled={this.canCloseJob === false}
onClick={() => {
this.props.showResetJobModal(this.props.jobs);
this.closePopover();
}}
data-test-subj="mlADJobListMultiSelectResetJobActionButton"
>
<FormattedMessage
id="xpack.ml.jobsList.multiJobsActions.resetJobsLabel"
defaultMessage="Reset {jobsCount, plural, one {job} other {jobs}}"
values={{ jobsCount: this.props.jobs.length }}
/>
</EuiContextMenuItem>
);
}
if (isStoppable(this.props.jobs)) {
items.push(
<EuiContextMenuItem

View file

@ -66,6 +66,7 @@ export class MultiJobActions extends Component {
jobs={this.props.selectedJobs}
showStartDatafeedModal={this.props.showStartDatafeedModal}
showDeleteJobModal={this.props.showDeleteJobModal}
showResetJobModal={this.props.showResetJobModal}
refreshJobs={this.props.refreshJobs}
showCreateAlertFlyout={this.props.showCreateAlertFlyout}
/>
@ -81,6 +82,7 @@ MultiJobActions.propTypes = {
allJobIds: PropTypes.array.isRequired,
showStartDatafeedModal: PropTypes.func.isRequired,
showDeleteJobModal: PropTypes.func.isRequired,
showResetJobModal: PropTypes.func.isRequired,
refreshJobs: PropTypes.func.isRequired,
showCreateAlertFlyout: PropTypes.func.isRequired,
};

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 { ResetJobModal } from './reset_job_modal';

View file

@ -0,0 +1,56 @@
/*
* 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, { FC, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { JOB_STATE } from '../../../../../../common/constants/states';
interface Props {
jobs: MlSummaryJob[];
}
export const OpenJobsWarningCallout: FC<Props> = ({ jobs }) => {
const openJobsCount = useMemo(() => jobs.filter((j) => j.jobState !== JOB_STATE.CLOSED).length, [
jobs,
]);
if (openJobsCount === 0) {
return null;
}
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.openJobsWarningCallout.title"
defaultMessage="{openJobsCount, plural, one {# job is} other {# jobs are}} not closed"
values={{ openJobsCount }}
/>
}
color="warning"
>
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.openJobsWarningCallout.description1"
defaultMessage="{openJobsCount, plural, one {This job} other {These jobs}} must be closed before {openJobsCount, plural, one {it} other {they}} can be reset. "
values={{ openJobsCount }}
/>
<br />
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.openJobsWarningCallout.description2"
defaultMessage="{openJobsCount, plural, one {This job} other {These jobs}} will not be reset when clicking the Reset button below."
values={{ openJobsCount }}
/>
</EuiCallOut>
<EuiSpacer size="m" />
</>
);
};

View file

@ -0,0 +1,132 @@
/*
* 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, { FC, useState, useEffect, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiSpacer,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiButtonEmpty,
EuiButton,
EuiText,
} from '@elastic/eui';
import { resetJobs } from '../utils';
import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { RESETTING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list';
import { OpenJobsWarningCallout } from './open_jobs_warning_callout';
type ShowFunc = (jobs: MlSummaryJob[]) => void;
interface Props {
setShowFunction(showFunc: ShowFunc): void;
unsetShowFunction(): void;
refreshJobs(): void;
}
export const ResetJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction, refreshJobs }) => {
const [resetting, setResetting] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [jobIds, setJobIds] = useState<string[]>([]);
const [jobs, setJobs] = useState<MlSummaryJob[]>([]);
useEffect(() => {
if (typeof setShowFunction === 'function') {
setShowFunction(showModal);
}
return () => {
if (typeof unsetShowFunction === 'function') {
unsetShowFunction();
}
};
}, []);
const showModal = useCallback((tempJobs: MlSummaryJob[]) => {
setJobIds(tempJobs.map(({ id }) => id));
setJobs(tempJobs);
setModalVisible(true);
setResetting(false);
}, []);
const closeModal = useCallback(() => {
setModalVisible(false);
}, []);
const resetJob = useCallback(async () => {
setResetting(true);
await resetJobs(jobIds);
closeModal();
setTimeout(() => {
refreshJobs();
}, RESETTING_JOBS_REFRESH_INTERVAL_MS);
}, [jobIds, refreshJobs]);
if (modalVisible === false || jobIds.length === 0) {
return null;
}
return (
<EuiModal data-test-subj="mlResetJobConfirmModal" onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.resetJobsTitle"
defaultMessage="Reset {jobsCount, plural, one {{jobId}} other {# jobs}}?"
values={{
jobsCount: jobIds.length,
jobId: jobIds[0],
}}
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<>
<OpenJobsWarningCallout jobs={jobs} />
<EuiText>
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.resetMultipleJobsDescription"
defaultMessage="Resetting {jobsCount, plural, one {a job} other {multiple jobs}} can be time consuming.
{jobsCount, plural, one {It} other {They}} will be reset in the background
and may not be updated in the jobs list instantly."
values={{
jobsCount: jobIds.length,
}}
/>
</EuiText>
</>
</EuiModalBody>
<>
<EuiSpacer />
<EuiModalFooter>
<EuiButtonEmpty onClick={closeModal} disabled={resetting}>
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton
onClick={resetJob}
fill
disabled={resetting}
color="danger"
data-test-subj="mlResetJobConfirmModalButton"
>
<FormattedMessage
id="xpack.ml.jobsList.resetJobModal.resetButtonLabel"
defaultMessage="Reset"
/>
</EuiButton>
</EuiModalFooter>
</>
</EuiModal>
);
};

View file

@ -8,4 +8,5 @@
import { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs';
export function deleteJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise<void>;
export function resetJobs(jobIds: string[], callback?: () => void): Promise<void>;
export function loadFullJob(jobId: string): Promise<CombinedJobWithStats>;

View file

@ -17,6 +17,7 @@ import { getToastNotifications } from '../../../util/dependency_cache';
import { ml } from '../../../services/ml_api_service';
import { stringMatch } from '../../../util/string_utils';
import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states';
import { JOB_ACTION } from '../../../../../common/constants/job_actions';
import { parseInterval } from '../../../../../common/util/parse_interval';
import { mlCalendarService } from '../../../services/calendar_service';
import { isPopulatedObject } from '../../../../../common/util/object_utils';
@ -76,6 +77,12 @@ export function isClosable(jobs) {
);
}
export function isResettable(jobs) {
return jobs.some(
(j) => j.jobState === JOB_STATE.CLOSED || j.blocked?.reason === JOB_ACTION.RESET
);
}
export function forceStartDatafeeds(jobs, start, end, finish = () => {}) {
const datafeedIds = jobs.filter((j) => j.hasDatafeed).map((j) => j.datafeedId);
mlJobService
@ -165,6 +172,13 @@ function showResults(resp, action) {
actionTextPT = i18n.translate('xpack.ml.jobsList.closedActionStatusText', {
defaultMessage: 'closed',
});
} else if (action === JOB_ACTION.RESET) {
actionText = i18n.translate('xpack.ml.jobsList.resetActionStatusText', {
defaultMessage: 'reset',
});
actionTextPT = i18n.translate('xpack.ml.jobsList.resetActionStatusText', {
defaultMessage: 'reset',
});
}
const toastNotifications = getToastNotifications();
@ -283,6 +297,24 @@ export function closeJobs(jobs, finish = () => {}) {
});
}
export function resetJobs(jobIds, finish = () => {}) {
mlJobService
.resetJobs(jobIds)
.then((resp) => {
showResults(resp, JOB_ACTION.RESET);
finish();
})
.catch((error) => {
getToastNotificationService().displayErrorToast(
error,
i18n.translate('xpack.ml.jobsList.resetJobErrorMessage', {
defaultMessage: 'Jobs failed to reset',
})
);
finish();
});
}
export function deleteJobs(jobs, finish = () => {}) {
const jobIds = jobs.map((j) => j.id);
mlJobService

View file

@ -386,6 +386,10 @@ class JobService {
return ml.jobs.closeJobs(jIds);
}
resetJobs(jIds) {
return ml.jobs.resetJobs(jIds);
}
validateDetector(detector) {
return new Promise((resolve, reject) => {
if (detector) {

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { estypes } from '@elastic/elasticsearch';
import { Observable } from 'rxjs';
import { HttpStart } from 'kibana/public';
import type { HttpStart } from 'kibana/public';
import { HttpService } from '../http_service';
import { annotations } from './annotations';
@ -16,16 +17,19 @@ import { resultsApiProvider } from './results';
import { jobsApiProvider } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
import { savedObjectsApiProvider } from './saved_objects';
import {
import type {
MlServerDefaults,
MlServerLimits,
MlNodeCount,
} from '../../../../common/types/ml_server_info';
import { MlCapabilitiesResponse } from '../../../../common/types/capabilities';
import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars';
import { BucketSpanEstimatorData } from '../../../../common/types/job_service';
import {
import type { MlCapabilitiesResponse } from '../../../../common/types/capabilities';
import type { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars';
import type {
BucketSpanEstimatorData,
ResetJobsResponse,
} from '../../../../common/types/job_service';
import type {
Job,
JobStats,
Datafeed,
@ -35,8 +39,8 @@ import {
ModelSnapshot,
IndicesOptions,
} from '../../../../common/types/anomaly_detection_jobs';
import { FieldHistogramRequestConfig } from '../../datavisualizer/index_based/common/request';
import { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules';
import type { FieldHistogramRequestConfig } from '../../datavisualizer/index_based/common/request';
import type { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules';
import { getHttp } from '../../util/dependency_cache';
import type { RuntimeMappings } from '../../../../common/types/fields';
@ -151,14 +155,14 @@ export function mlApiServicesProvider(httpService: HttpService) {
},
deleteJob({ jobId }: { jobId: string }) {
return httpService.http<any>({
return httpService.http<estypes.MlDeleteJobResponse>({
path: `${basePath()}/anomaly_detectors/${jobId}`,
method: 'DELETE',
});
},
forceDeleteJob({ jobId }: { jobId: string }) {
return httpService.http<any>({
return httpService.http<estypes.MlDeleteJobResponse>({
path: `${basePath()}/anomaly_detectors/${jobId}?force=true`,
method: 'DELETE',
});
@ -173,6 +177,13 @@ export function mlApiServicesProvider(httpService: HttpService) {
});
},
resetJob({ jobId }: { jobId: string }) {
return httpService.http<ResetJobsResponse>({
path: `${basePath()}/anomaly_detectors/${jobId}/_reset`,
method: 'POST',
});
},
estimateBucketSpan(obj: BucketSpanEstimatorData) {
const body = JSON.stringify(obj);
return httpService.http<BucketSpanEstimatorResponse>({

View file

@ -18,6 +18,7 @@ import type {
IndicesOptions,
} from '../../../../common/types/anomaly_detection_jobs';
import type { JobMessage } from '../../../../common/types/audit_message';
import type { JobAction } from '../../../../common/constants/job_actions';
import type { AggFieldNamePair, RuntimeMappings } from '../../../../common/types/fields';
import type { ExistingJobsAndGroups } from '../job_service';
import type {
@ -27,7 +28,11 @@ import type {
} from '../../../../common/types/categories';
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
import type { Category } from '../../../../common/types/categories';
import type { JobsExistResponse, BulkCreateResults } from '../../../../common/types/job_service';
import type {
JobsExistResponse,
BulkCreateResults,
ResetJobsResponse,
} from '../../../../common/types/job_service';
import { ML_BASE_PATH } from '../../../../common/constants/app';
export const jobsApiProvider = (httpService: HttpService) => ({
@ -127,6 +132,15 @@ export const jobsApiProvider = (httpService: HttpService) => ({
});
},
resetJobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
return httpService.http<ResetJobsResponse>({
path: `${ML_BASE_PATH}/jobs/reset_jobs`,
method: 'POST',
body,
});
},
forceStopAndCloseJob(jobId: string) {
const body = JSON.stringify({ jobId });
return httpService.http<{ success: boolean }>({
@ -169,9 +183,9 @@ export const jobsApiProvider = (httpService: HttpService) => ({
});
},
deletingJobTasks() {
return httpService.http<any>({
path: `${ML_BASE_PATH}/jobs/deleting_jobs_tasks`,
blockingJobTasks() {
return httpService.http<Record<string, JobAction>>({
path: `${ML_BASE_PATH}/jobs/blocking_jobs_tasks`,
method: 'GET',
});
},

View file

@ -51,7 +51,7 @@ describe('check_capabilities', () => {
);
const { capabilities } = await getCapabilities();
const count = Object.keys(capabilities).length;
expect(count).toBe(30);
expect(count).toBe(31);
});
});
@ -88,6 +88,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canResetJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
@ -137,6 +138,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteJob).toBe(true);
expect(capabilities.canOpenJob).toBe(true);
expect(capabilities.canCloseJob).toBe(true);
expect(capabilities.canResetJob).toBe(true);
expect(capabilities.canForecastJob).toBe(true);
expect(capabilities.canStartStopDatafeed).toBe(true);
expect(capabilities.canUpdateJob).toBe(true);
@ -185,6 +187,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canResetJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
@ -233,6 +236,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canResetJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
@ -281,6 +285,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canResetJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
@ -331,6 +336,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canResetJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);

View file

@ -474,6 +474,10 @@ export function getMlClient(
await jobIdsCheck('anomaly-detector', p);
return mlClient.updateJob(...p);
},
async resetJob(...p: Parameters<MlClient['resetJob']>) {
await jobIdsCheck('anomaly-detector', p);
return mlClient.resetJob(...p);
},
async updateModelSnapshot(...p: Parameters<MlClient['updateModelSnapshot']>) {
await jobIdsCheck('anomaly-detector', p);
return mlClient.updateModelSnapshot(...p);

View file

@ -7,9 +7,10 @@
import { i18n } from '@kbn/i18n';
import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states';
import { JobAction } from '../../../common/constants/job_actions';
const REQUEST_TIMEOUT_NAME = 'RequestTimeout';
type ACTION_STATE = DATAFEED_STATE | JOB_STATE;
type ACTION_STATE = DATAFEED_STATE | JOB_STATE | JobAction;
export function isRequestTimeout(error: { name: string }) {
return error.name === REQUEST_TIMEOUT_NAME;

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { uniq } from 'lodash';
import Boom from '@hapi/boom';
import { IScopedClusterClient } from 'kibana/server';
@ -14,6 +13,13 @@ import {
parseTimeIntervalForJob,
} from '../../../common/util/job_utils';
import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states';
import {
getJobActionString,
JOB_ACTION_TASK,
JOB_ACTION_TASKS,
JOB_ACTION,
JobAction,
} from '../../../common/constants/job_actions';
import {
MlSummaryJob,
AuditMessage,
@ -27,6 +33,7 @@ import {
MlJobsStatsResponse,
JobsExistResponse,
BulkCreateResults,
ResetJobsResponse,
} from '../../../common/types/job_service';
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds';
@ -145,6 +152,29 @@ export function jobsProvider(
return results;
}
async function resetJobs(jobIds: string[]) {
const results: ResetJobsResponse = {};
for (const jobId of jobIds) {
try {
const {
// @ts-expect-error @elastic-elasticsearch resetJob response incorrect, missing task
body: { task },
} = await mlClient.resetJob({
job_id: jobId,
wait_for_completion: false,
});
results[jobId] = { reset: true, task };
} catch (error) {
if (isRequestTimeout(error)) {
return fillResultsWithTimeouts(results, jobId, jobIds, JOB_ACTION.RESET);
} else {
results[jobId] = { reset: false, error: error.body };
}
}
}
return results;
}
async function forceStopAndCloseJob(jobId: string) {
const datafeedIds = await getDatafeedIdsByJobId();
const datafeedId = datafeedIds[jobId];
@ -181,10 +211,6 @@ export function jobsProvider(
// fail silently
}
const deletingStr = i18n.translate('xpack.ml.models.jobService.deletingJob', {
defaultMessage: 'deleting',
});
const jobs = fullJobsList.map((job) => {
const hasDatafeed = isPopulatedObject(job.datafeed_config);
const dataCounts = job.data_counts;
@ -201,7 +227,7 @@ export function jobsProvider(
parseTimeIntervalForJob(job.analysis_config?.bucket_span)
),
memory_status: job.model_size_stats ? job.model_size_stats.memory_status : '',
jobState: job.deleting === true ? deletingStr : job.state,
jobState: job.blocked === undefined ? job.state : getJobActionString(job.blocked.reason),
hasDatafeed,
datafeedId:
hasDatafeed && job.datafeed_config.datafeed_id ? job.datafeed_config.datafeed_id : '',
@ -217,11 +243,12 @@ export function jobsProvider(
isSingleMetricViewerJob: errorMessage === undefined,
isNotSingleMetricViewerJobMessage: errorMessage,
nodeName: job.node ? job.node.name : undefined,
deleting: job.deleting || undefined,
blocked: job.blocked ?? undefined,
awaitingNodeAssignment: isJobAwaitingNodeAssignment(job),
alertingRules: job.alerting_rules,
jobTags: job.custom_settings?.job_tags ?? {},
};
if (jobIds.find((j) => j === tempJob.id)) {
tempJob.fullJob = job;
}
@ -459,21 +486,25 @@ export function jobsProvider(
return jobs;
}
async function deletingJobTasks() {
const actions = ['cluster:admin/xpack/ml/job/delete'];
const detailed = true;
const jobIds: string[] = [];
async function blockingJobTasks() {
const jobs: Array<Record<string, JobAction>> = [];
try {
const { body } = await asInternalUser.tasks.list({
actions,
detailed,
actions: JOB_ACTION_TASKS,
detailed: true,
});
if (body.nodes) {
Object.keys(body.nodes).forEach((nodeId) => {
const tasks = body.nodes![nodeId].tasks;
Object.keys(tasks).forEach((taskId) => {
jobIds.push(tasks[taskId].description!.replace(/^delete-job-/, ''));
if (body.nodes !== undefined) {
Object.values(body.nodes).forEach(({ tasks }) => {
Object.values(tasks).forEach(({ action, description }) => {
if (description === undefined) {
return;
}
if (JOB_ACTION_TASK[action] === JOB_ACTION.DELETE) {
jobs.push({ [description.replace(/^delete-job-/, '')]: JOB_ACTION.DELETE });
} else {
jobs.push({ [description]: JOB_ACTION_TASK[action] });
}
});
});
}
@ -481,12 +512,16 @@ export function jobsProvider(
// if the user doesn't have permission to load the task list,
// use the jobs list to get the ids of deleting jobs
const {
body: { jobs },
} = await mlClient.getJobs<MlJobsResponse>();
body: { jobs: tempJobs },
} = await mlClient.getJobs();
jobIds.push(...jobs.filter((j) => j.deleting === true).map((j) => j.job_id));
jobs.push(
...tempJobs
.filter((j) => j.blocked !== undefined)
.map((j) => ({ [j.job_id]: j.blocked!.reason }))
);
}
return { jobIds };
return { jobs };
}
// Checks if each of the jobs in the specified list of IDs exist.
@ -613,12 +648,13 @@ export function jobsProvider(
forceDeleteJob,
deleteJobs,
closeJobs,
resetJobs,
forceStopAndCloseJob,
jobsSummary,
jobsWithTimerange,
getJobForCloning,
createFullJobsList,
deletingJobTasks,
blockingJobTasks,
jobsExist,
getAllJobAndGroupIds,
getLookBackProgress,

View file

@ -22,6 +22,8 @@ import {
getModelSnapshotsSchema,
updateModelSnapshotsSchema,
updateModelSnapshotBodySchema,
forceQuerySchema,
jobResetQuerySchema,
} from './schemas/anomaly_detectors_schema';
/**
@ -270,13 +272,14 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) {
* @apiDescription Closes an anomaly detection job.
*
* @apiSchema (params) jobIdSchema
* @apiSchema (query) forceQuerySchema
*/
router.post(
{
path: '/api/ml/anomaly_detectors/{jobId}/_close',
validate: {
params: jobIdSchema,
query: schema.object({ force: schema.maybe(schema.boolean()) }),
query: forceQuerySchema,
},
options: {
tags: ['access:ml:canCloseJob'],
@ -301,6 +304,46 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) {
})
);
/**
* @apiGroup AnomalyDetectors
*
* @api {post} /api/ml/anomaly_detectors/:jobId/_reset Reset specified job
* @apiName ResetAnomalyDetectorsJob
* @apiDescription Resets an anomaly detection job.
*
* @apiSchema (params) jobIdSchema
* @apiSchema (query) jobResetQuerySchema
*/
router.post(
{
path: '/api/ml/anomaly_detectors/{jobId}/_reset',
validate: {
params: jobIdSchema,
query: jobResetQuerySchema,
},
options: {
tags: ['access:ml:canCloseJob'],
},
},
routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => {
try {
const options: { job_id: string; wait_for_completion?: boolean } = {
// TODO change this to correct resetJob request type
job_id: request.params.jobId,
...(request.query.wait_for_completion !== undefined
? { wait_for_completion: request.query.wait_for_completion }
: {}),
};
const { body } = await mlClient.resetJob(options);
return response.ok({
body,
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
/**
* @apiGroup AnomalyDetectors
*
@ -309,13 +352,14 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) {
* @apiDescription Deletes specified anomaly detection job.
*
* @apiSchema (params) jobIdSchema
* @apiSchema (query) forceQuerySchema
*/
router.delete(
{
path: '/api/ml/anomaly_detectors/{jobId}',
validate: {
params: jobIdSchema,
query: schema.object({ force: schema.maybe(schema.boolean()) }),
query: forceQuerySchema,
},
options: {
tags: ['access:ml:canDeleteJob'],

View file

@ -88,7 +88,7 @@
"TopCategories",
"DatafeedPreview",
"UpdateGroups",
"DeletingJobTasks",
"BlockingJobTasks",
"DeleteJobs",
"RevertModelSnapshot",
"BulkCreateJobs",

View file

@ -174,6 +174,40 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) {
})
);
/**
* @apiGroup JobService
*
* @api {post} /api/ml/jobs/reset_jobs Reset multiple jobs
* @apiName ResetJobs
* @apiDescription Resets one or more anomaly detection jobs
*
* @apiSchema (body) jobIdsSchema
*/
router.post(
{
path: '/api/ml/jobs/reset_jobs',
validate: {
body: jobIdsSchema,
},
options: {
tags: ['access:ml:canResetJob'],
},
},
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => {
try {
const { resetJobs } = jobServiceProvider(client, mlClient);
const { jobIds } = request.body;
const resp = await resetJobs(jobIds);
return response.ok({
body: resp,
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
/**
* @apiGroup JobService
*
@ -422,13 +456,13 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) {
/**
* @apiGroup JobService
*
* @api {get} /api/ml/jobs/deleting_jobs_tasks Get deleting job tasks
* @apiName DeletingJobTasks
* @apiDescription Gets the ids of deleting anomaly detection jobs
* @api {get} /api/ml/jobs/blocking_jobs_tasks Get blocking job tasks
* @apiName BlockingJobTasks
* @apiDescription Gets the ids of deleting, resetting or reverting anomaly detection jobs
*/
router.get(
{
path: '/api/ml/jobs/deleting_jobs_tasks',
path: '/api/ml/jobs/blocking_jobs_tasks',
validate: false,
options: {
tags: ['access:ml:canGetJobs'],
@ -436,8 +470,8 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) {
},
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, response }) => {
try {
const { deletingJobTasks } = jobServiceProvider(client, mlClient);
const resp = await deletingJobTasks();
const { blockingJobTasks } = jobServiceProvider(client, mlClient);
const resp = await blockingJobTasks();
return response.ok({
body: resp,

View file

@ -219,3 +219,13 @@ export const updateModelSnapshotBodySchema = schema.object({
});
export const forecastAnomalyDetector = schema.object({ duration: schema.any() });
export const jobResetQuerySchema = schema.object({
/** wait for completion */
wait_for_completion: schema.maybe(schema.boolean()),
});
export const forceQuerySchema = schema.object({
/** force close */
force: schema.maybe(schema.boolean()),
});

View file

@ -18,6 +18,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canResetJob: false,
canForecastJob: false,
canGetDatafeeds: false,
canStartStopDatafeed: false,

View file

@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should have the right number of capabilities', async () => {
const { capabilities } = await runRequest(USER.ML_POWERUSER);
expect(Object.keys(capabilities).length).to.eql(30);
expect(Object.keys(capabilities).length).to.eql(31);
});
it('should get viewer capabilities', async () => {
@ -56,6 +56,7 @@ export default ({ getService }: FtrProviderContext) => {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canResetJob: false,
canUpdateJob: false,
canForecastJob: false,
canCreateDatafeed: false,
@ -93,6 +94,7 @@ export default ({ getService }: FtrProviderContext) => {
canDeleteJob: true,
canOpenJob: true,
canCloseJob: true,
canResetJob: true,
canUpdateJob: true,
canForecastJob: true,
canCreateDatafeed: true,

View file

@ -71,11 +71,11 @@ export default ({ getService }: FtrProviderContext) => {
it('should have the right number of capabilities - space with ML', async () => {
const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl);
expect(Object.keys(capabilities).length).to.eql(30);
expect(Object.keys(capabilities).length).to.eql(31);
});
it('should have the right number of capabilities - space without ML', async () => {
const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl);
expect(Object.keys(capabilities).length).to.eql(30);
expect(Object.keys(capabilities).length).to.eql(31);
});
it('should get viewer capabilities - space with ML', async () => {
@ -85,6 +85,7 @@ export default ({ getService }: FtrProviderContext) => {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canResetJob: false,
canUpdateJob: false,
canForecastJob: false,
canCreateDatafeed: false,
@ -121,6 +122,7 @@ export default ({ getService }: FtrProviderContext) => {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canResetJob: false,
canUpdateJob: false,
canForecastJob: false,
canCreateDatafeed: false,
@ -157,6 +159,7 @@ export default ({ getService }: FtrProviderContext) => {
canDeleteJob: true,
canOpenJob: true,
canCloseJob: true,
canResetJob: true,
canUpdateJob: true,
canForecastJob: true,
canCreateDatafeed: true,
@ -193,6 +196,7 @@ export default ({ getService }: FtrProviderContext) => {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canResetJob: false,
canUpdateJob: false,
canForecastJob: false,
canCreateDatafeed: false,