diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/index.js b/x-pack/plugins/ml/public/components/controls/select_severity/index.js
index 070cc16d19313..618edf599e509 100644
--- a/x-pack/plugins/ml/public/components/controls/select_severity/index.js
+++ b/x-pack/plugins/ml/public/components/controls/select_severity/index.js
@@ -6,4 +6,3 @@
import './select_severity_directive';
-import './styles/main.less';
diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js
index 59ce7af3272bf..a27b594f1aac2 100644
--- a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js
+++ b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js
@@ -9,6 +9,7 @@
/*
* React component for rendering a select element with threshold levels.
*/
+import PropTypes from 'prop-types';
import _ from 'lodash';
import React, { Component } from 'react';
@@ -18,6 +19,8 @@ import {
EuiHealth,
} from '@elastic/eui';
+import './styles/main.less';
+
import { getSeverityColor } from 'plugins/ml/../common/util/anomaly_utils';
const OPTIONS = [
@@ -103,5 +106,8 @@ class SelectSeverity extends Component {
);
}
}
+SelectSeverity.propTypes = {
+ mlSelectSeverityService: PropTypes.object.isRequired,
+};
export { SelectSeverity };
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/create_watch_flyout.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/create_watch_flyout.js
new file mode 100644
index 0000000000000..ac432e8323d05
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/create_watch_flyout.js
@@ -0,0 +1,176 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+import PropTypes from 'prop-types';
+import React, {
+ Component,
+} from 'react';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import { toastNotifications } from 'ui/notify';
+import { loadFullJob } from '../utils';
+import { mlCreateWatchService } from '../../../../jobs/new_job/simple/components/watcher/create_watch_service';
+import { CreateWatch } from '../../../../jobs/new_job/simple/components/watcher/create_watch_view';
+
+
+function getSuccessToast(id, url) {
+ return {
+ title: `Watch ${id} created successfully`,
+ text: (
+
+
+
+
+ Edit watch
+
+
+
+
+ )
+ };
+}
+
+export class CreateWatchFlyout extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ jobId: null,
+ bucketSpan: null,
+ };
+ }
+
+ componentDidMount() {
+ if (typeof this.props.setShowFunction === 'function') {
+ this.props.setShowFunction(this.showFlyout);
+ }
+ }
+
+ componentWillUnmount() {
+ if (typeof this.props.unsetShowFunction === 'function') {
+ this.props.unsetShowFunction();
+ }
+ }
+
+ closeFlyout = () => {
+ this.setState({ isFlyoutVisible: false });
+ }
+
+ showFlyout = (jobId) => {
+ loadFullJob(jobId)
+ .then((job) => {
+ const bucketSpan = job.analysis_config.bucket_span;
+ mlCreateWatchService.config.includeInfluencers = (job.analysis_config.influencers.length > 0);
+
+ this.setState({
+ job,
+ jobId,
+ bucketSpan,
+ isFlyoutVisible: true,
+ });
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ }
+
+ save = () => {
+ mlCreateWatchService.createNewWatch(this.state.jobId)
+ .then((resp) => {
+ toastNotifications.addSuccess(getSuccessToast(resp.id, resp.url));
+ this.closeFlyout();
+ })
+ .catch((error) => {
+ toastNotifications.addDanger(`Could not save watch`);
+ console.error(error);
+ });
+ }
+
+
+ render() {
+ const {
+ jobId,
+ bucketSpan
+ } = this.state;
+
+ let flyout;
+
+ if (this.state.isFlyoutVisible) {
+ flyout = (
+
+
+
+
+ Create watch for {jobId}
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
+ Save
+
+
+
+
+
+ );
+ }
+ return (
+
+ {flyout}
+
+ );
+
+ }
+}
+CreateWatchFlyout.propTypes = {
+ setShowFunction: PropTypes.func.isRequired,
+ unsetShowFunction: PropTypes.func.isRequired,
+};
+
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/index.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/index.js
new file mode 100644
index 0000000000000..e8e7ee52b3c5f
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/index.js
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+export { CreateWatchFlyout } from './create_watch_flyout';
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/create_watch_flyout/styles/main.less
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/edit_job_flyout.js
index d256c8f0868c3..f427c473f7a11 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/edit_job_flyout.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/edit_job_flyout.js
@@ -36,7 +36,7 @@ export class EditJobFlyout extends Component {
this.state = {
job: {},
hasDatafeed: false,
- isModalVisible: false,
+ isFlyoutVisible: false,
jobDescription: '',
jobGroups: [],
jobModelMemoryLimit: '',
@@ -68,7 +68,7 @@ export class EditJobFlyout extends Component {
}
closeFlyout = () => {
- this.setState({ isModalVisible: false });
+ this.setState({ isFlyoutVisible: false });
}
showFlyout = (jobLite) => {
@@ -78,7 +78,7 @@ export class EditJobFlyout extends Component {
this.extractJob(job, hasDatafeed);
this.setState({
job,
- isModalVisible: true,
+ isFlyoutVisible: true,
});
})
.catch((error) => {
@@ -187,7 +187,7 @@ export class EditJobFlyout extends Component {
render() {
let flyout;
- if (this.state.isModalVisible) {
+ if (this.state.isFlyoutVisible) {
const {
job,
jobDescription,
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js
index 89a1e7874ebda..bc84a388d7dde 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js
@@ -16,6 +16,7 @@ import { JobFilterBar } from '../job_filter_bar';
import { EditJobFlyout } from '../edit_job_flyout';
import { DeleteJobModal } from '../delete_job_modal';
import { StartDatafeedModal } from '../start_datafeed_modal';
+import { CreateWatchFlyout } from '../create_watch_flyout';
import { MultiJobActions } from '../multi_job_actions';
import PropTypes from 'prop-types';
@@ -45,6 +46,7 @@ export class JobsListView extends Component {
this.showEditJobFlyout = () => {};
this.showDeleteJobModal = () => {};
this.showStartDatafeedModal = () => {};
+ this.showCreateWatchFlyout = () => {};
this.blockRefresh = false;
}
@@ -191,6 +193,17 @@ export class JobsListView extends Component {
this.showStartDatafeedModal = () => {};
}
+ setShowCreateWatchFlyoutFunction = (func) => {
+ this.showCreateWatchFlyout = func;
+ }
+ unsetShowCreateWatchFlyoutFunction = () => {
+ this.showCreateWatchFlyout = () => {};
+ }
+ getShowCreateWatchFlyoutFunction = () => {
+ return this.showCreateWatchFlyout;
+ }
+
+
selectJobChange = (selectedJobs) => {
this.setState({ selectedJobs });
}
@@ -281,8 +294,14 @@ export class JobsListView extends Component {
this.refreshJobSummaryList(true)}
/>
+
);
}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/start_datafeed_modal.js
index 0a7a83ae21d95..e420e11b06b4d 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/start_datafeed_modal.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/start_datafeed_modal.js
@@ -19,6 +19,9 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
+ EuiHorizontalRule,
+ EuiCheckbox,
+
} from '@elastic/eui';
import moment from 'moment';
@@ -36,20 +39,20 @@ export class StartDatafeedModal extends Component {
isModalVisible: false,
startTime: moment(),
endTime: moment(),
+ createWatch: false,
+ allowCreateWatch: false,
initialSpecifiedStartTime: moment()
};
this.initialSpecifiedStartTime = moment();
this.refreshJobs = this.props.refreshJobs;
+ this.getShowCreateWatchFlyoutFunction = this.props.getShowCreateWatchFlyoutFunction;
}
componentDidMount() {
if (typeof this.props.setShowFunction === 'function') {
this.props.setShowFunction(this.showModal);
}
- if (typeof this.props.saveFunction === 'function') {
- this.externalSave = this.props.saveFunction;
- }
}
componentWillUnmount() {
@@ -66,32 +69,52 @@ export class StartDatafeedModal extends Component {
this.setState({ endTime: time });
}
+ setCreateWatch = (e) => {
+ this.setState({ createWatch: e.target.checked });
+ }
+
closeModal = () => {
this.setState({ isModalVisible: false });
}
- showModal = (jobs) => {
+ showModal = (jobs, showCreateWatchFlyout) => {
const startTime = undefined;
const endTime = moment();
const initialSpecifiedStartTime = getLowestLatestTime(jobs);
+ const allowCreateWatch = (jobs.length === 1);
this.setState({
jobs,
isModalVisible: true,
startTime,
endTime,
- initialSpecifiedStartTime
+ initialSpecifiedStartTime,
+ showCreateWatchFlyout,
+ allowCreateWatch,
+ createWatch: false,
});
}
save = () => {
+ const { jobs } = this.state;
const start = moment.isMoment(this.state.startTime) ? this.state.startTime.valueOf() : this.state.startTime;
const end = moment.isMoment(this.state.endTime) ? this.state.endTime.valueOf() : this.state.endTime;
- forceStartDatafeeds(this.state.jobs, start, end, this.refreshJobs);
+ forceStartDatafeeds(jobs, start, end, () => {
+ if (this.state.createWatch && jobs.length === 1) {
+ const jobId = jobs[0].id;
+ this.getShowCreateWatchFlyoutFunction()(jobId);
+ }
+ this.refreshJobs();
+ });
this.closeModal();
}
render() {
- const { jobs } = this.state;
+ const {
+ jobs,
+ initialSpecifiedStartTime,
+ endTime,
+ createWatch
+ } = this.state;
const startableJobs = (jobs !== undefined) ? jobs.filter(j => j.hasDatafeed) : [];
let modal;
@@ -110,11 +133,23 @@ export class StartDatafeedModal extends Component {
+ {
+ this.state.endTime === undefined &&
+
+
+
+
+ }
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/time_range_selector/time_range_selector.js
index c7a3ece449ef5..b6bd4922f845c 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/time_range_selector/time_range_selector.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/start_datafeed_modal/time_range_selector/time_range_selector.js
@@ -51,6 +51,9 @@ export class TimeRangeSelector extends Component {
case 0:
this.setEndTime(undefined);
break;
+ case 1:
+ this.setEndTime(moment());
+ break;
default:
break;
}
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch.html b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch.html
index 5f0b3626c1a81..f9bb775e99279 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch.html
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch.html
@@ -31,7 +31,7 @@
- Warning, watch ml-{{jobId}} already exists, clicking apply with overwrite the original.
+ Warning, watch ml-{{jobId}} already exists, clicking apply will overwrite the original.
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
index 93152964d81cc..33149d05d33dc 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
@@ -124,7 +124,10 @@ class CreateWatchService {
this.status.watch = this.STATUS.SAVED;
this.config.watcherEditURL =
`${chrome.getBasePath()}/app/kibana#/management/elasticsearch/watcher/watches/watch/${id}/edit?_g=()`;
- resolve();
+ resolve({
+ id,
+ url: this.config.watcherEditURL,
+ });
})
.catch((resp) => {
this.status.watch = this.STATUS.SAVE_FAILED;
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_view.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_view.js
new file mode 100644
index 0000000000000..c2c1b887f99c8
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_view.js
@@ -0,0 +1,209 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+import PropTypes from 'prop-types';
+import React, {
+ Component,
+} from 'react';
+
+import {
+ EuiCheckbox,
+ EuiFieldText,
+ EuiCallOut,
+} from '@elastic/eui';
+
+import { has } from 'lodash';
+
+import { parseInterval } from 'ui/utils/parse_interval';
+
+import { ml } from '../../../../../services/ml_api_service';
+import { SelectSeverity } from '../../../../../components/controls/select_severity/select_severity';
+import { mlCreateWatchService } from './create_watch_service';
+const STATUS = mlCreateWatchService.STATUS;
+
+export class CreateWatch extends Component {
+ constructor(props) {
+ super(props);
+ mlCreateWatchService.reset();
+ this.config = mlCreateWatchService.config;
+
+ this.state = {
+ jobId: this.props.jobId,
+ bucketSpan: this.props.bucketSpan,
+ interval: this.config.interval,
+ threshold: this.config.threshold,
+ includeEmail: this.config.emailIncluded,
+ email: this.config.email,
+ emailEnabled: false,
+ status: null,
+ watchAlreadyExists: false,
+ };
+
+ }
+
+ componentDidMount() {
+ // make the interval 2 times the bucket span
+ if (this.state.bucketSpan) {
+ const intervalObject = parseInterval(this.state.bucketSpan);
+ let bs = intervalObject.asMinutes() * 2;
+ if (bs < 1) {
+ bs = 1;
+ }
+
+ const interval = `${bs}m`;
+ this.setState({ interval }, () => {
+ this.config.interval = interval;
+ });
+ }
+
+ // load elasticsearch settings to see if email has been configured
+ ml.getNotificationSettings().then((resp) => {
+ if (has(resp, 'defaults.xpack.notification.email')) {
+ this.setState({ emailEnabled: true });
+ }
+ });
+
+ mlCreateWatchService.loadWatch(this.state.jobId)
+ .then(() => {
+ this.setState({ watchAlreadyExists: true });
+ })
+ .catch(() => {
+ this.setState({ watchAlreadyExists: false });
+ });
+ }
+
+ onThresholdChange = (threshold) => {
+ this.setState({ threshold }, () => {
+ this.config.threshold = threshold;
+ });
+ }
+
+ onIntervalChange = (e) => {
+ const interval = e.target.value;
+ this.setState({ interval }, () => {
+ this.config.interval = interval;
+ });
+ }
+
+ onIncludeEmailChanged = (e) => {
+ const includeEmail = e.target.checked;
+ this.setState({ includeEmail }, () => {
+ this.config.includeEmail = includeEmail;
+ });
+ }
+
+ onEmailChange = (e) => {
+ const email = e.target.value;
+ this.setState({ email }, () => {
+ this.config.email = email;
+ });
+ }
+
+ render() {
+ const mlSelectSeverityService = {
+ state: {
+ set: (name, threshold) => {
+ this.onThresholdChange(threshold);
+ return {
+ changed: () => {}
+ };
+ },
+ get: () => {
+ return this.config.threshold;
+ },
+ }
+ };
+ const { status } = this.state;
+
+ if (status === null || status === STATUS.SAVING || status === STATUS.SAVE_FAILED) {
+ return (
+
+
+
+
+
+
+ Now -
+
+
+
+
+
+
+
+
+
+
+
+ {
+ this.state.emailEnabled &&
+
+
+
+ {
+ this.state.includeEmail &&
+
+
+
+ }
+
+ }
+ {
+ this.state.watchAlreadyExists &&
+
+ }
+
+ );
+ } else if (status === STATUS.SAVED) {
+ return (
+ Success
+ );
+ } else {
+ return ();
+ }
+ }
+}
+CreateWatch.propTypes = {
+ jobId: PropTypes.string.isRequired,
+ bucketSpan: PropTypes.string.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less
index 7ffbf2f6b4bfd..ec7cbd09e68bc 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/styles/main.less
@@ -11,6 +11,17 @@
}
}
+ .form-group-flex {
+ display: flex;
+ }
+
+ .sub-form-group:first-child {
+ .euiFormControlLayout {
+ display: inline-block;
+ width: 70px;
+ }
+ }
+
.email-section {
padding: 10px;
padding-left: 0px;