[ML] UI enhancements for Anomaly detection rule type (#97626)
* [ML] update labels * [ML] update job summary endpoint to return associated alert rules * [ML] add alert rule icon to the table * [ML] edit alert rules from ML UI * [ML] register navigation * [ML] support single job selection only * [ML] remove groups options from the job selection * [ML] deps on rule id to avoid re-rendering * [ML] fix i18n * [ML] add info message to the alert context * [ML] fix typo * [ML] register usage collection * [ML] fix telemetry
This commit is contained in:
parent
fa0c74fe30
commit
58d4334c71
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { AnomalyResultType } from './anomalies';
|
||||
import { ANOMALY_RESULT_TYPE } from '../constants/anomalies';
|
||||
import { AlertTypeParams } from '../../../alerting/common';
|
||||
import type { AlertTypeParams, Alert } from '../../../alerting/common';
|
||||
|
||||
export type PreviewResultsKeys = 'record_results' | 'bucket_results' | 'influencer_results';
|
||||
export type TopHitsResultsKeys = 'top_record_hits' | 'top_bucket_hits' | 'top_influencer_hits';
|
||||
|
@ -25,6 +25,7 @@ export interface AlertExecutionResult {
|
|||
bucketRange: { start: string; end: string };
|
||||
topRecords: RecordAnomalyAlertDoc[];
|
||||
topInfluencers?: InfluencerAnomalyAlertDoc[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PreviewResponse {
|
||||
|
@ -101,3 +102,9 @@ export type MlAnomalyDetectionAlertAdvancedSettings = Pick<
|
|||
MlAnomalyDetectionAlertParams,
|
||||
'lookbackInterval' | 'topNBuckets'
|
||||
>;
|
||||
|
||||
export type MlAnomalyDetectionAlertRule = Omit<Alert<MlAnomalyDetectionAlertParams>, 'apiKey'>;
|
||||
|
||||
export interface JobAlertingRuleStats {
|
||||
alerting_rules?: MlAnomalyDetectionAlertRule[];
|
||||
}
|
||||
|
|
|
@ -9,8 +9,9 @@ import { Datafeed } from './datafeed';
|
|||
import { DatafeedStats } from './datafeed_stats';
|
||||
import { Job } from './job';
|
||||
import { JobStats } from './job_stats';
|
||||
import type { JobAlertingRuleStats } from '../alerts';
|
||||
|
||||
export type JobWithStats = Job & JobStats;
|
||||
export type JobWithStats = Job & JobStats & JobAlertingRuleStats;
|
||||
export type DatafeedWithStats = Datafeed & DatafeedStats;
|
||||
|
||||
// in older implementations of the job config, the datafeed was placed inside the job
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { Moment } from 'moment';
|
||||
|
||||
import { CombinedJob, CombinedJobWithStats } from './combined_job';
|
||||
import { MlAnomalyDetectionAlertRule } from '../alerts';
|
||||
export { Datafeed } from './datafeed';
|
||||
export { DatafeedStats } from './datafeed_stats';
|
||||
|
||||
|
@ -34,6 +35,7 @@ export interface MlSummaryJob {
|
|||
latestTimestampSortValue?: number;
|
||||
earliestStartTimestampMs?: number;
|
||||
awaitingNodeAssignment: boolean;
|
||||
alertingRules?: MlAnomalyDetectionAlertRule[];
|
||||
}
|
||||
|
||||
export interface AuditMessage {
|
||||
|
|
|
@ -30,6 +30,8 @@ export const userMlCapabilities = {
|
|||
canGetAnnotations: false,
|
||||
canCreateAnnotation: false,
|
||||
canDeleteAnnotation: false,
|
||||
// Alerts
|
||||
canUseMlAlerts: false,
|
||||
};
|
||||
|
||||
export const adminMlCapabilities = {
|
||||
|
@ -59,6 +61,7 @@ export const adminMlCapabilities = {
|
|||
canStartStopDataFrameAnalytics: false,
|
||||
// Alerts
|
||||
canCreateMlAlerts: false,
|
||||
canUseMlAlerts: false,
|
||||
};
|
||||
|
||||
export type UserMlCapabilities = typeof userMlCapabilities;
|
||||
|
|
8
x-pack/plugins/ml/public/alerting/index.ts
Normal file
8
x-pack/plugins/ml/public/alerting/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { registerMlAlerts } from './register_ml_alerts';
|
|
@ -66,12 +66,6 @@ export const JobSelectorControl: FC<JobSelectorControlProps> = ({
|
|||
}),
|
||||
options: jobIdOptions.map((v) => ({ label: v })),
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.ml.jobSelector.groupOptionsLabel', {
|
||||
defaultMessage: 'Groups',
|
||||
}),
|
||||
options: groupIdOptions.map((v) => ({ label: v })),
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
// TODO add error handling
|
||||
|
@ -114,6 +108,7 @@ export const JobSelectorControl: FC<JobSelectorControlProps> = ({
|
|||
error={errors}
|
||||
>
|
||||
<EuiComboBox<string>
|
||||
singleSelection
|
||||
selectedOptions={selectedOptions}
|
||||
options={options}
|
||||
onChange={onSelectionChange}
|
||||
|
|
|
@ -6,24 +6,30 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
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';
|
||||
import { MlAnomalyDetectionAlertRule } from '../../common/types/alerts';
|
||||
|
||||
interface MlAnomalyAlertFlyoutProps {
|
||||
jobIds: JobId[];
|
||||
initialAlert?: MlAnomalyDetectionAlertRule;
|
||||
jobIds?: JobId[];
|
||||
onSave?: () => void;
|
||||
onCloseFlyout: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke alerting flyout from the ML plugin context.
|
||||
* @param initialAlert
|
||||
* @param jobIds
|
||||
* @param onCloseFlyout
|
||||
* @param onSave
|
||||
* @constructor
|
||||
*/
|
||||
export const MlAnomalyAlertFlyout: FC<MlAnomalyAlertFlyoutProps> = ({
|
||||
initialAlert,
|
||||
jobIds,
|
||||
onCloseFlyout,
|
||||
onSave,
|
||||
|
@ -32,35 +38,45 @@ export const MlAnomalyAlertFlyout: FC<MlAnomalyAlertFlyoutProps> = ({
|
|||
services: { triggersActionsUi },
|
||||
} = useMlKibana();
|
||||
|
||||
const AddAlertFlyout = useMemo(
|
||||
() =>
|
||||
triggersActionsUi &&
|
||||
triggersActionsUi.getAddAlertFlyout({
|
||||
consumer: PLUGIN_ID,
|
||||
onClose: () => {
|
||||
onCloseFlyout();
|
||||
},
|
||||
// Callback for successful save
|
||||
onSave: async () => {
|
||||
if (onSave) {
|
||||
onSave();
|
||||
}
|
||||
},
|
||||
canChangeTrigger: false,
|
||||
alertTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION,
|
||||
metadata: {},
|
||||
initialValues: {
|
||||
params: {
|
||||
jobSelection: {
|
||||
jobIds,
|
||||
},
|
||||
const AlertFlyout = useMemo(() => {
|
||||
if (!triggersActionsUi) return;
|
||||
|
||||
const commonProps = {
|
||||
onClose: () => {
|
||||
onCloseFlyout();
|
||||
},
|
||||
onSave: async () => {
|
||||
if (onSave) {
|
||||
onSave();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (initialAlert) {
|
||||
return triggersActionsUi.getEditAlertFlyout({
|
||||
...commonProps,
|
||||
initialAlert,
|
||||
});
|
||||
}
|
||||
|
||||
return triggersActionsUi.getAddAlertFlyout({
|
||||
...commonProps,
|
||||
consumer: PLUGIN_ID,
|
||||
canChangeTrigger: false,
|
||||
alertTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION,
|
||||
metadata: {},
|
||||
initialValues: {
|
||||
params: {
|
||||
jobSelection: {
|
||||
jobIds,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[triggersActionsUi]
|
||||
);
|
||||
},
|
||||
});
|
||||
// deps on id to avoid re-rendering on auto-refresh
|
||||
}, [triggersActionsUi, initialAlert?.id, jobIds]);
|
||||
|
||||
return <>{AddAlertFlyout}</>;
|
||||
return <>{AlertFlyout}</>;
|
||||
};
|
||||
|
||||
interface JobListMlAnomalyAlertFlyoutProps {
|
||||
|
@ -103,3 +119,26 @@ export const JobListMlAnomalyAlertFlyout: FC<JobListMlAnomalyAlertFlyoutProps> =
|
|||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
interface EditRuleFlyoutProps {
|
||||
initialAlert: MlAnomalyDetectionAlertRule;
|
||||
}
|
||||
|
||||
export const EditAlertRule: FC<EditRuleFlyoutProps> = ({ initialAlert }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" onClick={setIsVisible.bind(null, !isVisible)}>
|
||||
{initialAlert.name}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
{isVisible ? (
|
||||
<MlAnomalyAlertFlyout
|
||||
initialAlert={initialAlert}
|
||||
onCloseFlyout={setIsVisible.bind(null, false)}
|
||||
onSave={setIsVisible.bind(null, false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,13 +8,17 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { lazy } from 'react';
|
||||
import { ML_ALERT_TYPES } from '../../common/constants/alerts';
|
||||
import { MlAnomalyDetectionAlertParams } from '../../common/types/alerts';
|
||||
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
|
||||
|
||||
export async function registerMlAlerts(triggersActionsUi: TriggersAndActionsUIPublicPluginSetup) {
|
||||
// async import validators to reduce initial bundle size
|
||||
const { validateLookbackInterval, validateTopNBucket } = await import('./validators');
|
||||
import type { MlAnomalyDetectionAlertParams } from '../../common/types/alerts';
|
||||
import type { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
|
||||
import type { PluginSetupContract as AlertingSetup } from '../../../alerting/public';
|
||||
import { PLUGIN_ID } from '../../common/constants/app';
|
||||
import { createExplorerUrl } from '../ml_url_generator/anomaly_detection_urls_generator';
|
||||
import { validateLookbackInterval, validateTopNBucket } from './validators';
|
||||
|
||||
export function registerMlAlerts(
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup,
|
||||
alerting?: AlertingSetup
|
||||
) {
|
||||
triggersActionsUi.alertTypeRegistry.register({
|
||||
id: ML_ALERT_TYPES.ANOMALY_DETECTION,
|
||||
description: i18n.translate('xpack.ml.alertTypes.anomalyDetection.description', {
|
||||
|
@ -47,6 +51,20 @@ export async function registerMlAlerts(triggersActionsUi: TriggersAndActionsUIPu
|
|||
);
|
||||
}
|
||||
|
||||
// Since 7.13 we support single job selection only
|
||||
if (
|
||||
(Array.isArray(alertParams.jobSelection?.groupIds) &&
|
||||
alertParams.jobSelection?.groupIds.length > 0) ||
|
||||
(Array.isArray(alertParams.jobSelection?.jobIds) &&
|
||||
alertParams.jobSelection?.jobIds.length > 1)
|
||||
) {
|
||||
validationResult.errors.jobSelection.push(
|
||||
i18n.translate('xpack.ml.alertTypes.anomalyDetection.singleJobSelection.errorMessage', {
|
||||
defaultMessage: 'Only one job per rule is allowed',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (alertParams.severity === undefined) {
|
||||
validationResult.errors.severity.push(
|
||||
i18n.translate('xpack.ml.alertTypes.anomalyDetection.severity.errorMessage', {
|
||||
|
@ -96,7 +114,7 @@ export async function registerMlAlerts(triggersActionsUi: TriggersAndActionsUIPu
|
|||
- Time: \\{\\{context.timestampIso8601\\}\\}
|
||||
- Anomaly score: \\{\\{context.score\\}\\}
|
||||
|
||||
Alerts are raised based on real-time scores. Remember that scores may be adjusted over time as data continues to be analyzed.
|
||||
\\{\\{context.message\\}\\}
|
||||
|
||||
\\{\\{#context.topInfluencers.length\\}\\}
|
||||
Top influencers:
|
||||
|
@ -118,4 +136,22 @@ Alerts are raised based on real-time scores. Remember that scores may be adjuste
|
|||
}
|
||||
),
|
||||
});
|
||||
|
||||
if (alerting) {
|
||||
registerNavigation(alerting);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerNavigation(alerting: AlertingSetup) {
|
||||
alerting.registerNavigation(PLUGIN_ID, ML_ALERT_TYPES.ANOMALY_DETECTION, (alert) => {
|
||||
const alertParams = alert.params as MlAnomalyDetectionAlertParams;
|
||||
const jobIds = [
|
||||
...new Set([
|
||||
...(alertParams.jobSelection.jobIds ?? []),
|
||||
...(alertParams.jobSelection.groupIds ?? []),
|
||||
]),
|
||||
];
|
||||
|
||||
return createExplorerUrl('', { jobIds });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -63,10 +63,10 @@ export function actionsMenuContent(
|
|||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.jobsList.managementActions.createAlertLabel', {
|
||||
defaultMessage: 'Create alert',
|
||||
defaultMessage: 'Create alert rule',
|
||||
}),
|
||||
description: i18n.translate('xpack.ml.jobsList.managementActions.createAlertLabel', {
|
||||
defaultMessage: 'Create alert',
|
||||
defaultMessage: 'Create alert rule',
|
||||
}),
|
||||
icon: 'bell',
|
||||
enabled: (item) => item.deleting !== true,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { detectorToString } from '../../../../util/string_utils';
|
|||
import { formatValues, filterObjects } from './format_values';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { EditAlertRule } from '../../../../../alerting/ml_alerting_flyout';
|
||||
|
||||
export function extractJobDetails(job, basePath) {
|
||||
if (Object.keys(job).length === 0) {
|
||||
|
@ -74,6 +75,17 @@ export function extractJobDetails(job, basePath) {
|
|||
}
|
||||
}
|
||||
|
||||
const alertRules = {
|
||||
id: 'alertRules',
|
||||
title: i18n.translate('xpack.ml.jobsList.jobDetails.alertRulesTitle', {
|
||||
defaultMessage: 'Alert rules',
|
||||
}),
|
||||
position: 'right',
|
||||
items: (job.alerting_rules ?? []).map((v) => {
|
||||
return ['', <EditAlertRule initialAlert={v} />];
|
||||
}),
|
||||
};
|
||||
|
||||
const detectors = {
|
||||
id: 'detectors',
|
||||
title: i18n.translate('xpack.ml.jobsList.jobDetails.detectorsTitle', {
|
||||
|
@ -206,5 +218,6 @@ export function extractJobDetails(job, basePath) {
|
|||
modelSizeStats,
|
||||
jobTimingStats,
|
||||
datafeedTimingStats,
|
||||
alertRules,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ export class JobDetailsUI extends Component {
|
|||
modelSizeStats,
|
||||
jobTimingStats,
|
||||
datafeedTimingStats,
|
||||
alertRules,
|
||||
} = extractJobDetails(job, basePath);
|
||||
|
||||
const { showFullDetails, refreshJobList } = this.props;
|
||||
|
@ -83,7 +84,7 @@ export class JobDetailsUI extends Component {
|
|||
content: (
|
||||
<JobDetailsPane
|
||||
data-test-subj="mlJobDetails-job-settings"
|
||||
sections={[general, customUrl, node, calendars]}
|
||||
sections={[general, customUrl, node, calendars, alertRules]}
|
||||
/>
|
||||
),
|
||||
time: job.open_time,
|
||||
|
|
|
@ -18,7 +18,13 @@ import { JobIcon } from '../../../../components/job_message_icon';
|
|||
import { JobSpacesList } from '../../../../components/job_spaces_list';
|
||||
import { TIME_FORMAT } from '../../../../../../common/constants/time_format';
|
||||
|
||||
import { EuiBasicTable, EuiButtonIcon, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiScreenReaderOnly,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AnomalyDetectionJobIdLink } from './job_id_link';
|
||||
|
@ -161,7 +167,7 @@ export class JobsList extends Component {
|
|||
}),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '20%',
|
||||
width: '15%',
|
||||
scope: 'row',
|
||||
render: isManagementTable ? (id) => this.getJobIdLink(id) : undefined,
|
||||
},
|
||||
|
@ -172,13 +178,45 @@ export class JobsList extends Component {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.auditMessageColumn.screenReaderDescription"
|
||||
defaultMessage="This column display icons when there are errors or warnings for the job in the past 24 hours"
|
||||
defaultMessage="This column displays icons when there are errors or warnings for the job in the past 24 hours"
|
||||
/>
|
||||
</p>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
render: (item) => <JobIcon message={item} showTooltip={true} />,
|
||||
},
|
||||
{
|
||||
field: 'alertingRules',
|
||||
name: (
|
||||
<EuiScreenReaderOnly>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.alertingRules.screenReaderDescription"
|
||||
defaultMessage="This column displays icons when there are alert rules associated with a job"
|
||||
/>
|
||||
</p>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
width: '30px',
|
||||
render: (item) => {
|
||||
return Array.isArray(item) ? (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.alertingRules.tooltipContent"
|
||||
defaultMessage="Job has {rulesCount} associated alert {rulesCount, plural, one { rule} other { rules}}"
|
||||
values={{ rulesCount: item.length }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="bell" />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<span />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.jobsList.descriptionLabel', {
|
||||
defaultMessage: 'Description',
|
||||
|
|
|
@ -159,7 +159,7 @@ class MultiJobActionsMenuUI extends Component {
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.multiJobsActions.createAlertsLabel"
|
||||
defaultMessage="Create alert"
|
||||
defaultMessage="Create alert rule"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
|
|
|
@ -173,7 +173,7 @@ export class StartDatafeedModal extends Component {
|
|||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.startDatafeedModal.createAlertDescription"
|
||||
defaultMessage="Create alert after datafeed has started"
|
||||
defaultMessage="Create alert rule after datafeed has started"
|
||||
/>
|
||||
}
|
||||
checked={createAlert}
|
||||
|
|
|
@ -87,7 +87,7 @@ export const PostSaveOptions: FC<Props> = ({ jobRunner }) => {
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.summaryStep.postSaveOptions.createAlert"
|
||||
defaultMessage="Create alert"
|
||||
defaultMessage="Create alert rule"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -51,8 +51,8 @@ import {
|
|||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '../../triggers_actions_ui/public';
|
||||
import { registerMlAlerts } from './alerting/register_ml_alerts';
|
||||
import { FileDataVisualizerPluginStart } from '../../file_data_visualizer/public';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../alerting/public';
|
||||
|
||||
export interface MlStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -79,6 +79,7 @@ export interface MlSetupDependencies {
|
|||
share: SharePluginSetup;
|
||||
indexPatternManagement: IndexPatternManagementSetup;
|
||||
triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup;
|
||||
alerting?: AlertingSetup;
|
||||
}
|
||||
|
||||
export type MlCoreSetup = CoreSetup<MlStartDependencies, MlPluginStart>;
|
||||
|
@ -132,10 +133,6 @@ 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();
|
||||
|
@ -166,6 +163,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
|||
registerManagementSection,
|
||||
registerMlUiActions,
|
||||
registerSearchLinks,
|
||||
registerMlAlerts,
|
||||
} = await import('./register_helper');
|
||||
|
||||
const mlEnabled = isMlEnabled(license);
|
||||
|
@ -181,6 +179,11 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
|||
}
|
||||
registerEmbeddables(pluginsSetup.embeddable, core);
|
||||
registerMlUiActions(pluginsSetup.uiActions, core);
|
||||
|
||||
const canUseMlAlerts = capabilities.ml?.canUseMlAlerts;
|
||||
if (pluginsSetup.triggersActionsUi && canUseMlAlerts) {
|
||||
registerMlAlerts(pluginsSetup.triggersActionsUi, pluginsSetup.alerting);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,3 +9,4 @@ export { registerEmbeddables } from '../embeddables';
|
|||
export { registerManagementSection } from '../application/management';
|
||||
export { registerMlUiActions } from '../ui_actions';
|
||||
export { registerSearchLinks } from './register_search_links';
|
||||
export { registerMlAlerts } from '../alerting';
|
||||
|
|
|
@ -289,6 +289,8 @@ export function alertingServiceProvider(mlClient: MlClient, datafeedsService: Da
|
|||
return {
|
||||
count: aggTypeResults.doc_count,
|
||||
key: v.key,
|
||||
message:
|
||||
'Alerts are raised based on real-time scores. Remember that scores may be adjusted over time as data continues to be analyzed.',
|
||||
alertInstanceKey,
|
||||
jobIds: [...new Set(requestedAnomalies.map((h) => h._source.job_id))],
|
||||
isInterim: requestedAnomalies.some((h) => h._source.is_interim),
|
||||
|
|
|
@ -78,6 +78,12 @@ export function registerAnomalyDetectionAlertType({
|
|||
defaultMessage: 'List of job IDs that triggered the alert instance',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
description: i18n.translate('xpack.ml.alertContext.messageDescription', {
|
||||
defaultMessage: 'Alert info message',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'isInterim',
|
||||
description: i18n.translate('xpack.ml.alertContext.isInterimDescription', {
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('check_capabilities', () => {
|
|||
);
|
||||
const { capabilities } = await getCapabilities();
|
||||
const count = Object.keys(capabilities).length;
|
||||
expect(count).toBe(29);
|
||||
expect(count).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -82,6 +82,7 @@ describe('check_capabilities', () => {
|
|||
expect(capabilities.canGetAnnotations).toBe(true);
|
||||
expect(capabilities.canCreateAnnotation).toBe(true);
|
||||
expect(capabilities.canDeleteAnnotation).toBe(true);
|
||||
expect(capabilities.canUseMlAlerts).toBe(true);
|
||||
|
||||
expect(capabilities.canCreateJob).toBe(false);
|
||||
expect(capabilities.canDeleteJob).toBe(false);
|
||||
|
|
|
@ -20,12 +20,17 @@ import { jobSavedObjectServiceFactory, JobSavedObjectService } from '../saved_ob
|
|||
import { MlLicense } from '../../common/license';
|
||||
|
||||
import { MlClient, getMlClient } from '../lib/ml_client';
|
||||
import type { AlertingApiRequestHandlerContext } from '../../../alerting/server';
|
||||
|
||||
type MLRequestHandlerContext = RequestHandlerContext & {
|
||||
alerting?: AlertingApiRequestHandlerContext;
|
||||
};
|
||||
|
||||
type Handler = (handlerParams: {
|
||||
client: IScopedClusterClient;
|
||||
request: KibanaRequest<any, any, any, any>;
|
||||
response: KibanaResponseFactory;
|
||||
context: RequestHandlerContext;
|
||||
context: MLRequestHandlerContext;
|
||||
jobSavedObjectService: JobSavedObjectService;
|
||||
mlClient: MlClient;
|
||||
}) => ReturnType<RequestHandler>;
|
||||
|
@ -66,7 +71,7 @@ export class RouteGuard {
|
|||
|
||||
private _guard(check: () => boolean, handler: Handler) {
|
||||
return (
|
||||
context: RequestHandlerContext,
|
||||
context: MLRequestHandlerContext,
|
||||
request: KibanaRequest<any, any, any, any>,
|
||||
response: KibanaResponseFactory
|
||||
) => {
|
||||
|
|
|
@ -13,11 +13,16 @@ import { newJobCapsProvider } from './new_job_caps';
|
|||
import { newJobChartsProvider, topCategoriesProvider } from './new_job';
|
||||
import { modelSnapshotProvider } from './model_snapshots';
|
||||
import type { MlClient } from '../../lib/ml_client';
|
||||
import type { AlertsClient } from '../../../../alerting/server';
|
||||
|
||||
export function jobServiceProvider(client: IScopedClusterClient, mlClient: MlClient) {
|
||||
export function jobServiceProvider(
|
||||
client: IScopedClusterClient,
|
||||
mlClient: MlClient,
|
||||
alertsClient?: AlertsClient
|
||||
) {
|
||||
return {
|
||||
...datafeedsProvider(client, mlClient),
|
||||
...jobsProvider(client, mlClient),
|
||||
...jobsProvider(client, mlClient, alertsClient),
|
||||
...groupsProvider(mlClient),
|
||||
...newJobCapsProvider(client),
|
||||
...newJobChartsProvider(client),
|
||||
|
|
|
@ -40,6 +40,9 @@ import {
|
|||
import { groupsProvider } from './groups';
|
||||
import type { MlClient } from '../../lib/ml_client';
|
||||
import { isPopulatedObject } from '../../../common/util/object_utils';
|
||||
import type { AlertsClient } from '../../../../alerting/server';
|
||||
import { ML_ALERT_TYPES } from '../../../common/constants/alerts';
|
||||
import { MlAnomalyDetectionAlertParams } from '../../routes/schemas/alerting_schema';
|
||||
|
||||
interface Results {
|
||||
[id: string]: {
|
||||
|
@ -48,7 +51,11 @@ interface Results {
|
|||
};
|
||||
}
|
||||
|
||||
export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
|
||||
export function jobsProvider(
|
||||
client: IScopedClusterClient,
|
||||
mlClient: MlClient,
|
||||
alertsClient?: AlertsClient
|
||||
) {
|
||||
const { asInternalUser } = client;
|
||||
|
||||
const { forceDeleteDatafeed, getDatafeedIdsByJobId, getDatafeedByJobId } = datafeedsProvider(
|
||||
|
@ -212,6 +219,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
|
|||
nodeName: job.node ? job.node.name : undefined,
|
||||
deleting: job.deleting || undefined,
|
||||
awaitingNodeAssignment: isJobAwaitingNodeAssignment(job),
|
||||
alertingRules: job.alerting_rules,
|
||||
};
|
||||
if (jobIds.find((j) => j === tempJob.id)) {
|
||||
tempJob.fullJob = job;
|
||||
|
@ -416,6 +424,39 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
|
|||
|
||||
jobs.push(tempJob);
|
||||
});
|
||||
|
||||
if (alertsClient) {
|
||||
const mlAlertingRules = await alertsClient.find<MlAnomalyDetectionAlertParams>({
|
||||
options: {
|
||||
filter: `alert.attributes.alertTypeId:${ML_ALERT_TYPES.ANOMALY_DETECTION}`,
|
||||
perPage: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
mlAlertingRules.data.forEach((curr) => {
|
||||
const {
|
||||
params: {
|
||||
jobSelection: { jobIds: ruleJobIds, groupIds: ruleGroupIds },
|
||||
},
|
||||
} = curr;
|
||||
|
||||
jobs.forEach((j) => {
|
||||
const isIncluded =
|
||||
(Array.isArray(ruleJobIds) && ruleJobIds.includes(j.job_id)) ||
|
||||
(Array.isArray(ruleGroupIds) &&
|
||||
Array.isArray(j.groups) &&
|
||||
j.groups.some((g) => ruleGroupIds.includes(g)));
|
||||
|
||||
if (isIncluded) {
|
||||
if (Array.isArray(j.alerting_rules)) {
|
||||
j.alerting_rules.push(curr);
|
||||
} else {
|
||||
j.alerting_rules = [curr];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
|
|
|
@ -227,9 +227,13 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) {
|
|||
tags: ['access:ml:canGetJobs'],
|
||||
},
|
||||
},
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => {
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => {
|
||||
try {
|
||||
const { jobsSummary } = jobServiceProvider(client, mlClient);
|
||||
const { jobsSummary } = jobServiceProvider(
|
||||
client,
|
||||
mlClient,
|
||||
context.alerting?.getAlertsClient()
|
||||
);
|
||||
const { jobIds } = request.body;
|
||||
const resp = await jobsSummary(jobIds);
|
||||
|
||||
|
@ -328,9 +332,13 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) {
|
|||
tags: ['access:ml:canGetJobs'],
|
||||
},
|
||||
},
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => {
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => {
|
||||
try {
|
||||
const { createFullJobsList } = jobServiceProvider(client, mlClient);
|
||||
const { createFullJobsList } = jobServiceProvider(
|
||||
client,
|
||||
mlClient,
|
||||
context.alerting?.getAlertsClient()
|
||||
);
|
||||
const { jobIds } = request.body;
|
||||
const resp = await createFullJobsList(jobIds);
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = {
|
|||
canCreateDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
canCreateMlAlerts: false,
|
||||
canUseMlAlerts: false,
|
||||
},
|
||||
isPlatinumOrTrialLicense: false,
|
||||
mlFeatureEnabledInSpace: false,
|
||||
|
|
|
@ -13830,7 +13830,6 @@
|
|||
"xpack.ml.jobSelector.filterBar.invalidSearchErrorMessage": "無効な検索:{errorMessage}",
|
||||
"xpack.ml.jobSelector.flyoutTitle": "ジョブの選択",
|
||||
"xpack.ml.jobSelector.formControlLabel": "ジョブまたはグループを選択",
|
||||
"xpack.ml.jobSelector.groupOptionsLabel": "グループ",
|
||||
"xpack.ml.jobSelector.groupsTab": "グループ",
|
||||
"xpack.ml.jobSelector.hideBarBadges": "非表示",
|
||||
"xpack.ml.jobSelector.hideFlyoutBadges": "非表示",
|
||||
|
|
|
@ -14017,7 +14017,6 @@
|
|||
"xpack.ml.jobSelector.filterBar.jobGroupTitle": "({jobsCount, plural, other {# 个作业}})",
|
||||
"xpack.ml.jobSelector.flyoutTitle": "作业选择",
|
||||
"xpack.ml.jobSelector.formControlLabel": "选择作业或组",
|
||||
"xpack.ml.jobSelector.groupOptionsLabel": "组",
|
||||
"xpack.ml.jobSelector.groupsTab": "组",
|
||||
"xpack.ml.jobSelector.hideBarBadges": "隐藏",
|
||||
"xpack.ml.jobSelector.hideFlyoutBadges": "隐藏",
|
||||
|
|
|
@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
it('should have the right number of capabilities', async () => {
|
||||
const { capabilities } = await runRequest(USER.ML_POWERUSER);
|
||||
expect(Object.keys(capabilities).length).to.eql(29);
|
||||
expect(Object.keys(capabilities).length).to.eql(30);
|
||||
});
|
||||
|
||||
it('should get viewer capabilities', async () => {
|
||||
|
@ -72,6 +72,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
canDeleteDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
canCreateMlAlerts: false,
|
||||
canUseMlAlerts: true,
|
||||
canAccessML: true,
|
||||
canGetJobs: true,
|
||||
canGetDatafeeds: true,
|
||||
|
@ -108,6 +109,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
canDeleteDataFrameAnalytics: true,
|
||||
canStartStopDataFrameAnalytics: true,
|
||||
canCreateMlAlerts: true,
|
||||
canUseMlAlerts: true,
|
||||
canAccessML: true,
|
||||
canGetJobs: true,
|
||||
canGetDatafeeds: true,
|
||||
|
|
|
@ -71,11 +71,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
it('should have the right number of capabilities - space with ML', async () => {
|
||||
const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl);
|
||||
expect(Object.keys(capabilities).length).to.eql(29);
|
||||
expect(Object.keys(capabilities).length).to.eql(30);
|
||||
});
|
||||
it('should have the right number of capabilities - space without ML', async () => {
|
||||
const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl);
|
||||
expect(Object.keys(capabilities).length).to.eql(29);
|
||||
expect(Object.keys(capabilities).length).to.eql(30);
|
||||
});
|
||||
|
||||
it('should get viewer capabilities - space with ML', async () => {
|
||||
|
@ -101,6 +101,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
canDeleteDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
canCreateMlAlerts: false,
|
||||
canUseMlAlerts: true,
|
||||
canAccessML: true,
|
||||
canGetJobs: true,
|
||||
canGetDatafeeds: true,
|
||||
|
@ -136,6 +137,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
canDeleteDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
canCreateMlAlerts: false,
|
||||
canUseMlAlerts: false,
|
||||
canAccessML: false,
|
||||
canGetJobs: false,
|
||||
canGetDatafeeds: false,
|
||||
|
@ -171,6 +173,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
canDeleteDataFrameAnalytics: true,
|
||||
canStartStopDataFrameAnalytics: true,
|
||||
canCreateMlAlerts: true,
|
||||
canUseMlAlerts: true,
|
||||
canAccessML: true,
|
||||
canGetJobs: true,
|
||||
canGetDatafeeds: true,
|
||||
|
@ -206,6 +209,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
canDeleteDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
canCreateMlAlerts: false,
|
||||
canUseMlAlerts: false,
|
||||
canAccessML: false,
|
||||
canGetJobs: false,
|
||||
canGetDatafeeds: false,
|
||||
|
|
Loading…
Reference in a new issue