mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
c60530f4a6
commit
61097e70c0
33 changed files with 637 additions and 93 deletions
43
x-pack/plugins/ml/common/constants/job_actions.ts
Normal file
43
x-pack/plugins/ml/common/constants/job_actions.ts
Normal 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);
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -40,6 +40,7 @@ export const adminMlCapabilities = {
|
|||
canDeleteJob: false,
|
||||
canOpenJob: false,
|
||||
canCloseJob: false,
|
||||
canResetJob: false,
|
||||
canUpdateJob: false,
|
||||
canForecastJob: false,
|
||||
canCreateDatafeed: false,
|
||||
|
|
|
@ -48,3 +48,11 @@ export interface BulkCreateResults {
|
|||
datafeed: { success: boolean; error?: ErrorType };
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResetJobsResponse {
|
||||
[jobId: string]: {
|
||||
reset: boolean;
|
||||
task?: string;
|
||||
error?: ErrorType;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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';
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
"TopCategories",
|
||||
"DatafeedPreview",
|
||||
"UpdateGroups",
|
||||
"DeletingJobTasks",
|
||||
"BlockingJobTasks",
|
||||
"DeleteJobs",
|
||||
"RevertModelSnapshot",
|
||||
"BulkCreateJobs",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()),
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = {
|
|||
canDeleteJob: false,
|
||||
canOpenJob: false,
|
||||
canCloseJob: false,
|
||||
canResetJob: false,
|
||||
canForecastJob: false,
|
||||
canGetDatafeeds: false,
|
||||
canStartStopDatafeed: false,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue