[ML] Anomaly Detection alert initialisation from the ML app (#91283)

This commit is contained in:
Dima Arnautov 2021-02-15 20:47:00 +01:00 committed by GitHub
parent 0a5e054fdc
commit a8e1e47de6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 289 additions and 1159 deletions

View file

@ -89,4 +89,5 @@ export type MlAnomalyDetectionAlertParams = {
};
severity: number;
resultType: AnomalyResultType;
includeInterim: boolean;
} & AlertTypeParams;

View file

@ -57,6 +57,8 @@ export const adminMlCapabilities = {
canCreateDataFrameAnalytics: false,
canDeleteDataFrameAnalytics: false,
canStartStopDataFrameAnalytics: false,
// Alerts
canCreateMlAlerts: false,
};
export type UserMlCapabilities = typeof userMlCapabilities;

View file

@ -0,0 +1,34 @@
/*
* 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 } from 'react';
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
interface InterimResultsControlProps {
value: boolean;
onChange: (update: boolean) => void;
}
export const InterimResultsControl: FC<InterimResultsControlProps> = React.memo(
({ value, onChange }) => {
return (
<EuiFormRow>
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.interimResultsControl.label"
defaultMessage="Include interim results"
/>
}
checked={value}
onChange={onChange.bind(null, !value)}
/>
</EuiFormRow>
);
}
);

View file

@ -19,7 +19,7 @@ interface JobSelection {
export interface JobSelectorControlProps {
jobSelection?: JobSelection;
onSelectionChange: (jobSelection: JobSelection) => void;
onChange: (jobSelection: JobSelection) => void;
adJobsApiService: MlApiServices['jobs'];
/**
* Validation is handled by alerting framework
@ -29,7 +29,7 @@ export interface JobSelectorControlProps {
export const JobSelectorControl: FC<JobSelectorControlProps> = ({
jobSelection,
onSelectionChange,
onChange,
adJobsApiService,
errors,
}) => {
@ -70,7 +70,7 @@ export const JobSelectorControl: FC<JobSelectorControlProps> = ({
}
}, [adJobsApiService]);
const onChange: EuiComboBoxProps<string>['onChange'] = useCallback(
const onSelectionChange: EuiComboBoxProps<string>['onChange'] = useCallback(
(selectedOptions) => {
const selectedJobIds: JobId[] = [];
const selectedGroupIds: string[] = [];
@ -81,7 +81,7 @@ export const JobSelectorControl: FC<JobSelectorControlProps> = ({
selectedGroupIds.push(label);
}
});
onSelectionChange({
onChange({
...(selectedJobIds.length > 0 ? { jobIds: selectedJobIds } : {}),
...(selectedGroupIds.length > 0 ? { groupIds: selectedGroupIds } : {}),
});
@ -114,7 +114,7 @@ export const JobSelectorControl: FC<JobSelectorControlProps> = ({
<EuiComboBox<string>
selectedOptions={selectedOptions}
options={options}
onChange={onChange}
onChange={onSelectionChange}
fullWidth
data-test-subj={'mlAnomalyAlertJobSelection'}
isInvalid={!!errors?.length}

View file

@ -0,0 +1,105 @@
/*
* 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, useCallback, useEffect, useMemo, useState } from 'react';
import { JobId } from '../../common/types/anomaly_detection_jobs';
import { useMlKibana } from '../application/contexts/kibana';
import { ML_ALERT_TYPES } from '../../common/constants/alerts';
import { PLUGIN_ID } from '../../common/constants/app';
interface MlAnomalyAlertFlyoutProps {
jobIds: JobId[];
onSave?: () => void;
onCloseFlyout: () => void;
}
/**
* Invoke alerting flyout from the ML plugin context.
* @param jobIds
* @param onCloseFlyout
* @constructor
*/
export const MlAnomalyAlertFlyout: FC<MlAnomalyAlertFlyoutProps> = ({
jobIds,
onCloseFlyout,
onSave,
}) => {
const {
services: { triggersActionsUi },
} = useMlKibana();
const AddAlertFlyout = useMemo(
() =>
triggersActionsUi &&
triggersActionsUi.getAddAlertFlyout({
consumer: PLUGIN_ID,
onClose: () => {
onCloseFlyout();
},
// Callback for successful save
reloadAlerts: async () => {
if (onSave) {
onSave();
}
},
canChangeTrigger: false,
alertTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION,
metadata: {},
initialValues: {
params: {
jobSelection: {
jobIds,
},
},
},
}),
[triggersActionsUi]
);
return <>{AddAlertFlyout}</>;
};
interface JobListMlAnomalyAlertFlyoutProps {
setShowFunction: (callback: Function) => void;
unsetShowFunction: () => void;
}
/**
* Component to wire the Alerting flyout with the Job list view.
* @param setShowFunction
* @param unsetShowFunction
* @constructor
*/
export const JobListMlAnomalyAlertFlyout: FC<JobListMlAnomalyAlertFlyoutProps> = ({
setShowFunction,
unsetShowFunction,
}) => {
const [isVisible, setIsVisible] = useState(false);
const [jobIds, setJobIds] = useState<JobId[] | undefined>();
const showFlyoutCallback = useCallback((jobIdsUpdate: JobId[]) => {
setJobIds(jobIdsUpdate);
setIsVisible(true);
}, []);
useEffect(() => {
setShowFunction(showFlyoutCallback);
return () => {
unsetShowFunction();
};
}, []);
return isVisible && jobIds ? (
<MlAnomalyAlertFlyout
jobIds={jobIds}
onCloseFlyout={() => setIsVisible(false)}
onSave={() => {
setIsVisible(false);
}}
/>
) : null;
};

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import React, { FC, useCallback, useEffect, useMemo } from 'react';
import React, { FC, useCallback, useMemo } from 'react';
import { EuiSpacer, EuiForm } from '@elastic/eui';
import useMount from 'react-use/lib/useMount';
import { JobSelectorControl } from './job_selector';
import { useMlKibana } from '../application/contexts/kibana';
import { jobsApiProvider } from '../application/services/ml_api_service/jobs';
@ -18,6 +19,7 @@ import { PreviewAlertCondition } from './preview_alert_condition';
import { ANOMALY_THRESHOLD } from '../../common';
import { MlAnomalyDetectionAlertParams } from '../../common/types/alerts';
import { ANOMALY_RESULT_TYPE } from '../../common/constants/anomalies';
import { InterimResultsControl } from './interim_results_control';
interface MlAnomalyAlertTriggerProps {
alertParams: MlAnomalyDetectionAlertParams;
@ -25,12 +27,14 @@ interface MlAnomalyAlertTriggerProps {
key: T,
value: MlAnomalyDetectionAlertParams[T]
) => void;
setAlertProperty: (prop: string, update: Partial<MlAnomalyDetectionAlertParams>) => void;
errors: Record<keyof MlAnomalyDetectionAlertParams, string[]>;
}
const MlAnomalyAlertTrigger: FC<MlAnomalyAlertTriggerProps> = ({
alertParams,
setAlertParams,
setAlertProperty,
errors,
}) => {
const {
@ -49,21 +53,26 @@ const MlAnomalyAlertTrigger: FC<MlAnomalyAlertTriggerProps> = ({
[]
);
useEffect(function setDefaults() {
if (alertParams.severity === undefined) {
onAlertParamChange('severity')(ANOMALY_THRESHOLD.CRITICAL);
useMount(function setDefaults() {
const { jobSelection, ...rest } = alertParams;
if (Object.keys(rest).length === 0) {
setAlertProperty('params', {
// Set defaults
severity: ANOMALY_THRESHOLD.CRITICAL,
resultType: ANOMALY_RESULT_TYPE.BUCKET,
includeInterim: true,
// Preserve job selection
jobSelection,
});
}
if (alertParams.resultType === undefined) {
onAlertParamChange('resultType')(ANOMALY_RESULT_TYPE.BUCKET);
}
}, []);
});
return (
<EuiForm data-test-subj={'mlAnomalyAlertForm'}>
<JobSelectorControl
jobSelection={alertParams.jobSelection}
adJobsApiService={adJobsApiService}
onSelectionChange={useCallback(onAlertParamChange('jobSelection'), [])}
onChange={useCallback(onAlertParamChange('jobSelection'), [])}
errors={errors.jobSelection}
/>
<ResultTypeSelector
@ -75,6 +84,12 @@ const MlAnomalyAlertTrigger: FC<MlAnomalyAlertTriggerProps> = ({
onChange={useCallback(onAlertParamChange('severity'), [])}
/>
<EuiSpacer size="m" />
<InterimResultsControl
value={alertParams.includeInterim}
onChange={useCallback(onAlertParamChange('includeInterim'), [])}
/>
<EuiSpacer size="m" />
<PreviewAlertCondition alertingApiService={alertingApiService} alertParams={alertParams} />
<EuiSpacer size="m" />

View file

@ -7,14 +7,12 @@
import { i18n } from '@kbn/i18n';
import { lazy } from 'react';
import { MlStartDependencies } from '../plugin';
import { ML_ALERT_TYPES } from '../../common/constants/alerts';
import { MlAnomalyDetectionAlertParams } from '../../common/types/alerts';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
export function registerMlAlerts(
alertTypeRegistry: MlStartDependencies['triggersActionsUi']['alertTypeRegistry']
) {
alertTypeRegistry.register({
export function registerMlAlerts(triggersActionsUi: TriggersAndActionsUIPublicPluginSetup) {
triggersActionsUi.alertTypeRegistry.register({
id: ML_ALERT_TYPES.ANOMALY_DETECTION,
description: i18n.translate('xpack.ml.alertTypes.anomalyDetection.description', {
defaultMessage: 'Alert when anomaly detection jobs results match the condition.',

View file

@ -81,6 +81,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
storage: localStorage,
embeddable: deps.embeddable,
maps: deps.maps,
triggersActionsUi: deps.triggersActionsUi,
...coreStart,
};

View file

@ -19,6 +19,7 @@ import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/p
import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public';
import type { MapsStartApi } from '../../../../../maps/public';
import type { LensPublicStart } from '../../../../../lens/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
interface StartPlugins {
data: DataPublicPluginStart;
@ -28,6 +29,7 @@ interface StartPlugins {
embeddable: EmbeddableStart;
maps?: MapsStartApi;
lens?: LensPublicStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
}
export type StartServices = CoreStart &
StartPlugins & {

View file

@ -1,182 +0,0 @@
/*
* 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 PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { loadFullJob } from '../utils';
import { mlCreateWatchService } from './create_watch_service';
import { CreateWatch } from './create_watch_view';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
function getSuccessToast(id, url) {
return {
title: i18n.translate(
'xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage',
{
defaultMessage: 'Watch {id} created successfully',
values: { id },
}
),
text: (
<React.Fragment>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton size="s" href={url} target="_blank" iconType="link">
{i18n.translate('xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel', {
defaultMessage: 'Edit watch',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
),
};
}
export class CreateWatchFlyoutUI 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 = (watchCreated = false) => {
this.setState({ isFlyoutVisible: false }, () => {
if (typeof this.props.flyoutHidden === 'function') {
this.props.flyoutHidden(watchCreated);
}
});
};
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 = () => {
const { toasts } = this.props.kibana.services.notifications;
mlCreateWatchService
.createNewWatch(this.state.jobId)
.then((resp) => {
toasts.addSuccess(getSuccessToast(resp.id, resp.url));
this.closeFlyout(true);
})
.catch((error) => {
toasts.addDanger(
i18n.translate(
'xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage',
{
defaultMessage: '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>
<FormattedMessage
id="xpack.ml.jobsList.createWatchFlyout.pageTitle"
defaultMessage="Create watch for {jobId}"
values={{ 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">
<FormattedMessage
id="xpack.ml.jobsList.createWatchFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={this.save} fill>
<FormattedMessage
id="xpack.ml.jobsList.createWatchFlyout.saveButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
return <div>{flyout}</div>;
}
}
CreateWatchFlyoutUI.propTypes = {
setShowFunction: PropTypes.func.isRequired,
unsetShowFunction: PropTypes.func.isRequired,
flyoutHidden: PropTypes.func,
};
export const CreateWatchFlyout = withKibana(CreateWatchFlyoutUI);

View file

@ -1,199 +0,0 @@
/*
* 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 { template } from 'lodash';
import { http } from '../../../../services/http_service';
import emailBody from './email.html';
import emailInfluencersBody from './email_influencers.html';
import { DEFAULT_WATCH_SEVERITY } from './select_severity';
import { watch } from './watch.js';
import { i18n } from '@kbn/i18n';
import { getBasePath, getApplication } from '../../../../util/dependency_cache';
const compiledEmailBody = template(emailBody);
const compiledEmailInfluencersBody = template(emailInfluencersBody);
const emailSection = {
send_email: {
throttle_period_in_millis: 900000, // 15m
email: {
profile: 'standard',
to: [],
subject: i18n.translate('xpack.ml.newJob.simple.watcher.email.mlWatcherAlertSubjectTitle', {
defaultMessage: 'ML Watcher Alert',
}),
body: {
html: '',
},
},
},
};
// generate a random number between min and max
function randomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function saveWatch(watchModel) {
const path = `/api/watcher/watch/${watchModel.id}`;
return http({
path,
method: 'PUT',
body: JSON.stringify(watchModel.upstreamJSON),
});
}
class CreateWatchService {
constructor() {
this.config = {};
this.STATUS = {
SAVE_FAILED: -1,
SAVING: 0,
SAVED: 1,
};
this.status = {
realtimeJob: null,
watch: null,
};
}
reset() {
this.status.realtimeJob = null;
this.status.watch = null;
this.config.id = '';
this.config.includeEmail = false;
this.config.email = '';
this.config.interval = '20m';
this.config.watcherEditURL = '';
this.config.includeInfluencers = false;
// Current implementation means that default needs to match that of the select severity control.
const { display, val } = DEFAULT_WATCH_SEVERITY;
this.config.threshold = { display, val };
}
createNewWatch = function (jobId) {
return new Promise((resolve, reject) => {
this.status.watch = this.STATUS.SAVING;
if (jobId !== undefined) {
const id = `ml-${jobId}`;
this.config.id = id;
// set specific properties of the the watch
watch.input.search.request.body.query.bool.filter[0].term.job_id = jobId;
watch.input.search.request.body.query.bool.filter[1].range.timestamp.gte = `now-${this.config.interval}`;
watch.input.search.request.body.aggs.bucket_results.filter.range.anomaly_score.gte = this.config.threshold.val;
if (this.config.includeEmail && this.config.email !== '') {
const { getUrlForApp } = getApplication();
const emails = this.config.email.split(',');
emailSection.send_email.email.to = emails;
// create the html by adding the variables to the compiled email body.
emailSection.send_email.email.body.html = compiledEmailBody({
serverAddress: getUrlForApp('ml', { absolute: true }),
influencersSection:
this.config.includeInfluencers === true
? compiledEmailInfluencersBody({
topInfluencersLabel: i18n.translate(
'xpack.ml.newJob.simple.watcher.email.topInfluencersLabel',
{
defaultMessage: 'Top influencers:',
}
),
})
: '',
elasticStackMachineLearningAlertLabel: i18n.translate(
'xpack.ml.newJob.simple.watcher.email.elasticStackMachineLearningAlertLabel',
{
defaultMessage: 'Elastic Stack Machine Learning Alert',
}
),
jobLabel: i18n.translate('xpack.ml.newJob.simple.watcher.email.jobLabel', {
defaultMessage: 'Job',
}),
timeLabel: i18n.translate('xpack.ml.newJob.simple.watcher.email.timeLabel', {
defaultMessage: 'Time',
}),
anomalyScoreLabel: i18n.translate(
'xpack.ml.newJob.simple.watcher.email.anomalyScoreLabel',
{
defaultMessage: 'Anomaly score',
}
),
openInAnomalyExplorerLinkText: i18n.translate(
'xpack.ml.newJob.simple.watcher.email.openInAnomalyExplorerLinkText',
{
defaultMessage: 'Click here to open in Anomaly Explorer.',
}
),
topRecordsLabel: i18n.translate(
'xpack.ml.newJob.simple.watcher.email.topRecordsLabel',
{ defaultMessage: 'Top records:' }
),
});
// add email section to watch
watch.actions.send_email = emailSection.send_email;
}
// set the trigger interval to be a random number between 60 and 120 seconds
// this is to avoid all watches firing at once if the server restarts
// and the watches synchronize
const triggerInterval = randomNumber(60, 120);
watch.trigger.schedule.interval = `${triggerInterval}s`;
const watchModel = {
id,
upstreamJSON: {
id,
type: 'json',
isNew: false, // Set to false, as we want to allow watches to be overwritten.
isActive: true,
watch,
},
};
const basePath = getBasePath();
if (id !== '') {
saveWatch(watchModel)
.then(() => {
this.status.watch = this.STATUS.SAVED;
this.config.watcherEditURL = `${basePath.get()}/app/management/insightsAndAlerting/watcher/watches/watch/${id}/edit?_g=()`;
resolve({
id,
url: this.config.watcherEditURL,
});
})
.catch((resp) => {
this.status.watch = this.STATUS.SAVE_FAILED;
reject(resp);
});
}
} else {
this.status.watch = this.STATUS.SAVE_FAILED;
reject();
}
});
};
loadWatch(jobId) {
const id = `ml-${jobId}`;
const path = `/api/watcher/watch/${id}`;
return http({
path,
method: 'GET',
});
}
}
export const mlCreateWatchService = new CreateWatchService();

View file

@ -1,215 +0,0 @@
/*
* 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 PropTypes from 'prop-types';
import React, { Component } from 'react';
import { EuiCheckbox, EuiFieldText, EuiCallOut } from '@elastic/eui';
import { has } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { parseInterval } from '../../../../../../common/util/parse_interval';
import { ml } from '../../../../services/ml_api_service';
import { SelectSeverity } from './select_severity';
import { mlCreateWatchService } from './create_watch_service';
const STATUS = mlCreateWatchService.STATUS;
export class CreateWatch extends Component {
static propTypes = {
jobId: PropTypes.string.isRequired,
bucketSpan: PropTypes.string.isRequired,
};
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 { 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">
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.timeRangeLabel"
defaultMessage="Time range"
/>
</label>
</div>
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.nowLabel"
defaultMessage="Now - {selectInterval}"
values={{
selectInterval: (
<EuiFieldText
id="selectInterval"
value={this.state.interval}
onChange={this.onIntervalChange}
/>
),
}}
/>
</div>
<div className="sub-form-group">
<div>
<label htmlFor="selectSeverity" className="euiFormLabel">
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.severityThresholdLabel"
defaultMessage="Severity threshold"
/>
</label>
</div>
<div className="dropdown-group">
<SelectSeverity onChange={this.onThresholdChange} />
</div>
</div>
</div>
{this.state.emailEnabled && (
<div className="form-group">
<EuiCheckbox
id="includeEmail"
label={
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.sendEmailLabel"
defaultMessage="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={i18n.translate(
'xpack.ml.newJob.simple.createWatchView.emailAddressPlaceholder',
{
defaultMessage: 'email address',
}
)}
aria-label={i18n.translate(
'xpack.ml.newJob.simple.createWatchView.watchEmailAddressAriaLabel',
{
defaultMessage: 'Watch email address',
}
)}
/>
</div>
)}
</div>
)}
{this.state.watchAlreadyExists && (
<EuiCallOut
title={
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.watchAlreadyExistsWarningMessage"
defaultMessage="Warning, watch ml-{jobId} already exists, clicking apply will overwrite the original."
values={{
jobId: this.state.jobId,
}}
/>
}
/>
)}
</div>
);
} else if (status === STATUS.SAVED) {
return (
<div>
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.successLabel"
defaultMessage="Success"
/>
</div>
);
} else {
return <div />;
}
}
}

View file

@ -1,43 +0,0 @@
<html>
<body>
<strong>
<%= elasticStackMachineLearningAlertLabel %>
</strong>
<br />
<br />
<strong>
<%= jobLabel %>
</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}}
<br />
<strong>
<%= timeLabel %>
</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_iso8601.0}}
<br />
<strong>
<%= anomalyScoreLabel %>
</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.score.0}}
<br />
<br />
<a href="<%= serverAddress %>#/explorer/?_g=(ml:(jobIds:!('{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}}')),time:(from:'{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.start.0}}',mode:absolute,to:'{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.end.0}}'))&_a=(explorer:(mlExplorerSwimlane:(selectedLanes:!(Overall),selectedTimes:{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_epoch.0}},selectedType:overall)),query:(query_string:(analyze_wildcard:!t,query:'*')))">
<%= openInAnomalyExplorerLinkText %>
</a>
<br />
<br />
<%= influencersSection %>
<strong>
<%= topRecordsLabel %>
</strong>
<br />
{{#ctx.payload.aggregations.record_results.top_record_hits.hits.hits}}
{{_source.function}}({{_source.field_name}}) {{_source.by_field_value}} {{_source.over_field_value}} {{_source.partition_field_value}} [{{fields.score.0}}]
<br />
{{/ctx.payload.aggregations.record_results.top_record_hits.hits.hits}}
</body>
</html>

View file

@ -1,9 +0,0 @@
<strong>
<%= topInfluencersLabel %>
</strong>
<br />
{{#ctx.payload.aggregations.influencer_results.top_influencer_hits.hits.hits}}
{{_source.influencer_field_name}} = {{_source.influencer_field_value}} [{{fields.score.0}}]
<br />
{{/ctx.payload.aggregations.influencer_results.top_influencer_hits.hits.hits}}
<br />

View file

@ -1,8 +0,0 @@
/*
* 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 { CreateWatchFlyout } from './create_watch_flyout';

View file

@ -1,134 +0,0 @@
/*
* 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.
*/
/*
* React component for rendering a select element with threshold levels.
* This is basically a copy of SelectSeverity in public/application/components/controls/select_severity
* but which stores its state internally rather than in the appState
*/
import React, { Fragment, FC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
import { getSeverityColor } from '../../../../../../common/util/anomaly_utils';
const warningLabel = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', {
defaultMessage: 'warning',
});
const minorLabel = i18n.translate('xpack.ml.controls.selectSeverity.minorLabel', {
defaultMessage: 'minor',
});
const majorLabel = i18n.translate('xpack.ml.controls.selectSeverity.majorLabel', {
defaultMessage: 'major',
});
const criticalLabel = i18n.translate('xpack.ml.controls.selectSeverity.criticalLabel', {
defaultMessage: 'critical',
});
const optionsMap = {
[warningLabel]: 0,
[minorLabel]: 25,
[majorLabel]: 50,
[criticalLabel]: 75,
};
interface TableSeverity {
val: number;
display: string;
color: string;
}
export const SEVERITY_OPTIONS: TableSeverity[] = [
{
val: 0,
display: warningLabel,
color: getSeverityColor(0),
},
{
val: 25,
display: minorLabel,
color: getSeverityColor(25),
},
{
val: 50,
display: majorLabel,
color: getSeverityColor(50),
},
{
val: 75,
display: criticalLabel,
color: getSeverityColor(75),
},
];
function optionValueToThreshold(value: number) {
// Get corresponding threshold object with required display and val properties from the specified value.
let threshold = SEVERITY_OPTIONS.find((opt) => opt.val === value);
// Default to warning if supplied value doesn't map to one of the options.
if (threshold === undefined) {
threshold = SEVERITY_OPTIONS[0];
}
return threshold;
}
export const DEFAULT_WATCH_SEVERITY = SEVERITY_OPTIONS[3];
const getSeverityOptions = () =>
SEVERITY_OPTIONS.map(({ color, display, val }) => ({
value: display,
inputDisplay: (
<Fragment>
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
{display}
</EuiHealth>
</Fragment>
),
dropdownDisplay: (
<Fragment>
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
{display}
</EuiHealth>
<EuiSpacer size="xs" />
<EuiText size="xs" color="subdued">
<p className="euiTextColor--subdued">
<FormattedMessage
id="xpack.ml.controls.selectSeverity.scoreDetailsDescription"
defaultMessage="score {value} and above"
values={{ value: val }}
/>
</p>
</EuiText>
</Fragment>
),
}));
interface Props {
onChange: (sev: TableSeverity) => void;
}
export const SelectSeverity: FC<Props> = ({ onChange }) => {
const [severity, setSeverity] = useState(DEFAULT_WATCH_SEVERITY);
const onSeverityChange = (valueDisplay: string) => {
const option = optionValueToThreshold(optionsMap[valueDisplay]);
setSeverity(option);
onChange(option);
};
return (
<EuiSuperSelect
hasDividers
options={getSeverityOptions()}
valueOfSelected={severity.display}
onChange={onSeverityChange}
/>
);
};

View file

@ -1,232 +0,0 @@
/*
* 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 { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns';
export const watch = {
trigger: {
schedule: {
interval: '60s',
},
},
input: {
search: {
request: {
search_type: 'query_then_fetch',
indices: [ML_RESULTS_INDEX_PATTERN],
body: {
size: 0,
query: {
bool: {
filter: [
{
term: {
job_id: null,
},
},
{
range: {
timestamp: {
gte: null,
},
},
},
{
terms: {
result_type: ['bucket', 'record', 'influencer'],
},
},
],
},
},
aggs: {
bucket_results: {
filter: {
range: {
anomaly_score: {
gte: null,
},
},
},
aggs: {
top_bucket_hits: {
top_hits: {
sort: [
{
anomaly_score: {
order: 'desc',
},
},
],
_source: {
includes: [
'job_id',
'result_type',
'timestamp',
'anomaly_score',
'is_interim',
],
},
size: 1,
script_fields: {
start: {
script: {
lang: 'painless',
source: `LocalDateTime.ofEpochSecond((doc["timestamp"].value.getMillis()-((doc["bucket_span"].value * 1000)
* params.padding)) / 1000, 0, ZoneOffset.UTC).toString()+\":00.000Z\"`,
params: {
padding: 10,
},
},
},
end: {
script: {
lang: 'painless',
source: `LocalDateTime.ofEpochSecond((doc["timestamp"].value.getMillis()+((doc["bucket_span"].value * 1000)
* params.padding)) / 1000, 0, ZoneOffset.UTC).toString()+\":00.000Z\"`,
params: {
padding: 10,
},
},
},
timestamp_epoch: {
script: {
lang: 'painless',
source: 'doc["timestamp"].value.getMillis()/1000',
},
},
timestamp_iso8601: {
script: {
lang: 'painless',
source: 'doc["timestamp"].value',
},
},
score: {
script: {
lang: 'painless',
source: 'Math.round(doc["anomaly_score"].value)',
},
},
},
},
},
},
},
influencer_results: {
filter: {
range: {
influencer_score: {
gte: 3,
},
},
},
aggs: {
top_influencer_hits: {
top_hits: {
sort: [
{
influencer_score: {
order: 'desc',
},
},
],
_source: {
includes: [
'result_type',
'timestamp',
'influencer_field_name',
'influencer_field_value',
'influencer_score',
'isInterim',
],
},
size: 3,
script_fields: {
score: {
script: {
lang: 'painless',
source: 'Math.round(doc["influencer_score"].value)',
},
},
},
},
},
},
},
record_results: {
filter: {
range: {
record_score: {
gte: 3,
},
},
},
aggs: {
top_record_hits: {
top_hits: {
sort: [
{
record_score: {
order: 'desc',
},
},
],
_source: {
includes: [
'result_type',
'timestamp',
'record_score',
'is_interim',
'function',
'field_name',
'by_field_value',
'over_field_value',
'partition_field_value',
],
},
size: 3,
script_fields: {
score: {
script: {
lang: 'painless',
source: 'Math.round(doc["record_score"].value)',
},
},
},
},
},
},
},
},
},
},
},
},
condition: {
compare: {
'ctx.payload.aggregations.bucket_results.doc_count': {
gt: 0,
},
},
},
actions: {
log: {
logging: {
level: 'info',
text: '', // this gets populated below.
},
},
},
};
// Add logging text. Broken over a few lines due to its length.
let txt =
'Alert for job [{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}}] at ';
txt +=
'[{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_iso8601.0}}] score ';
txt += '[{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.score.0}}]';
watch.actions.log.logging.text = txt;

View file

@ -17,7 +17,8 @@ export function actionsMenuContent(
showEditJobFlyout,
showDeleteJobModal,
showStartDatafeedModal,
refreshJobs
refreshJobs,
showCreateAlertFlyout
) {
const canCreateJob = checkPermission('canCreateJob') && mlNodesAvailable();
const canUpdateJob = checkPermission('canUpdateJob');
@ -25,6 +26,7 @@ export function actionsMenuContent(
const canUpdateDatafeed = checkPermission('canUpdateDatafeed');
const canStartStopDatafeed = checkPermission('canStartStopDatafeed') && mlNodesAvailable();
const canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable();
const canCreateMlAlerts = checkPermission('canCreateMlAlerts');
return [
{
@ -59,6 +61,22 @@ export function actionsMenuContent(
},
'data-test-subj': 'mlActionButtonStopDatafeed',
},
{
name: i18n.translate('xpack.ml.jobsList.managementActions.createAlertLabel', {
defaultMessage: 'Create alert',
}),
description: i18n.translate('xpack.ml.jobsList.managementActions.createAlertLabel', {
defaultMessage: 'Create alert',
}),
icon: 'bell',
enabled: (item) => item.deleting !== true,
available: () => canCreateMlAlerts,
onClick: (item) => {
showCreateAlertFlyout([item.id]);
closeMenu(true);
},
'data-test-subj': 'mlActionButtonCreateAlert',
},
{
name: i18n.translate('xpack.ml.jobsList.managementActions.closeJobLabel', {
defaultMessage: 'Close job',

View file

@ -116,8 +116,8 @@ export class JobsList extends Component {
onSelectionChange: this.props.selectJobChange,
};
// Adding 'width' props to columns for use in the Kibana management jobs list table
// The version of the table used in ML > Job Managment depends on many EUI class overrides that set the width explicitly.
// The ML > Job Managment table won't change as the overwritten class styles take precedence, though these values may need to
// The version of the table used in ML > Job Management depends on many EUI class overrides that set the width explicitly.
// The ML > Job Management table won't change as the overwritten class styles take precedence, though these values may need to
// be updated if we move to always using props for width.
const columns = [
{
@ -299,7 +299,8 @@ export class JobsList extends Component {
this.props.showEditJobFlyout,
this.props.showDeleteJobModal,
this.props.showStartDatafeedModal,
this.props.refreshJobs
this.props.refreshJobs,
this.props.showCreateAlertFlyout
),
});
}
@ -371,6 +372,7 @@ JobsList.propTypes = {
showEditJobFlyout: PropTypes.func,
showDeleteJobModal: PropTypes.func,
showStartDatafeedModal: PropTypes.func,
showCreateAlertFlyout: PropTypes.func,
refreshJobs: PropTypes.func,
selectedJobsCount: PropTypes.number.isRequired,
loading: PropTypes.bool,

View file

@ -28,7 +28,6 @@ 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 { NewJobButton } from '../new_job_button';
import { JobStatsBar } from '../jobs_stats_bar';
@ -40,6 +39,7 @@ import { UpgradeWarning } from '../../../../components/upgrade';
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;
@ -66,7 +66,7 @@ export class JobsListView extends Component {
this.showEditJobFlyout = () => {};
this.showDeleteJobModal = () => {};
this.showStartDatafeedModal = () => {};
this.showCreateWatchFlyout = () => {};
this.showCreateAlertFlyout = () => {};
// work around to keep track of whether the component is mounted
// used to block timeouts for results polling
// which can run after unmounting
@ -205,14 +205,14 @@ export class JobsListView extends Component {
this.showStartDatafeedModal = () => {};
};
setShowCreateWatchFlyoutFunction = (func) => {
this.showCreateWatchFlyout = func;
setShowCreateAlertFlyoutFunction = (func) => {
this.showCreateAlertFlyout = func;
};
unsetShowCreateWatchFlyoutFunction = () => {
this.showCreateWatchFlyout = () => {};
unsetShowCreateAlertFlyoutFunction = () => {
this.showCreateAlertFlyout = () => {};
};
getShowCreateWatchFlyoutFunction = () => {
return this.showCreateWatchFlyout;
getShowCreateAlertFlyoutFunction = () => {
return this.showCreateAlertFlyout;
};
selectJobChange = (selectedJobs) => {
@ -477,6 +477,7 @@ export class JobsListView extends Component {
allJobIds={jobIds}
showStartDatafeedModal={this.showStartDatafeedModal}
showDeleteJobModal={this.showDeleteJobModal}
showCreateAlertFlyout={this.showCreateAlertFlyout}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
<JobFilterBar
@ -497,6 +498,7 @@ export class JobsListView extends Component {
jobsViewState={this.props.jobsViewState}
onJobsViewStateUpdate={this.props.onJobsViewStateUpdate}
selectedJobsCount={this.state.selectedJobs.length}
showCreateAlertFlyout={this.showCreateAlertFlyout}
loading={loading}
/>
<EditJobFlyout
@ -513,12 +515,12 @@ export class JobsListView extends Component {
<StartDatafeedModal
setShowFunction={this.setShowStartDatafeedModalFunction}
unsetShowFunction={this.unsetShowDeleteJobModalFunction}
getShowCreateWatchFlyoutFunction={this.getShowCreateWatchFlyoutFunction}
getShowCreateAlertFlyoutFunction={this.getShowCreateAlertFlyoutFunction}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
<CreateWatchFlyout
setShowFunction={this.setShowCreateWatchFlyoutFunction}
unsetShowFunction={this.unsetShowCreateWatchFlyoutFunction}
<JobListMlAnomalyAlertFlyout
setShowFunction={this.setShowCreateAlertFlyoutFunction}
unsetShowFunction={this.unsetShowCreateAlertFlyoutFunction}
/>
</div>
</EuiPageContent>

View file

@ -27,6 +27,7 @@ class MultiJobActionsMenuUI extends Component {
this.canDeleteJob = checkPermission('canDeleteJob');
this.canStartStopDatafeed = checkPermission('canStartStopDatafeed') && mlNodesAvailable();
this.canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable();
this.canCreateMlAlerts = checkPermission('canCreateMlAlerts');
}
onButtonClick = () => {
@ -144,6 +145,26 @@ class MultiJobActionsMenuUI extends Component {
);
}
if (this.canCreateMlAlerts) {
items.push(
<EuiContextMenuItem
key="create alert"
icon="bell"
disabled={false}
onClick={() => {
this.props.showCreateAlertFlyout(this.props.jobs.map(({ id }) => id));
this.closePopover();
}}
data-test-subj="mlADJobListMultiSelectCreateAlertActionButton"
>
<FormattedMessage
id="xpack.ml.jobsList.multiJobsActions.createAlertsLabel"
defaultMessage="Create alert"
/>
</EuiContextMenuItem>
);
}
return (
<EuiPopover
button={button}
@ -162,6 +183,7 @@ MultiJobActionsMenuUI.propTypes = {
showStartDatafeedModal: PropTypes.func.isRequired,
showDeleteJobModal: PropTypes.func.isRequired,
refreshJobs: PropTypes.func.isRequired,
showCreateAlertFlyout: PropTypes.func.isRequired,
};
export const MultiJobActionsMenu = MultiJobActionsMenuUI;

View file

@ -54,6 +54,7 @@ export class MultiJobActions extends Component {
showStartDatafeedModal={this.props.showStartDatafeedModal}
showDeleteJobModal={this.props.showDeleteJobModal}
refreshJobs={this.props.refreshJobs}
showCreateAlertFlyout={this.props.showCreateAlertFlyout}
/>
</Fragment>
)}
@ -67,4 +68,5 @@ MultiJobActions.propTypes = {
showStartDatafeedModal: PropTypes.func.isRequired,
showDeleteJobModal: PropTypes.func.isRequired,
refreshJobs: PropTypes.func.isRequired,
showCreateAlertFlyout: PropTypes.func.isRequired,
};

View file

@ -39,8 +39,8 @@ export class StartDatafeedModal extends Component {
isModalVisible: false,
startTime: now,
endTime: now,
createWatch: false,
allowCreateWatch: false,
createAlert: false,
allowCreateAlert: false,
initialSpecifiedStartTime: now,
now,
timeRangeValid: true,
@ -48,7 +48,7 @@ export class StartDatafeedModal extends Component {
this.initialSpecifiedStartTime = now;
this.refreshJobs = this.props.refreshJobs;
this.getShowCreateWatchFlyoutFunction = this.props.getShowCreateWatchFlyoutFunction;
this.getShowCreateAlertFlyoutFunction = this.props.getShowCreateAlertFlyoutFunction;
}
componentDidMount() {
@ -71,8 +71,8 @@ export class StartDatafeedModal extends Component {
this.setState({ endTime: time });
};
setCreateWatch = (e) => {
this.setState({ createWatch: e.target.checked });
setCreateAlert = (e) => {
this.setState({ createAlert: e.target.checked });
};
closeModal = () => {
@ -83,21 +83,21 @@ export class StartDatafeedModal extends Component {
this.setState({ timeRangeValid });
};
showModal = (jobs, showCreateWatchFlyout) => {
showModal = (jobs, showCreateAlertFlyout) => {
const startTime = undefined;
const now = moment();
const endTime = now;
const initialSpecifiedStartTime = getLowestLatestTime(jobs);
const allowCreateWatch = jobs.length === 1;
const allowCreateAlert = jobs.length > 0;
this.setState({
jobs,
isModalVisible: true,
startTime,
endTime,
initialSpecifiedStartTime,
showCreateWatchFlyout,
allowCreateWatch,
createWatch: false,
showCreateAlertFlyout,
allowCreateAlert,
createAlert: false,
now,
});
};
@ -112,9 +112,8 @@ export class StartDatafeedModal extends Component {
: this.state.endTime;
forceStartDatafeeds(jobs, start, end, () => {
if (this.state.createWatch && jobs.length === 1) {
const jobId = jobs[0].id;
this.getShowCreateWatchFlyoutFunction()(jobId);
if (this.state.createAlert && jobs.length > 0) {
this.getShowCreateAlertFlyoutFunction()(jobs.map((job) => job.id));
}
this.refreshJobs();
});
@ -127,7 +126,7 @@ export class StartDatafeedModal extends Component {
initialSpecifiedStartTime,
startTime,
endTime,
createWatch,
createAlert,
now,
timeRangeValid,
} = this.state;
@ -172,15 +171,15 @@ export class StartDatafeedModal extends Component {
<div className="create-watch">
<EuiHorizontalRule />
<EuiCheckbox
id="createWatch"
id="createAlert"
label={
<FormattedMessage
id="xpack.ml.jobsList.startDatafeedModal.createWatchDescription"
defaultMessage="Create watch after datafeed has started"
id="xpack.ml.jobsList.startDatafeedModal.createAlertDescription"
defaultMessage="Create alert after datafeed has started"
/>
}
checked={createWatch}
onChange={this.setCreateWatch}
checked={createAlert}
onChange={this.setCreateAlert}
/>
</div>
)}

View file

@ -13,38 +13,21 @@ import { JobRunner } from '../../../../../common/job_runner';
import { useMlKibana } from '../../../../../../../contexts/kibana';
import { extractErrorMessage } from '../../../../../../../../../common/util/errors';
// @ts-ignore
import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index';
import { JobCreatorContext } from '../../../job_creator_context';
import { DATAFEED_STATE } from '../../../../../../../../../common/constants/states';
import { MlAnomalyAlertFlyout } from '../../../../../../../../alerting/ml_alerting_flyout';
interface Props {
jobRunner: JobRunner | null;
}
type ShowFlyout = (jobId: string) => void;
export const PostSaveOptions: FC<Props> = ({ jobRunner }) => {
const {
services: { notifications },
} = useMlKibana();
const { jobCreator } = useContext(JobCreatorContext);
const [datafeedState, setDatafeedState] = useState(DATAFEED_STATE.STOPPED);
const [watchFlyoutVisible, setWatchFlyoutVisible] = useState(false);
const [watchCreated, setWatchCreated] = useState(false);
function setShowCreateWatchFlyoutFunction(showFlyout: ShowFlyout) {
showFlyout(jobCreator.jobId);
}
function flyoutHidden(jobCreated: boolean) {
setWatchFlyoutVisible(false);
setWatchCreated(jobCreated);
}
function unsetShowCreateWatchFlyoutFunction() {
setWatchFlyoutVisible(false);
}
const [alertFlyoutVisible, setAlertFlyoutVisible] = useState(false);
async function startJobInRealTime() {
const { toasts } = notifications;
@ -93,28 +76,26 @@ export const PostSaveOptions: FC<Props> = ({ jobRunner }) => {
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={
datafeedState === DATAFEED_STATE.STOPPED ||
datafeedState === DATAFEED_STATE.STARTING ||
watchCreated === true
datafeedState === DATAFEED_STATE.STOPPED || datafeedState === DATAFEED_STATE.STARTING
}
onClick={() => setWatchFlyoutVisible(true)}
data-test-subj="mlJobWizardButtonCreateWatch"
onClick={setAlertFlyoutVisible.bind(null, true)}
data-test-subj="mlJobWizardButtonCreateAlert"
>
<FormattedMessage
id="xpack.ml.newJob.wizard.summaryStep.postSaveOptions.createWatch"
defaultMessage="Create watch"
id="xpack.ml.newJob.wizard.summaryStep.postSaveOptions.createAlert"
defaultMessage="Create alert"
/>
</EuiButton>
</EuiFlexItem>
{datafeedState === DATAFEED_STATE.STARTED && watchFlyoutVisible && (
<CreateWatchFlyout
setShowFunction={setShowCreateWatchFlyoutFunction}
unsetShowFunction={unsetShowCreateWatchFlyoutFunction}
flyoutHidden={flyoutHidden}
{datafeedState === DATAFEED_STATE.STARTED && alertFlyoutVisible && (
<MlAnomalyAlertFlyout
jobIds={[jobCreator.jobId]}
onCloseFlyout={setAlertFlyoutVisible.bind(null, false)}
/>
)}
</Fragment>

View file

@ -62,7 +62,7 @@ export interface MlStartDependencies {
embeddable: EmbeddableStart;
maps?: MapsStartApi;
lens?: LensPublicStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
}
export interface MlSetupDependencies {
@ -76,7 +76,7 @@ export interface MlSetupDependencies {
kibanaVersion: string;
share: SharePluginSetup;
indexPatternManagement: IndexPatternManagementSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup;
}
export type MlCoreSetup = CoreSetup<MlStartDependencies, MlPluginStart>;
@ -129,6 +129,10 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
this.urlGenerator = registerUrlGenerator(pluginsSetup.share, core);
}
if (pluginsSetup.triggersActionsUi) {
registerMlAlerts(pluginsSetup.triggersActionsUi);
}
const licensing = pluginsSetup.licensing.license$.pipe(take(1));
licensing.subscribe(async (license) => {
const [coreStart] = await core.getStartServices();
@ -190,7 +194,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
http: core.http,
i18n: core.i18n,
});
registerMlAlerts(deps.triggersActionsUi.alertTypeRegistry);
return {
urlGenerator: this.urlGenerator,
};

View file

@ -309,6 +309,13 @@ export function alertingServiceProvider(mlClient: MlClient) {
result_type: Object.values(ANOMALY_RESULT_TYPE),
},
},
...(params.includeInterim
? []
: [
{
term: { is_interim: false },
},
]),
],
},
},

View file

@ -51,7 +51,7 @@ describe('check_capabilities', () => {
);
const { capabilities } = await getCapabilities();
const count = Object.keys(capabilities).length;
expect(count).toBe(28);
expect(count).toBe(29);
});
});
@ -102,6 +102,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateMlAlerts).toBe(false);
});
test('full capabilities', async () => {

View file

@ -17,6 +17,8 @@ export function alertingRoutes({ router, routeGuard }: RouteInitialization) {
* @api {post} /api/ml/alerting/preview Preview alerting condition
* @apiName PreviewAlert
* @apiDescription Returns a preview of the alerting condition
*
* @apiSchema (body) mlAnomalyDetectionAlertPreviewRequest
*/
router.post(
{

View file

@ -27,6 +27,7 @@ export const mlAnomalyDetectionAlertParams = schema.object({
),
severity: schema.number(),
resultType: schema.string(),
includeInterim: schema.boolean({ defaultValue: true }),
});
export const mlAnomalyDetectionAlertPreviewRequest = schema.object({

View file

@ -37,6 +37,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = {
canDeleteDataFrameAnalytics: false,
canCreateDataFrameAnalytics: false,
canStartStopDataFrameAnalytics: false,
canCreateMlAlerts: false,
},
isPlatinumOrTrialLicense: false,
mlFeatureEnabledInSpace: false,

View file

@ -13288,12 +13288,6 @@
"xpack.ml.jobsList.closeJobErrorMessage": "ジョブをクローズできませんでした",
"xpack.ml.jobsList.collapseJobDetailsAriaLabel": "{itemId} の詳細を非表示",
"xpack.ml.jobsList.createNewJobButtonLabel": "ジョブを作成",
"xpack.ml.jobsList.createWatchFlyout.closeButtonLabel": "閉じる",
"xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel": "ウォッチを編集",
"xpack.ml.jobsList.createWatchFlyout.pageTitle": "{jobId} のウォッチを作成",
"xpack.ml.jobsList.createWatchFlyout.saveButtonLabel": "保存",
"xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage": "ウォッチ {id} が作成されました",
"xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage": "ウォッチを保存できませんでした",
"xpack.ml.jobsList.datafeedStateLabel": "データフィード状態",
"xpack.ml.jobsList.deleteActionStatusText": "削除",
"xpack.ml.jobsList.deletedActionStatusText": "削除されました",
@ -13443,7 +13437,6 @@
"xpack.ml.jobsList.startDatafeedModal.continueFromNowLabel": "今から続行",
"xpack.ml.jobsList.startDatafeedModal.continueFromSpecifiedTimeLabel": "特定の時刻から続行",
"xpack.ml.jobsList.startDatafeedModal.continueFromStartTimeLabel": "{formattedLatestStartTime} から続行",
"xpack.ml.jobsList.startDatafeedModal.createWatchDescription": "データフィードの開始後ウォッチを作成します",
"xpack.ml.jobsList.startDatafeedModal.enterDateText\"": "日付を入力",
"xpack.ml.jobsList.startDatafeedModal.noEndTimeLabel": "終了時刻が指定されていません (リアルタイム検索)",
"xpack.ml.jobsList.startDatafeedModal.searchEndTimeTitle": "検索終了時刻",
@ -13657,23 +13650,7 @@
"xpack.ml.newJob.recognize.viewResultsAriaLabel": "結果を表示",
"xpack.ml.newJob.recognize.viewResultsLinkText": "結果を表示",
"xpack.ml.newJob.recognize.visualizationsLabel": "ビジュアライゼーション",
"xpack.ml.newJob.simple.createWatchView.emailAddressPlaceholder": "メールアドレス",
"xpack.ml.newJob.simple.createWatchView.nowLabel": "今 - {selectInterval}",
"xpack.ml.newJob.simple.createWatchView.sendEmailLabel": "メールを送信",
"xpack.ml.newJob.simple.createWatchView.severityThresholdLabel": "深刻度のしきい値",
"xpack.ml.newJob.simple.createWatchView.successLabel": "成功",
"xpack.ml.newJob.simple.createWatchView.timeRangeLabel": "時間範囲",
"xpack.ml.newJob.simple.createWatchView.watchAlreadyExistsWarningMessage": "警告、ウォッチ mi-{jobId} は既に存在します。適用をクリックするとオリジナルが上書きされます。",
"xpack.ml.newJob.simple.createWatchView.watchEmailAddressAriaLabel": "ウォッチのメールアドレス",
"xpack.ml.newJob.simple.recognize.jobsCreationFailedTitle": "ジョブの作成に失敗",
"xpack.ml.newJob.simple.watcher.email.anomalyScoreLabel": "異常スコア",
"xpack.ml.newJob.simple.watcher.email.elasticStackMachineLearningAlertLabel": "Elastic Stack 機械学習アラート",
"xpack.ml.newJob.simple.watcher.email.jobLabel": "ジョブ名",
"xpack.ml.newJob.simple.watcher.email.mlWatcherAlertSubjectTitle": "ML Watcher アラート",
"xpack.ml.newJob.simple.watcher.email.openInAnomalyExplorerLinkText": "異常エクスプローラーを開くにはここをクリックしてください。",
"xpack.ml.newJob.simple.watcher.email.timeLabel": "時間",
"xpack.ml.newJob.simple.watcher.email.topInfluencersLabel": "トップ影響因子:",
"xpack.ml.newJob.simple.watcher.email.topRecordsLabel": "トップの記録:",
"xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error": "インデックスの開始時刻と終了時刻の取得中にエラーが発生しました",
"xpack.ml.newJob.wizard.categorizationAnalyzerFlyout.closeButton": "閉じる",
"xpack.ml.newJob.wizard.categorizationAnalyzerFlyout.saveButton": "保存",
@ -13904,7 +13881,6 @@
"xpack.ml.newJob.wizard.summaryStep.jobDetails.splitField.title": "フィールドの分割",
"xpack.ml.newJob.wizard.summaryStep.jobDetails.summaryCountField.title": "サマリーカウントフィールド",
"xpack.ml.newJob.wizard.summaryStep.jobDetails.useDedicatedIndex.title": "専用インデックスを使用",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.createWatch": "ウォッチを作成",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTime": "リアルタイムで実行中のジョブを開始",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeError": "ジョブの開始エラー",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeSuccess": "ジョブ {jobId} が開始しました",

View file

@ -13320,12 +13320,6 @@
"xpack.ml.jobsList.closeJobErrorMessage": "作业无法关闭",
"xpack.ml.jobsList.collapseJobDetailsAriaLabel": "隐藏 {itemId} 的详情",
"xpack.ml.jobsList.createNewJobButtonLabel": "创建作业",
"xpack.ml.jobsList.createWatchFlyout.closeButtonLabel": "关闭",
"xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel": "编辑监视",
"xpack.ml.jobsList.createWatchFlyout.pageTitle": "创建 {jobId} 的监视",
"xpack.ml.jobsList.createWatchFlyout.saveButtonLabel": "保存",
"xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage": "监视 {id} 已成功创建",
"xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage": "无法保存监视",
"xpack.ml.jobsList.datafeedStateLabel": "数据馈送状态",
"xpack.ml.jobsList.deleteActionStatusText": "删除",
"xpack.ml.jobsList.deletedActionStatusText": "已删除",
@ -13475,7 +13469,6 @@
"xpack.ml.jobsList.startDatafeedModal.continueFromNowLabel": "从当前继续",
"xpack.ml.jobsList.startDatafeedModal.continueFromSpecifiedTimeLabel": "从指定时间继续",
"xpack.ml.jobsList.startDatafeedModal.continueFromStartTimeLabel": "从 {formattedLatestStartTime} 继续",
"xpack.ml.jobsList.startDatafeedModal.createWatchDescription": "在数据馈送开始后创建监视",
"xpack.ml.jobsList.startDatafeedModal.enterDateText\"": "输入日期",
"xpack.ml.jobsList.startDatafeedModal.noEndTimeLabel": "无结束时间(实时搜索)",
"xpack.ml.jobsList.startDatafeedModal.searchEndTimeTitle": "搜索结束时间",
@ -13694,23 +13687,7 @@
"xpack.ml.newJob.recognize.viewResultsAriaLabel": "查看结果",
"xpack.ml.newJob.recognize.viewResultsLinkText": "查看结果",
"xpack.ml.newJob.recognize.visualizationsLabel": "可视化",
"xpack.ml.newJob.simple.createWatchView.emailAddressPlaceholder": "电子邮件地址",
"xpack.ml.newJob.simple.createWatchView.nowLabel": "立即 - {selectInterval}",
"xpack.ml.newJob.simple.createWatchView.sendEmailLabel": "发送电子邮件",
"xpack.ml.newJob.simple.createWatchView.severityThresholdLabel": "严重性阈值",
"xpack.ml.newJob.simple.createWatchView.successLabel": "成功",
"xpack.ml.newJob.simple.createWatchView.timeRangeLabel": "时间范围",
"xpack.ml.newJob.simple.createWatchView.watchAlreadyExistsWarningMessage": "警告,监视 ml-{jobId} 已存在,点击“应用”将覆盖原始监视。",
"xpack.ml.newJob.simple.createWatchView.watchEmailAddressAriaLabel": "监视电子邮件地址",
"xpack.ml.newJob.simple.recognize.jobsCreationFailedTitle": "作业创建失败",
"xpack.ml.newJob.simple.watcher.email.anomalyScoreLabel": "异常分数",
"xpack.ml.newJob.simple.watcher.email.elasticStackMachineLearningAlertLabel": "Elastic Stack Machine Learning 告警",
"xpack.ml.newJob.simple.watcher.email.jobLabel": "作业",
"xpack.ml.newJob.simple.watcher.email.mlWatcherAlertSubjectTitle": "ML Watcher 告警",
"xpack.ml.newJob.simple.watcher.email.openInAnomalyExplorerLinkText": "单击此处在 Anomaly Explorer 中打开。",
"xpack.ml.newJob.simple.watcher.email.timeLabel": "时间",
"xpack.ml.newJob.simple.watcher.email.topInfluencersLabel": "排在前面的影响因素:",
"xpack.ml.newJob.simple.watcher.email.topRecordsLabel": "排在前面的记录:",
"xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error": "检索索引的开始和结束时间",
"xpack.ml.newJob.wizard.categorizationAnalyzerFlyout.closeButton": "关闭",
"xpack.ml.newJob.wizard.categorizationAnalyzerFlyout.saveButton": "保存",
@ -13941,7 +13918,6 @@
"xpack.ml.newJob.wizard.summaryStep.jobDetails.splitField.title": "分割字段",
"xpack.ml.newJob.wizard.summaryStep.jobDetails.summaryCountField.title": "汇总计数字段",
"xpack.ml.newJob.wizard.summaryStep.jobDetails.useDedicatedIndex.title": "使用专用索引",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.createWatch": "创建监视",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTime": "启动实时运行的作业",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeError": "启动作业时出错",
"xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeSuccess": "作业 {jobId} 已启动",