[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:
Dima Arnautov 2021-04-21 20:21:07 +02:00 committed by GitHub
parent fa0c74fe30
commit 58d4334c71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 291 additions and 71 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { registerMlAlerts } from './register_ml_alerts';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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',

View file

@ -159,7 +159,7 @@ class MultiJobActionsMenuUI extends Component {
>
<FormattedMessage
id="xpack.ml.jobsList.multiJobsActions.createAlertsLabel"
defaultMessage="Create alert"
defaultMessage="Create alert rule"
/>
</EuiContextMenuItem>
);

View file

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

View file

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

View file

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

View file

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

View file

@ -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),

View file

@ -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', {

View file

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

View file

@ -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
) => {

View file

@ -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),

View file

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

View file

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

View file

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

View file

@ -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": "非表示",

View file

@ -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": "隐藏",

View file

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

View file

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