mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Anomaly Detection alert initialisation from the ML app (#91283)
This commit is contained in:
parent
0a5e054fdc
commit
a8e1e47de6
32 changed files with 289 additions and 1159 deletions
|
@ -89,4 +89,5 @@ export type MlAnomalyDetectionAlertParams = {
|
|||
};
|
||||
severity: number;
|
||||
resultType: AnomalyResultType;
|
||||
includeInterim: boolean;
|
||||
} & AlertTypeParams;
|
||||
|
|
|
@ -57,6 +57,8 @@ export const adminMlCapabilities = {
|
|||
canCreateDataFrameAnalytics: false,
|
||||
canDeleteDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
// Alerts
|
||||
canCreateMlAlerts: false,
|
||||
};
|
||||
|
||||
export type UserMlCapabilities = typeof userMlCapabilities;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
|
|
105
x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx
Normal file
105
x-pack/plugins/ml/public/alerting/ml_alerting_flyout.tsx
Normal 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;
|
||||
};
|
|
@ -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" />
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -81,6 +81,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
storage: localStorage,
|
||||
embeddable: deps.embeddable,
|
||||
maps: deps.maps,
|
||||
triggersActionsUi: deps.triggersActionsUi,
|
||||
...coreStart,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
|
@ -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 />;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 />
|
|
@ -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';
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -309,6 +309,13 @@ export function alertingServiceProvider(mlClient: MlClient) {
|
|||
result_type: Object.values(ANOMALY_RESULT_TYPE),
|
||||
},
|
||||
},
|
||||
...(params.includeInterim
|
||||
? []
|
||||
: [
|
||||
{
|
||||
term: { is_interim: false },
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -37,6 +37,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = {
|
|||
canDeleteDataFrameAnalytics: false,
|
||||
canCreateDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
canCreateMlAlerts: false,
|
||||
},
|
||||
isPlatinumOrTrialLicense: false,
|
||||
mlFeatureEnabledInSpace: false,
|
||||
|
|
|
@ -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} が開始しました",
|
||||
|
|
|
@ -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} 已启动",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue