diff --git a/x-pack/plugins/ml/common/types/alerts.ts b/x-pack/plugins/ml/common/types/alerts.ts index d19385a175efd..51d06b9906230 100644 --- a/x-pack/plugins/ml/common/types/alerts.ts +++ b/x-pack/plugins/ml/common/types/alerts.ts @@ -89,4 +89,5 @@ export type MlAnomalyDetectionAlertParams = { }; severity: number; resultType: AnomalyResultType; + includeInterim: boolean; } & AlertTypeParams; diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index cccf87f0a7950..61a5013642cd7 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -57,6 +57,8 @@ export const adminMlCapabilities = { canCreateDataFrameAnalytics: false, canDeleteDataFrameAnalytics: false, canStartStopDataFrameAnalytics: false, + // Alerts + canCreateMlAlerts: false, }; export type UserMlCapabilities = typeof userMlCapabilities; diff --git a/x-pack/plugins/ml/public/alerting/interim_results_control.tsx b/x-pack/plugins/ml/public/alerting/interim_results_control.tsx new file mode 100644 index 0000000000000..fa930d9a0ea0f --- /dev/null +++ b/x-pack/plugins/ml/public/alerting/interim_results_control.tsx @@ -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 = React.memo( + ({ value, onChange }) => { + return ( + + + } + checked={value} + onChange={onChange.bind(null, !value)} + /> + + ); + } +); diff --git a/x-pack/plugins/ml/public/alerting/job_selector.tsx b/x-pack/plugins/ml/public/alerting/job_selector.tsx index 969ed5af79107..60bb7517406b8 100644 --- a/x-pack/plugins/ml/public/alerting/job_selector.tsx +++ b/x-pack/plugins/ml/public/alerting/job_selector.tsx @@ -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 = ({ jobSelection, - onSelectionChange, + onChange, adJobsApiService, errors, }) => { @@ -70,7 +70,7 @@ export const JobSelectorControl: FC = ({ } }, [adJobsApiService]); - const onChange: EuiComboBoxProps['onChange'] = useCallback( + const onSelectionChange: EuiComboBoxProps['onChange'] = useCallback( (selectedOptions) => { const selectedJobIds: JobId[] = []; const selectedGroupIds: string[] = []; @@ -81,7 +81,7 @@ export const JobSelectorControl: FC = ({ selectedGroupIds.push(label); } }); - onSelectionChange({ + onChange({ ...(selectedJobIds.length > 0 ? { jobIds: selectedJobIds } : {}), ...(selectedGroupIds.length > 0 ? { groupIds: selectedGroupIds } : {}), }); @@ -114,7 +114,7 @@ export const JobSelectorControl: FC = ({ selectedOptions={selectedOptions} options={options} - onChange={onChange} + onChange={onSelectionChange} fullWidth data-test-subj={'mlAnomalyAlertJobSelection'} isInvalid={!!errors?.length} diff --git a/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx b/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx new file mode 100644 index 0000000000000..ba573fe42f5f2 --- /dev/null +++ b/x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx @@ -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 = ({ + 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 = ({ + setShowFunction, + unsetShowFunction, +}) => { + const [isVisible, setIsVisible] = useState(false); + const [jobIds, setJobIds] = useState(); + + const showFlyoutCallback = useCallback((jobIdsUpdate: JobId[]) => { + setJobIds(jobIdsUpdate); + setIsVisible(true); + }, []); + + useEffect(() => { + setShowFunction(showFlyoutCallback); + return () => { + unsetShowFunction(); + }; + }, []); + + return isVisible && jobIds ? ( + setIsVisible(false)} + onSave={() => { + setIsVisible(false); + }} + /> + ) : null; +}; diff --git a/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx b/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx index 5991a603890d7..3dd023a6187dd 100644 --- a/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx +++ b/x-pack/plugins/ml/public/alerting/ml_anomaly_alert_trigger.tsx @@ -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) => void; errors: Record; } const MlAnomalyAlertTrigger: FC = ({ alertParams, setAlertParams, + setAlertProperty, errors, }) => { const { @@ -49,21 +53,26 @@ const MlAnomalyAlertTrigger: FC = ({ [] ); - 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 ( = ({ onChange={useCallback(onAlertParamChange('severity'), [])} /> + + + diff --git a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts index 7f55eba9cbdc2..1d7bd06989bd9 100644 --- a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts +++ b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts @@ -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.', diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 0199e13e93d8c..3df67bc16ab05 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -81,6 +81,7 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { storage: localStorage, embeddable: deps.embeddable, maps: deps.maps, + triggersActionsUi: deps.triggersActionsUi, ...coreStart, }; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 1dd30d5d99335..99d4b77547d9d 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -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 & { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js deleted file mode 100644 index 49dc06888161f..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js +++ /dev/null @@ -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: ( - - - - - {i18n.translate('xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel', { - defaultMessage: 'Edit watch', - })} - - - - - ), - }; -} - -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 = ( - - - -

- -

-
-
- - - - - - - - - - - - - - - - - -
- ); - } - return
{flyout}
; - } -} -CreateWatchFlyoutUI.propTypes = { - setShowFunction: PropTypes.func.isRequired, - unsetShowFunction: PropTypes.func.isRequired, - flyoutHidden: PropTypes.func, -}; - -export const CreateWatchFlyout = withKibana(CreateWatchFlyoutUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js deleted file mode 100644 index cd81355b3f97e..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js +++ /dev/null @@ -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(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js deleted file mode 100644 index 2997d56b68f06..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js +++ /dev/null @@ -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 ( -
-
-
-
- -
- - ), - }} - /> -
- -
-
- -
-
- -
-
-
- {this.state.emailEnabled && ( -
- - } - checked={this.state.includeEmail} - onChange={this.onIncludeEmailChanged} - /> - {this.state.includeEmail && ( -
- -
- )} -
- )} - {this.state.watchAlreadyExists && ( - - } - /> - )} -
- ); - } else if (status === STATUS.SAVED) { - return ( -
- -
- ); - } else { - return
; - } - } -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html deleted file mode 100644 index 713a68ba0c036..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - <%= elasticStackMachineLearningAlertLabel %> - -
-
- - - <%= jobLabel %> - : {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}} -
- - - <%= timeLabel %> - : {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_iso8601.0}} -
- - - <%= anomalyScoreLabel %> - : {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.score.0}} -
-
- - - <%= openInAnomalyExplorerLinkText %> - -
-
- - <%= influencersSection %> - - - <%= topRecordsLabel %> - -
- {{#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}}] -
- {{/ctx.payload.aggregations.record_results.top_record_hits.hits.hits}} - - - diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html deleted file mode 100644 index ab22ef672e97b..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html +++ /dev/null @@ -1,9 +0,0 @@ - - <%= topInfluencersLabel %> - -
- {{#ctx.payload.aggregations.influencer_results.top_influencer_hits.hits.hits}} - {{_source.influencer_field_name}} = {{_source.influencer_field_value}} [{{fields.score.0}}] -
- {{/ctx.payload.aggregations.influencer_results.top_influencer_hits.hits.hits}} -
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js deleted file mode 100644 index 0658867183280..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js +++ /dev/null @@ -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'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/select_severity.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/select_severity.tsx deleted file mode 100644 index 347e25816672b..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/select_severity.tsx +++ /dev/null @@ -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: ( - - - {display} - - - ), - dropdownDisplay: ( - - - {display} - - - -

- -

-
-
- ), - })); - -interface Props { - onChange: (sev: TableSeverity) => void; -} - -export const SelectSeverity: FC = ({ onChange }) => { - const [severity, setSeverity] = useState(DEFAULT_WATCH_SEVERITY); - - const onSeverityChange = (valueDisplay: string) => { - const option = optionValueToThreshold(optionsMap[valueDisplay]); - setSeverity(option); - onChange(option); - }; - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js deleted file mode 100644 index 2fcde2184bf06..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js +++ /dev/null @@ -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; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 8f955e771327e..471295938acde 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -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', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 261c58bebaaa8..4674342990df4 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -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, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 352bd839ba1f4..ac7224b3f3164 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -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)} /> this.refreshJobSummaryList(true)} /> -
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index 5760fbeb38642..e1314eb718836 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -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( + { + this.props.showCreateAlertFlyout(this.props.jobs.map(({ id }) => id)); + this.closePopover(); + }} + data-test-subj="mlADJobListMultiSelectCreateAlertActionButton" + > + + + ); + } + return ( )} @@ -67,4 +68,5 @@ MultiJobActions.propTypes = { showStartDatafeedModal: PropTypes.func.isRequired, showDeleteJobModal: PropTypes.func.isRequired, refreshJobs: PropTypes.func.isRequired, + showCreateAlertFlyout: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index 3ac6455bd745f..5f5759e49208c 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -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 {
} - checked={createWatch} - onChange={this.setCreateWatch} + checked={createAlert} + onChange={this.setCreateAlert} />
)} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index a39ffd171d1ca..6cefc239905c7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -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 = ({ 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 = ({ jobRunner }) => { /> + setWatchFlyoutVisible(true)} - data-test-subj="mlJobWizardButtonCreateWatch" + onClick={setAlertFlyoutVisible.bind(null, true)} + data-test-subj="mlJobWizardButtonCreateAlert" > - {datafeedState === DATAFEED_STATE.STARTED && watchFlyoutVisible && ( - )} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index b4eb5a6d702b7..212d6fe13a6b4 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -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; @@ -129,6 +129,10 @@ export class MlPlugin implements Plugin { 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 { http: core.http, i18n: core.i18n, }); - registerMlAlerts(deps.triggersActionsUi.alertTypeRegistry); + return { urlGenerator: this.urlGenerator, }; diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index 3b83e6d005077..5ef883cc50fbb 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -309,6 +309,13 @@ export function alertingServiceProvider(mlClient: MlClient) { result_type: Object.values(ANOMALY_RESULT_TYPE), }, }, + ...(params.includeInterim + ? [] + : [ + { + term: { is_interim: false }, + }, + ]), ], }, }, diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 8bfa825baacd9..49a63d2796969 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -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 () => { diff --git a/x-pack/plugins/ml/server/routes/alerting.ts b/x-pack/plugins/ml/server/routes/alerting.ts index b7a1be2434e8b..7b7f3a7db9723 100644 --- a/x-pack/plugins/ml/server/routes/alerting.ts +++ b/x-pack/plugins/ml/server/routes/alerting.ts @@ -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( { diff --git a/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts b/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts index 636185808f9a5..9e13b7ed81a15 100644 --- a/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts @@ -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({ diff --git a/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts b/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts index e966e3fb714ad..54c2beaa06b09 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts @@ -37,6 +37,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = { canDeleteDataFrameAnalytics: false, canCreateDataFrameAnalytics: false, canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, }, isPlatinumOrTrialLicense: false, mlFeatureEnabledInSpace: false, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4d94539a514d1..6cf9b73480316 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13292,12 +13292,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": "削除されました", @@ -13447,7 +13441,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": "検索終了時刻", @@ -13661,23 +13654,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": "保存", @@ -13908,7 +13885,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} が開始しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a35d4c67dde00..9b4dd59f38040 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13324,12 +13324,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": "已删除", @@ -13479,7 +13473,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": "搜索结束时间", @@ -13698,23 +13691,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": "保存", @@ -13945,7 +13922,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} 已启动",