[ML] Create watch from new jobs list (#21112)

* [ML] [WIP] Create watch from new jobs list

* removing comments

* adding interval calculation

* adding checkbox to start datafeed modal

* adding proptypes check to SelectSeverity

* fixing typo

* changes based on review

* correcting input labels
This commit is contained in:
James Gowdy 2018-07-24 11:54:22 +01:00 committed by GitHub
parent 770ff205cd
commit 33bad59e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 485 additions and 16 deletions

View file

@ -6,4 +6,3 @@
import './select_severity_directive';
import './styles/main.less';

View file

@ -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 };

View file

@ -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: (
<React.Fragment>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
size="s"
href={url}
target="_blank"
iconType="link"
>
Edit watch
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
)
};
}
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 = (
<EuiFlyout
// ownFocus
onClose={this.closeFlyout}
size="s"
>
<EuiFlyoutHeader>
<EuiTitle>
<h2>
Create watch for {jobId}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<CreateWatch
jobId={jobId}
bucketSpan={bucketSpan}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
onClick={this.closeFlyout}
flush="left"
>
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={this.save}
fill
>
Save
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
return (
<div>
{flyout}
</div>
);
}
}
CreateWatchFlyout.propTypes = {
setShowFunction: PropTypes.func.isRequired,
unsetShowFunction: 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;
* you may not use this file except in compliance with the Elastic License.
*/
export { CreateWatchFlyout } from './create_watch_flyout';

View file

@ -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) => {
@ -185,7 +185,7 @@ export class EditJobFlyout extends Component {
render() {
let flyout;
if (this.state.isModalVisible) {
if (this.state.isFlyoutVisible) {
const {
job,
jobDescription,

View file

@ -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 {
<StartDatafeedModal
setShowFunction={this.setShowStartDatafeedModalFunction}
unsetShowFunction={this.unsetShowDeleteJobModalFunction}
getShowCreateWatchFlyoutFunction={this.getShowCreateWatchFlyoutFunction}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
<CreateWatchFlyout
setShowFunction={this.setShowCreateWatchFlyoutFunction}
unsetShowFunction={this.unsetShowCreateWatchFlyoutFunction}
compile={this.props.compile}
/>
</div>
);
}

View file

@ -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 {
<EuiModalBody>
<TimeRangeSelector
startTime={this.state.initialSpecifiedStartTime}
endTime={this.state.endTime}
startTime={initialSpecifiedStartTime}
endTime={endTime}
setStartTime={this.setStartTime}
setEndTime={this.setEndTime}
/>
{
this.state.endTime === undefined &&
<div className="create-watch">
<EuiHorizontalRule />
<EuiCheckbox
id="createWatch"
label="Create watch after datafeed has started"
checked={createWatch}
onChange={this.setCreateWatch}
/>
</div>
}
</EuiModalBody>
<EuiModalFooter>

View file

@ -51,6 +51,9 @@ export class TimeRangeSelector extends Component {
case 0:
this.setEndTime(undefined);
break;
case 1:
this.setEndTime(moment());
break;
default:
break;
}

View file

@ -31,7 +31,7 @@
</div>
<div ng-if="ui.watchAlreadyExists" class="watch-exists-warning">
Warning, watch ml-{{jobId}} already exists, clicking apply with overwrite the original.
Warning, watch ml-{{jobId}} already exists, clicking apply will overwrite the original.
</div>
</div>

View file

@ -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;

View file

@ -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 (
<div className="create-watch">
<div className="form-group form-group-flex">
<div className="sub-form-group">
<div>
<label
htmlFor="selectInterval"
className="euiFormLabel"
>
Time range
</label>
</div>
Now - <EuiFieldText
id="selectInterval"
value={this.state.interval}
onChange={this.onIntervalChange}
/>
</div>
<div className="sub-form-group">
<div>
<label
htmlFor="selectSeverity"
className="euiFormLabel"
>
Severity threshold
</label>
</div>
<div className="dropdown-group">
<SelectSeverity
id="selectSeverity"
mlSelectSeverityService={mlSelectSeverityService}
/>
</div>
</div>
</div>
{
this.state.emailEnabled &&
<div className="form-group">
<EuiCheckbox
id="includeEmail"
label="Send email"
checked={this.state.includeEmail}
onChange={this.onIncludeEmailChanged}
/>
{
this.state.includeEmail &&
<div className="email-section">
<EuiFieldText
value={this.state.email}
onChange={this.onEmailChange}
placeholder="email address"
aria-label="Watch email address"
/>
</div>
}
</div>
}
{
this.state.watchAlreadyExists &&
<EuiCallOut
title={`Warning, watch ml-${this.state.jobId} already exists, clicking apply will overwrite the original.`}
/>
}
</div>
);
} else if (status === STATUS.SAVED) {
return (
<div>Success</div>
);
} else {
return (<div />);
}
}
}
CreateWatch.propTypes = {
jobId: PropTypes.string.isRequired,
bucketSpan: PropTypes.string.isRequired,
};

View file

@ -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;