[ML] Create categorization job from pattern analysis (#170567)

Adds the ability to quickly create a categorisation anomaly detection
job from the pattern analysis flyout.
Adds a new `created_by` ID `categorization-wizard-from-pattern-analysis`
which can be picked up by telemetry.

Creates a new package for sharing our AIOPs ui actions IDs. I think we
should move the pattern analysis ID to this package too, but that can be
done in a separate PR.


51349f93-f072-4983-85f0-98741902fb5a



6e618581-8916-4e63-930f-945c96c25e6c

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
James Gowdy 2023-11-21 15:52:12 +00:00 committed by GitHub
parent 39af788067
commit 5e3b124ae0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1194 additions and 145 deletions

1
.github/CODEOWNERS vendored
View file

@ -536,6 +536,7 @@ x-pack/packages/ml/route_utils @elastic/ml-ui
x-pack/packages/ml/runtime_field_utils @elastic/ml-ui
x-pack/packages/ml/string_hash @elastic/ml-ui
x-pack/packages/ml/trained_models_utils @elastic/ml-ui
x-pack/packages/ml/ui_actions @elastic/ml-ui
x-pack/packages/ml/url_state @elastic/ml-ui
packages/kbn-monaco @elastic/appex-sharedux
x-pack/plugins/monitoring_collection @elastic/obs-ux-infra_services-team

View file

@ -555,6 +555,7 @@
"@kbn/ml-runtime-field-utils": "link:x-pack/packages/ml/runtime_field_utils",
"@kbn/ml-string-hash": "link:x-pack/packages/ml/string_hash",
"@kbn/ml-trained-models-utils": "link:x-pack/packages/ml/trained_models_utils",
"@kbn/ml-ui-actions": "link:x-pack/packages/ml/ui_actions",
"@kbn/ml-url-state": "link:x-pack/packages/ml/url_state",
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/monitoring-collection-plugin": "link:x-pack/plugins/monitoring_collection",

View file

@ -1066,6 +1066,8 @@
"@kbn/ml-string-hash/*": ["x-pack/packages/ml/string_hash/*"],
"@kbn/ml-trained-models-utils": ["x-pack/packages/ml/trained_models_utils"],
"@kbn/ml-trained-models-utils/*": ["x-pack/packages/ml/trained_models_utils/*"],
"@kbn/ml-ui-actions": ["x-pack/packages/ml/ui_actions"],
"@kbn/ml-ui-actions/*": ["x-pack/packages/ml/ui_actions/*"],
"@kbn/ml-url-state": ["x-pack/packages/ml/url_state"],
"@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"],
"@kbn/monaco": ["packages/kbn-monaco"],

View file

@ -7,7 +7,7 @@
import React, { FC, useCallback, useMemo, useState } from 'react';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import {
EuiButton,

View file

@ -8,7 +8,7 @@
import moment from 'moment';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import dateMath from '@kbn/datemath';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { ToastsStart, HttpStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public';

View file

@ -6,7 +6,7 @@
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { HttpStart } from '@kbn/core/public';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { cloneDeep } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';

View file

@ -0,0 +1,3 @@
# @kbn/ml-ui-actions
Empty package generated by @kbn/generate

View file

@ -0,0 +1,12 @@
/*
* 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 {
CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION,
CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
type CreateCategorizationADJobContext,
} from './src/ui_actions';

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/ml/ui_actions'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-ui-actions",
"owner": "@elastic/ml-ui"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/ml-ui-actions",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"
}

View file

@ -0,0 +1,22 @@
/*
* 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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import type { TimeRange } from '@kbn/es-query';
export interface CreateCategorizationADJobContext {
field: DataViewField;
dataView: DataView;
query: QueryDslQueryContainer;
timeRange: TimeRange;
}
export const CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION = 'createMLADCategorizationJobAction';
export const CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER =
'CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER';

View file

@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/data-views-plugin",
"@kbn/es-query",
]
}

View file

@ -0,0 +1,76 @@
/*
* 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 moment from 'moment';
import { EuiButtonEmpty } from '@elastic/eui';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
type CreateCategorizationADJobContext,
} from '@kbn/ml-ui-actions';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
interface Props {
dataView: DataView;
field: DataViewField;
query: QueryDslQueryContainer;
earliest: number | undefined;
latest: number | undefined;
}
export const CreateCategorizationJobButton: FC<Props> = ({
dataView,
field,
query,
earliest,
latest,
}) => {
const {
uiActions,
application: { capabilities },
} = useAiopsAppContext();
const createADJob = () => {
if (uiActions === undefined) {
return;
}
const triggerOptions: CreateCategorizationADJobContext = {
dataView,
field,
query,
timeRange: { from: moment(earliest).toISOString(), to: moment(latest).toISOString() },
};
uiActions.getTrigger(CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER).exec(triggerOptions);
};
if (uiActions === undefined || capabilities.ml.canCreateJob === false) {
return null;
}
return (
<>
<EuiButtonEmpty
data-test-subj="aiopsLogCategorizationFlyoutAdJobButton"
onClick={createADJob}
flush="left"
iconSide="left"
iconType={'machineLearningApp'}
>
<FormattedMessage
id="xpack.aiops.categorizeFlyout.findAnomalies"
defaultMessage="Find anomalies in patterns"
/>
</EuiButtonEmpty>
</>
);
};

View file

@ -44,6 +44,7 @@ import { TechnicalPreviewBadge } from './technical_preview_badge';
import { LoadingCategorization } from './loading_categorization';
import { useValidateFieldRequest } from './use_validate_category_field';
import { FieldValidationCallout } from './category_validation_callout';
import { CreateCategorizationJobButton } from './create_categorization_job';
export interface LogCategorizationPageProps {
dataView: DataView;
@ -261,17 +262,21 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj="mlJobSelectorFlyoutBody">
<CreateCategorizationJobButton
dataView={dataView}
field={selectedField}
query={searchQuery}
earliest={earliest}
latest={latest}
/>
<FieldValidationCallout validationResults={fieldValidationResult} />
{loading === true ? <LoadingCategorization onClose={onClose} /> : null}
<InformationText
loading={loading}
categoriesLength={data?.categories?.length ?? null}
eventRateLength={eventRate.length}
fieldSelected={selectedField !== null}
/>
{loading === false && data !== null && data.categories.length > 0 ? (
<CategoryTable
categories={data.categories}

View file

@ -19,7 +19,7 @@ import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import type { AiopsPluginStartDeps } from '../../types';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext, type AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { LogCategorizationFlyout } from './log_categorization_for_flyout';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
@ -41,7 +41,7 @@ export async function showCategorizeFlyout(
resolve();
};
const appDependencies = {
const appDependencies: AiopsAppDependencies = {
notifications,
uiSettings,
http,

View file

@ -33,6 +33,7 @@ import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { CasesUiStart } from '@kbn/cases-plugin/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
/**
* AIOps App Dependencies to be provided via React context.
@ -97,6 +98,10 @@ export interface AiopsAppDependencies {
* Used to create lens embeddables.
*/
lens: LensPublicStart;
/**
* UI actions.
*/
uiActions?: UiActionsStart;
/**
* Internationalisation service
*/

View file

@ -65,6 +65,7 @@
"@kbn/ml-chi2test",
"@kbn/usage-collection-plugin",
"@kbn/analytics",
"@kbn/ml-ui-actions",
"@kbn/core-http-server",
],
"exclude": [

View file

@ -55,6 +55,7 @@ export const ML_PAGES = {
ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: 'jobs/new_job/step/job_type',
ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: 'jobs/new_job/step/index_or_search',
ANOMALY_DETECTION_CREATE_JOB_FROM_LENS: 'jobs/new_job/from_lens',
ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS: 'jobs/new_job/from_pattern_analysis',
ANOMALY_DETECTION_CREATE_JOB_FROM_MAP: 'jobs/new_job/from_map',
ANOMALY_DETECTION_MODULES_VIEW_OR_CREATE: 'modules/check_view_or_create',
SETTINGS: 'settings',

View file

@ -26,6 +26,7 @@ export enum CREATED_BY_LABEL {
APM_TRANSACTION = 'ml-module-apm-transaction',
SINGLE_METRIC_FROM_LENS = 'single-metric-wizard-from-lens',
MULTI_METRIC_FROM_LENS = 'multi-metric-wizard-from-lens',
CATEGORIZATION_FROM_PATTERN_ANALYSIS = 'categorization-wizard-from-pattern-analysis',
}
export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB';

View file

@ -49,6 +49,7 @@ export type MlGenericUrlState = MLPageState<
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_LENS
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_MAP
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB

View file

@ -65,6 +65,7 @@ export const ChangePointDetectionPage: FC = () => {
'share',
'storage',
'theme',
'uiActions',
'uiSettings',
'unifiedSearch',
'usageCollection',

View file

@ -56,6 +56,7 @@ export const LogCategorizationPage: FC = () => {
'share',
'storage',
'theme',
'uiActions',
'uiSettings',
'unifiedSearch',
])}

View file

@ -59,6 +59,7 @@ export const LogRateAnalysisPage: FC = () => {
'share',
'storage',
'theme',
'uiActions',
'uiSettings',
'unifiedSearch',
])}

View file

@ -361,7 +361,7 @@ const CategoryExamples: FC<{ definition: CategoryDefinition; examples: string[]
<EuiFlexGroup
direction="column"
justifyContent="center"
gutterSize="m"
gutterSize="xs"
className="mlAnomalyCategoryExamples"
>
{definition !== undefined && definition.terms && (

View file

@ -69,21 +69,21 @@ export class QuickJobCreatorBase {
datafeedConfig,
jobConfig,
createdByLabel,
dashboard,
start,
end,
startJob,
runInRealTime,
dashboard,
}: {
jobId: string;
datafeedConfig: Datafeed;
jobConfig: Job;
createdByLabel: CREATED_BY_LABEL;
dashboard: Dashboard;
start: number | undefined;
end: number | undefined;
startJob: boolean;
runInRealTime: boolean;
dashboard?: Dashboard;
}) {
const datafeedId = createDatafeedId(jobId);
const datafeed = { ...datafeedConfig, job_id: jobId, datafeed_id: datafeedId };
@ -93,7 +93,7 @@ export class QuickJobCreatorBase {
job_id: jobId,
custom_settings: {
created_by: createdByLabel,
...(await this.getCustomUrls(dashboard, datafeed)),
...(dashboard ? await this.getCustomUrls(dashboard, datafeed) : {}),
},
};
@ -230,7 +230,7 @@ export class QuickJobCreatorBase {
return mergedQueries;
}
protected async createDashboardLink(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
private async createDashboardLink(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
const dashboardTitle = dashboard?.getTitle();
if (dashboardTitle === undefined || dashboardTitle === '') {
// embeddable may have not been in a dashboard
@ -274,7 +274,7 @@ export class QuickJobCreatorBase {
return { url_name: urlName, url_value: url, time_range: 'auto' };
}
protected async getCustomUrls(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
private async getCustomUrls(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
const customUrls = await this.createDashboardLink(dashboard, datafeedConfig);
return dashboard !== undefined && customUrls !== null ? { custom_urls: [customUrls] } : {};
}

View file

@ -135,7 +135,7 @@ export class QuickLensJobCreator extends QuickJobCreatorBase {
}
}
async createJob(
private async createJob(
chartInfo: ChartInfo,
startString: string,
endString: string,

View file

@ -15,7 +15,7 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { QuickLensJobCreator } from './quick_create_job';
import type { MlApiServices } from '../../../services/ml_api_service';
import { getDefaultQuery } from '../utils/new_job_utils';
import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils';
interface Dependencies {
lens: LensPublicStart;
@ -27,8 +27,8 @@ interface Dependencies {
export async function resolver(
deps: Dependencies,
lensSavedObjectRisonString: string | undefined,
fromRisonStrong: string,
toRisonStrong: string,
fromRisonString: string,
toRisonString: string,
queryRisonString: string,
filtersRisonString: string,
layerIndexRisonString: string
@ -43,37 +43,11 @@ export async function resolver(
throw new Error('Cannot create visualization');
}
let query: Query;
let filters: Filter[];
try {
query = rison.decode(queryRisonString) as Query;
} catch (error) {
query = getDefaultQuery();
}
try {
filters = rison.decode(filtersRisonString) as Filter[];
} catch (error) {
filters = [];
}
let from: string;
let to: string;
try {
from = rison.decode(fromRisonStrong) as string;
} catch (error) {
from = '';
}
try {
to = rison.decode(toRisonStrong) as string;
} catch (error) {
to = '';
}
let layerIndex: number | undefined;
try {
layerIndex = rison.decode(layerIndexRisonString) as number;
} catch (error) {
layerIndex = undefined;
}
const query = getRisonValue<Query>(queryRisonString, getDefaultQuery()) as Query;
const filters = getRisonValue<Filter[]>(filtersRisonString, []);
const from = getRisonValue<string>(fromRisonString, '');
const to = getRisonValue<string>(toRisonString, '');
const layerIndex = getRisonValue<number | undefined>(layerIndexRisonString, undefined);
const jobCreator = new QuickLensJobCreator(
lens,

View file

@ -162,7 +162,7 @@ export class QuickGeoJobCreator extends QuickJobCreatorBase {
}
}
async createGeoJob({
private async createGeoJob({
dataViewId,
sourceDataView,
from,

View file

@ -5,16 +5,13 @@
* 2.0.
*/
import rison from '@kbn/rison';
import type { Query } from '@kbn/es-query';
import type { Filter } from '@kbn/es-query';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { MlApiServices } from '../../../services/ml_api_service';
import { QuickGeoJobCreator } from './quick_create_job';
import { getDefaultQuery } from '../utils/new_job_utils';
import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils';
interface Dependencies {
kibanaConfig: IUiSettingsClient;
@ -24,66 +21,32 @@ interface Dependencies {
}
export async function resolver(
deps: Dependencies,
dashboard: string,
dataViewId: string,
embeddable: string,
geoField: string,
splitField: string,
dashboardRisonString: string,
dataViewIdRisonString: string,
embeddableRisonString: string,
geoFieldRisonString: string,
splitFieldRisonString: string,
fromRisonString: string,
toRisonString: string,
layer?: string
layerRisonString?: string
) {
const { kibanaConfig, timeFilter, dashboardService, mlApiServices } = deps;
let decodedDashboard;
let decodedEmbeddable;
let decodedLayer;
let splitFieldDecoded;
let dvId;
const defaultLayer = { query: getDefaultQuery(), filters: [] };
try {
dvId = rison.decode(dataViewId) as string;
} catch (error) {
dvId = '';
}
const dashboard = getRisonValue<typeof defaultLayer>(dashboardRisonString, defaultLayer);
const embeddable = getRisonValue<typeof defaultLayer>(embeddableRisonString, defaultLayer);
try {
decodedDashboard = rison.decode(dashboard) as { query: Query; filters: Filter[] };
} catch (error) {
decodedDashboard = { query: getDefaultQuery(), filters: [] };
}
const layer =
layerRisonString !== undefined
? getRisonValue<typeof defaultLayer>(layerRisonString, defaultLayer)
: defaultLayer;
try {
decodedEmbeddable = rison.decode(embeddable) as { query: Query; filters: Filter[] };
} catch (error) {
decodedEmbeddable = { query: getDefaultQuery(), filters: [] };
}
const geoField = getRisonValue<string>(geoFieldRisonString, '');
const splitField = getRisonValue<string | null>(splitFieldRisonString, null);
const dataViewId = getRisonValue<string>(dataViewIdRisonString, '');
if (layer) {
try {
decodedLayer = rison.decode(layer) as { query: Query };
} catch (error) {
decodedLayer = { query: getDefaultQuery(), filters: [] };
}
}
try {
splitFieldDecoded = rison.decode(splitField) as string;
} catch (error) {
splitFieldDecoded = null;
}
let from: string;
let to: string;
try {
from = rison.decode(fromRisonString) as string;
} catch (error) {
from = '';
}
try {
to = rison.decode(toRisonString) as string;
} catch (error) {
to = '';
}
const from = getRisonValue<string>(fromRisonString, '');
const to = getRisonValue<string>(toRisonString, '');
const jobCreator = new QuickGeoJobCreator(
kibanaConfig,
@ -93,15 +56,15 @@ export async function resolver(
);
await jobCreator.createAndStashGeoJob(
dvId,
dataViewId,
from,
to,
decodedDashboard.query,
decodedDashboard.filters,
decodedEmbeddable.query,
decodedEmbeddable.filters,
dashboard.query,
dashboard.filters,
embeddable.query,
embeddable.filters,
geoField,
splitFieldDecoded,
decodedLayer?.query
splitField,
layer?.query
);
}

View file

@ -0,0 +1,14 @@
/*
* 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 {
QuickCategorizationJobCreator,
CATEGORIZATION_TYPE,
type CategorizationType,
} from './quick_create_job';
export { resolver } from './route_resolver';

View file

@ -0,0 +1,209 @@
/*
* 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 type { IUiSettingsClient } from '@kbn/core/public';
import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { TimeRange } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { MLCATEGORY, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { CREATED_BY_LABEL, DEFAULT_BUCKET_SPAN } from '../../../../../common/constants/new_job';
import { type CreateState, QuickJobCreatorBase } from '../job_from_dashboard/quick_create_job_base';
import type { MlApiServices } from '../../../services/ml_api_service';
import { createEmptyDatafeed, createEmptyJob } from '../common/job_creator/util/default_configs';
import { stashJobForCloning } from '../common/job_creator/util/general';
import type { JobCreatorType } from '../common/job_creator';
export const CATEGORIZATION_TYPE = {
COUNT: ML_JOB_AGGREGATION.COUNT,
RARE: ML_JOB_AGGREGATION.RARE,
} as const;
export type CategorizationType = typeof CATEGORIZATION_TYPE[keyof typeof CATEGORIZATION_TYPE];
export class QuickCategorizationJobCreator extends QuickJobCreatorBase {
constructor(
kibanaConfig: IUiSettingsClient,
timeFilter: TimefilterContract,
dashboardService: DashboardStart,
private data: DataPublicPluginStart,
mlApiServices: MlApiServices
) {
super(kibanaConfig, timeFilter, dashboardService, mlApiServices);
}
public async createAndSaveJob(
categorizationType: CategorizationType,
jobId: string,
bucketSpan: string,
dataView: DataView,
field: DataViewField,
partitionField: DataViewField | null,
stopOnWarn: boolean,
query: QueryDslQueryContainer,
timeRange: TimeRange,
startJob: boolean,
runInRealTime: boolean
): Promise<CreateState> {
if (query === undefined) {
throw new Error('Cannot create job, query and filters are undefined');
}
const { jobConfig, datafeedConfig, start, end } = await this.createJob(
categorizationType,
dataView,
field,
partitionField,
stopOnWarn,
timeRange,
query,
bucketSpan
);
const createdByLabel = CREATED_BY_LABEL.CATEGORIZATION_FROM_PATTERN_ANALYSIS;
const result = await this.putJobAndDataFeed({
jobId,
datafeedConfig,
jobConfig,
createdByLabel,
start,
end,
startJob,
runInRealTime,
});
return result;
}
public async createAndStashADJob(
categorizationType: CategorizationType,
dataViewId: string,
fieldName: string,
partitionFieldName: string | null,
stopOnWarn: boolean,
startString: string,
endString: string,
query: QueryDslQueryContainer
) {
try {
const dataView = await this.data.dataViews.get(dataViewId);
const field = dataView.getFieldByName(fieldName);
const partitionField = partitionFieldName
? dataView.getFieldByName(partitionFieldName) ?? null
: null;
if (field === undefined) {
throw new Error('Cannot create job, field is undefined');
}
const { jobConfig, datafeedConfig, start, end, includeTimeRange } = await this.createJob(
categorizationType,
dataView,
field,
partitionField,
stopOnWarn,
{ from: startString, to: endString },
query,
DEFAULT_BUCKET_SPAN
);
// add job config and start and end dates to the
// job cloning stash, so they can be used
// by the new job wizards
stashJobForCloning(
{
jobConfig,
datafeedConfig,
createdBy: CREATED_BY_LABEL.CATEGORIZATION_FROM_PATTERN_ANALYSIS,
start,
end,
} as JobCreatorType,
true,
includeTimeRange,
!includeTimeRange
);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
private async createJob(
categorizationType: CategorizationType,
dataView: DataView,
field: DataViewField,
partitionField: DataViewField | null,
stopOnWarn: boolean,
timeRange: TimeRange,
query: QueryDslQueryContainer,
bucketSpan: string
) {
const jobConfig = createEmptyJob();
const datafeedConfig = createEmptyDatafeed(dataView.getIndexPattern());
datafeedConfig.query = query;
jobConfig.analysis_config = {
categorization_field_name: field.name,
per_partition_categorization: {
enabled: partitionField !== null,
stop_on_warn: stopOnWarn,
},
influencers: [MLCATEGORY],
detectors: [
{
function: categorizationType,
by_field_name: MLCATEGORY,
},
],
bucket_span: bucketSpan,
};
if (partitionField !== null) {
jobConfig.analysis_config.detectors[0].partition_field_name = partitionField.name;
jobConfig.analysis_config.influencers!.push(partitionField.name);
}
jobConfig.data_description.time_field = dataView.timeFieldName;
let start: number | undefined;
let end: number | undefined;
let includeTimeRange = true;
try {
// attempt to parse the start and end dates.
// if start and end values cannot be determined
// instruct the job cloning code to auto-select the
// full time range for the index.
const { min, max } = this.timeFilter.calculateBounds(timeRange);
start = min?.valueOf();
end = max?.valueOf();
if (start === undefined || end === undefined || isNaN(start) || isNaN(end)) {
throw Error(
i18n.translate('xpack.ml.newJob.fromLens.createJob.error.timeRange', {
defaultMessage: 'Incompatible time range',
})
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
includeTimeRange = false;
start = undefined;
end = undefined;
}
return {
jobConfig,
datafeedConfig,
start,
end,
includeTimeRange,
};
}
}

View file

@ -0,0 +1,71 @@
/*
* 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 type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import {
type CategorizationType,
QuickCategorizationJobCreator,
CATEGORIZATION_TYPE,
} from './quick_create_job';
import type { MlApiServices } from '../../../services/ml_api_service';
import { getDefaultDatafeedQuery, getRisonValue } from '../utils/new_job_utils';
interface Dependencies {
kibanaConfig: IUiSettingsClient;
timeFilter: TimefilterContract;
dashboardService: DashboardStart;
data: DataPublicPluginStart;
mlApiServices: MlApiServices;
}
export async function resolver(
deps: Dependencies,
categorizationTypeRisonString: string,
dataViewIdRisonString: string,
fieldRisonString: string,
partitionFieldRisonString: string | null,
stopOnWarnRisonString: string,
fromRisonString: string,
toRisonString: string,
queryRisonString: string
) {
const { mlApiServices, timeFilter, kibanaConfig, dashboardService, data } = deps;
const query = getRisonValue<QueryDslQueryContainer>(queryRisonString, getDefaultDatafeedQuery());
const from = getRisonValue<string>(fromRisonString, '');
const to = getRisonValue<string>(toRisonString, '');
const categorizationType = getRisonValue<CategorizationType>(
categorizationTypeRisonString,
CATEGORIZATION_TYPE.COUNT
);
const dataViewId = getRisonValue<string>(dataViewIdRisonString, '');
const field = getRisonValue<string>(fieldRisonString, '');
const partitionField =
partitionFieldRisonString === null ? '' : getRisonValue<string>(partitionFieldRisonString, '');
const stopOnWarn = getRisonValue<boolean>(stopOnWarnRisonString, false);
const jobCreator = new QuickCategorizationJobCreator(
kibanaConfig,
timeFilter,
dashboardService,
data,
mlApiServices
);
await jobCreator.createAndStashADJob(
categorizationType,
dataViewId,
field,
partitionField,
stopOnWarn,
from,
to,
query
);
}

View file

@ -0,0 +1,43 @@
/*
* 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 type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { ML_APP_LOCATOR } from '../../../../../common/constants/locator';
import { ML_PAGES } from '../../../../locator';
import type { CategorizationType } from './quick_create_job';
export async function redirectToADJobWizards(
categorizationType: CategorizationType,
dataView: DataView,
field: DataViewField,
partitionField: DataViewField | null,
stopOnWarn: boolean,
query: QueryDslQueryContainer,
timeRange: TimeRange,
share: SharePluginStart
) {
const locator = share.url.locators.get(ML_APP_LOCATOR)!;
const url = await locator.getUrl({
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS,
pageState: {
categorizationType,
dataViewId: dataView.id,
field: field.name,
partitionField: partitionField?.name || null,
stopOnWarn,
from: timeRange.from,
to: timeRange.to,
query: JSON.stringify(query),
},
});
window.open(url, '_blank');
}

View file

@ -55,6 +55,7 @@ async function getWizardUrlFromCloningJob(createdBy: string | undefined, dataVie
page = JOB_TYPE.POPULATION;
break;
case CREATED_BY_LABEL.CATEGORIZATION:
case CREATED_BY_LABEL.CATEGORIZATION_FROM_PATTERN_ANALYSIS:
page = JOB_TYPE.CATEGORIZATION;
break;
case CREATED_BY_LABEL.RARE:

View file

@ -7,6 +7,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { cloneDeep } from 'lodash';
import rison from '@kbn/rison';
import {
Query,
fromKueryExpression,
@ -162,3 +163,14 @@ export function checkCardinalitySuccess(data: any) {
return response;
}
export function getRisonValue<T extends string | boolean | number | object | undefined | null>(
risonString: string,
defaultValue: T
) {
try {
return rison.decode(risonString) as T;
} catch (error) {
return defaultValue;
}
}

View file

@ -0,0 +1,71 @@
/*
* 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 { Redirect } from 'react-router-dom';
import { parse } from 'query-string';
import { useMlKibana } from '../../../contexts/kibana';
import { ML_PAGES } from '../../../../locator';
import { createPath, MlRoute, PageLoader, PageProps } from '../../router';
import { useRouteResolver } from '../../use_resolver';
import { resolver } from '../../../jobs/new_job/job_from_pattern_analysis';
export const fromPatternAnalysisRouteFactory = (): MlRoute => ({
path: createPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS),
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [],
});
const PageWrapper: FC<PageProps> = ({ location }) => {
const {
categorizationType,
dataViewId,
field,
partitionField,
stopOnWarn,
from,
to,
query,
}: Record<string, any> = parse(location.search, {
sort: false,
});
const {
services: {
data,
dashboard: dashboardService,
uiSettings: kibanaConfig,
mlServices: { mlApiServices },
},
} = useMlKibana();
const { context } = useRouteResolver('full', ['canCreateJob'], {
redirect: () =>
resolver(
{
mlApiServices,
timeFilter: data.query.timefilter.timefilter,
kibanaConfig,
dashboardService,
data,
},
categorizationType,
dataViewId,
field,
partitionField,
stopOnWarn,
from,
to,
query
),
});
return (
<PageLoader context={context}>
{<Redirect to={createPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB)} />}
</PageLoader>
);
};

View file

@ -12,3 +12,4 @@ export * from './wizard';
export * from './recognize';
export * from './from_lens';
export * from './from_map';
export * from './from_pattern_analysis';

View file

@ -14,15 +14,21 @@ import {
EVENT_RATE_FIELD_ID,
} from '@kbn/ml-anomaly-utils';
import { getGeoFields, filterCategoryFields } from '../../../../common/util/fields_utils';
import { ml } from '../ml_api_service';
import { ml, type MlApiServices } from '../ml_api_service';
import { processTextAndKeywordFields, NewJobCapabilitiesServiceBase } from './new_job_capabilities';
class NewJobCapsService extends NewJobCapabilitiesServiceBase {
export class NewJobCapsService extends NewJobCapabilitiesServiceBase {
private _catFields: Field[] = [];
private _dateFields: Field[] = [];
private _geoFields: Field[] = [];
private _includeEventRateField: boolean = true;
private _removeTextFields: boolean = true;
private _mlApiService: MlApiServices;
constructor(mlApiService: MlApiServices) {
super();
this._mlApiService = mlApiService;
}
public get catFields(): Field[] {
return this._catFields;
@ -49,7 +55,10 @@ class NewJobCapsService extends NewJobCapabilitiesServiceBase {
this._includeEventRateField = includeEventRateField;
this._removeTextFields = removeTextFields;
const resp = await ml.jobs.newJobCaps(dataView.getIndexPattern(), dataView.type === 'rollup');
const resp = await this._mlApiService.jobs.newJobCaps(
dataView.getIndexPattern(),
dataView.type === 'rollup'
);
const { fields: allFields, aggs } = createObjects(resp, dataView.getIndexPattern());
if (this._includeEventRateField === true) {
@ -175,4 +184,4 @@ function addEventRateField(aggs: Aggregation[], fields: Field[]) {
fields.splice(0, 0, eventRateField);
}
export const newJobCapsService = new NewJobCapsService();
export const newJobCapsService = new NewJobCapsService(ml);

View file

@ -0,0 +1,273 @@
/*
* 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, useMemo, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiCheckableCard,
EuiTitle,
EuiSpacer,
EuiSwitch,
EuiHorizontalRule,
EuiComboBoxOptionOption,
EuiComboBox,
EuiFormRow,
EuiCallOut,
} from '@elastic/eui';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { TimeRange } from '@kbn/es-query';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { redirectToADJobWizards } from '../../../../application/jobs/new_job/job_from_pattern_analysis/utils';
import { createFieldOptions } from '../../../../application/jobs/new_job/common/job_creator/util/general';
import { NewJobCapsService } from '../../../../application/services/new_job_capabilities/new_job_capabilities_service';
import {
type CategorizationType,
CATEGORIZATION_TYPE,
QuickCategorizationJobCreator,
} from '../../../../application/jobs/new_job/job_from_pattern_analysis';
import { useMlFromLensKibanaContext } from '../../common/context';
import { JobDetails, type CreateADJobParams } from '../../common/job_details';
interface Props {
dataView: DataView;
field: DataViewField;
query: QueryDslQueryContainer;
timeRange: TimeRange;
}
export const CreateJob: FC<Props> = ({ dataView, field, query, timeRange }) => {
const {
services: {
data,
share,
uiSettings,
mlServices: { mlApiServices },
dashboardService,
},
} = useMlFromLensKibanaContext();
const [categorizationType, setCategorizationType] = useState<CategorizationType>(
CATEGORIZATION_TYPE.COUNT
);
const [enablePerPartitionCategorization, setEnablePerPartitionCategorization] = useState(false);
const [stopOnWarn, setStopOnWarn] = useState(false);
const [categoryFieldOptions, setCategoryFieldsOptions] = useState<EuiComboBoxOptionOption[]>([]);
const [selectedPartitionFieldOptions, setSelectedPartitionFieldOptions] = useState<
EuiComboBoxOptionOption[]
>([]);
const [formComplete, setFormComplete] = useState<boolean | undefined>(undefined);
const toggleEnablePerPartitionCategorization = useCallback(
() => setEnablePerPartitionCategorization(!enablePerPartitionCategorization),
[enablePerPartitionCategorization]
);
const toggleStopOnWarn = useCallback(() => setStopOnWarn(!stopOnWarn), [stopOnWarn]);
useMemo(() => {
const newJobCapsService = new NewJobCapsService(mlApiServices);
newJobCapsService.initializeFromDataVIew(dataView).then(() => {
const options: EuiComboBoxOptionOption[] = [
...createFieldOptions(newJobCapsService.categoryFields, []),
].map((o) => ({
...o,
}));
setCategoryFieldsOptions(options);
});
}, [dataView, mlApiServices]);
const quickJobCreator = useMemo(
() =>
new QuickCategorizationJobCreator(
uiSettings,
data.query.timefilter.timefilter,
dashboardService,
data,
mlApiServices
),
[dashboardService, data, mlApiServices, uiSettings]
);
function createADJobInWizard() {
const partitionField = selectedPartitionFieldOptions.length
? dataView.getFieldByName(selectedPartitionFieldOptions[0].label) ?? null
: null;
redirectToADJobWizards(
categorizationType,
dataView,
field,
partitionField,
stopOnWarn,
query,
timeRange,
share
);
}
useEffect(() => {
setSelectedPartitionFieldOptions([]);
setStopOnWarn(false);
}, [enablePerPartitionCategorization]);
useEffect(() => {
setFormComplete(
enablePerPartitionCategorization === false || selectedPartitionFieldOptions.length > 0
);
}, [enablePerPartitionCategorization, selectedPartitionFieldOptions]);
async function createADJob({ jobId, bucketSpan, startJob, runInRealTime }: CreateADJobParams) {
const partitionField = selectedPartitionFieldOptions.length
? dataView.getFieldByName(selectedPartitionFieldOptions[0].label) ?? null
: null;
const result = await quickJobCreator.createAndSaveJob(
categorizationType,
jobId,
bucketSpan,
dataView,
field,
partitionField,
stopOnWarn,
query,
timeRange,
startJob,
runInRealTime
);
return result;
}
return (
<JobDetails
createADJob={createADJob}
createADJobInWizard={createADJobInWizard}
embeddable={undefined}
timeRange={timeRange}
layer={undefined}
layerIndex={0}
outerFormComplete={formComplete}
>
<>
<EuiCheckableCard
id={'count'}
label={
<>
<EuiTitle size="xs">
<h5>
<FormattedMessage
defaultMessage="Count"
id="xpack.ml.newJobFromPatternAnalysisFlyout.count.title"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<FormattedMessage
defaultMessage="Look for anomalies in the event rate of a category."
id="xpack.ml.newJobFromPatternAnalysisFlyout.count.description"
/>
</>
}
checked={categorizationType === CATEGORIZATION_TYPE.COUNT}
onChange={() => setCategorizationType(CATEGORIZATION_TYPE.COUNT)}
/>
<EuiSpacer size="m" />
<EuiCheckableCard
id={'rare'}
label={
<>
<EuiTitle size="xs">
<h5>
<FormattedMessage
defaultMessage="Rare"
id="xpack.ml.newJobFromPatternAnalysisFlyout.rare.title"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<FormattedMessage
defaultMessage="Look for categories that occur rarely in time."
id="xpack.ml.newJobFromPatternAnalysisFlyout.rare.description"
/>
</>
}
checked={categorizationType === CATEGORIZATION_TYPE.RARE}
onChange={() => setCategorizationType(CATEGORIZATION_TYPE.RARE)}
/>
<EuiSpacer size="m" />
<EuiSwitch
name="categorizationPerPartitionSwitch"
disabled={false}
checked={enablePerPartitionCategorization}
onChange={toggleEnablePerPartitionCategorization}
data-test-subj="mlNewJobFromPatternAnalysisFlyoutSwitchCategorizationPerPartition"
label={
<FormattedMessage
id="xpack.ml.newJobFromPatternAnalysisFlyout.perPartitionCategorizationSwitchLabel"
defaultMessage="Enable per-partition categorization"
/>
}
/>
{enablePerPartitionCategorization ? (
<>
<EuiSpacer size="m" />
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.ml.newJobFromPatternAnalysisFlyout.categorizationPerPartitionField.infoCallout"
defaultMessage="Determine categories independently for each value of the partition field."
/>
}
/>
<EuiSpacer size="m" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.ml.newJobFromPatternAnalysisFlyout.categorizationPerPartitionFieldLabel"
defaultMessage="Partition field"
/>
}
>
<EuiComboBox
singleSelection={{ asPlainText: true }}
options={categoryFieldOptions}
selectedOptions={selectedPartitionFieldOptions}
onChange={setSelectedPartitionFieldOptions}
isClearable={true}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiSwitch
name="categorizationPerPartitionSwitch"
disabled={false}
checked={stopOnWarn}
onChange={toggleStopOnWarn}
label={
<FormattedMessage
id="xpack.ml.newJobFromPatternAnalysisFlyout.stopOnWarnSwitchLabel"
defaultMessage="Stop on warn"
/>
}
/>
</>
) : null}
<EuiSpacer size="m" />
<EuiHorizontalRule margin="m" />
</>
</JobDetails>
);
};

View file

@ -0,0 +1,79 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiFlyoutBody,
EuiTitle,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { TimeRange } from '@kbn/es-query';
import { CreateJob } from './create_job';
interface Props {
dataView: DataView;
field: DataViewField;
query: QueryDslQueryContainer;
timeRange: TimeRange;
onClose: () => void;
}
export const CreateCategorizationJobFlyout: FC<Props> = ({
onClose,
dataView,
field,
query,
timeRange,
}) => {
return (
<>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.ml.embeddables.newJobFromPatternAnalysisFlyout.title"
defaultMessage="Create anomaly detection job"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText size="s">
<FormattedMessage
id="xpack.ml.embeddables.newJobFromPatternAnalysisFlyout.secondTitle"
defaultMessage="Create a categorization job for {field}"
values={{ field: field.name }}
/>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<CreateJob dataView={dataView} field={field} query={query} timeRange={timeRange} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<FormattedMessage
id="xpack.ml.embeddables.newJobFromPatternAnalysisFlyout.closeButton"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

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 { CreateCategorizationJobFlyout } from './flyout';

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 { showPatternAnalysisToADJobFlyout } from './show_flyout';

View file

@ -0,0 +1,41 @@
/*
* 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 type { CoreStart } from '@kbn/core/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { TimeRange } from '@kbn/es-query';
import { createFlyout, type FlyoutComponentProps } from '../common/create_flyout';
import { CreateCategorizationJobFlyout } from './flyout';
export async function showPatternAnalysisToADJobFlyout(
dataView: DataView,
field: DataViewField,
query: QueryDslQueryContainer,
timeRange: TimeRange,
coreStart: CoreStart,
share: SharePluginStart,
data: DataPublicPluginStart,
dashboardService: DashboardStart,
lens?: LensPublicStart
): Promise<void> {
const Comp: FC<FlyoutComponentProps> = ({ onClose }) => (
<CreateCategorizationJobFlyout
dataView={dataView}
field={field}
query={query}
timeRange={timeRange}
onClose={onClose}
/>
);
return createFlyout(Comp, coreStart, share, data, dashboardService, lens);
}

View file

@ -14,15 +14,16 @@ import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { MapEmbeddable } from '@kbn/maps-plugin/public';
import type { Embeddable } from '@kbn/lens-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { getMlGlobalServices } from '../../../application/app';
export interface FlyoutComponentProps {
onClose: () => void;
}
export function createFlyout(
FlyoutComponent: React.FunctionComponent<any>,
embeddable: MapEmbeddable | Embeddable,
coreStart: CoreStart,
share: SharePluginStart,
data: DataPublicPluginStart,
@ -57,7 +58,6 @@ export function createFlyout(
}}
>
<FlyoutComponent
embeddable={embeddable}
onClose={() => {
onFlyoutClose();
resolve();

View file

@ -32,6 +32,7 @@ import type { Embeddable } from '@kbn/lens-plugin/public';
import type { MapEmbeddable } from '@kbn/maps-plugin/public';
import { extractErrorMessage } from '@kbn/ml-error-utils';
import type { TimeRange } from '@kbn/es-query';
import { QuickLensJobCreator } from '../../../application/jobs/new_job/job_from_lens';
import type { LayerResult } from '../../../application/jobs/new_job/job_from_lens';
import type { CreateState } from '../../../application/jobs/new_job/job_from_dashboard';
@ -40,12 +41,12 @@ import { basicJobValidation } from '../../../../common/util/job_utils';
import { JOB_ID_MAX_LENGTH } from '../../../../common/constants/validation';
import { invalidTimeIntervalMessage } from '../../../application/jobs/new_job/common/job_validator/util';
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator';
import { useMlFromLensKibanaContext } from '../lens/context';
import { useMlFromLensKibanaContext } from './context';
export interface CreateADJobParams {
jobId: string;
bucketSpan: string;
embeddable: MapEmbeddable | Embeddable;
embeddable: MapEmbeddable | Embeddable | undefined;
startJob: boolean;
runInRealTime: boolean;
}
@ -56,8 +57,10 @@ interface Props {
createADJob: (args: CreateADJobParams) => Promise<CreateState>;
layer?: LayerResult;
layerIndex: number;
embeddable: Embeddable | MapEmbeddable;
embeddable: Embeddable | MapEmbeddable | undefined;
timeRange: TimeRange | undefined;
incomingCreateError?: { text: string; errorText: string };
outerFormComplete?: boolean;
}
enum STATE {
@ -75,7 +78,9 @@ export const JobDetails: FC<Props> = ({
layer,
layerIndex,
embeddable,
timeRange,
incomingCreateError,
outerFormComplete,
}) => {
const {
services: {
@ -121,7 +126,6 @@ export const JobDetails: FC<Props> = ({
const viewResults = useCallback(
async (type: JOB_TYPE | null) => {
const { timeRange } = embeddable.getInput();
const locator = share.url.locators.get(ML_APP_LOCATOR);
if (locator) {
const page = startJob
@ -144,7 +148,7 @@ export const JobDetails: FC<Props> = ({
application.navigateToUrl(url);
}
},
[jobId, embeddable, share, application, startJob]
[share, startJob, jobId, timeRange, application]
);
function setStartJobWrapper(start: boolean) {
@ -313,7 +317,8 @@ export const JobDetails: FC<Props> = ({
state === STATE.VALIDATING ||
jobId === '' ||
jobIdValidationError !== '' ||
bucketSpanValidationError !== ''
bucketSpanValidationError !== '' ||
outerFormComplete === false
}
onClick={createJob.bind(null, layerIndex)}
size="s"

View file

@ -23,7 +23,7 @@ import {
import { Layer } from './layer';
import type { LayerResult } from '../../../../application/jobs/new_job/job_from_lens';
import { VisualizationExtractor } from '../../../../application/jobs/new_job/job_from_lens';
import { useMlFromLensKibanaContext } from '../context';
import { useMlFromLensKibanaContext } from '../../common/context';
interface Props {
embeddable: Embeddable;

View file

@ -17,7 +17,7 @@ import {
} from '../../../../../application/jobs/new_job/job_from_lens';
import type { LayerResult } from '../../../../../application/jobs/new_job/job_from_lens';
import { JOB_TYPE } from '../../../../../../common/constants/new_job';
import { useMlFromLensKibanaContext } from '../../context';
import { useMlFromLensKibanaContext } from '../../../common/context';
import { JobDetails, CreateADJobParams } from '../../../common/job_details';
interface Props {
@ -79,6 +79,7 @@ export const CompatibleLayer: FC<Props> = ({ layer, layerIndex, embeddable }) =>
createADJob={createADJob}
createADJobInWizard={createADJobInWizard}
embeddable={embeddable}
timeRange={embeddable.getInput().timeRange}
layer={layer}
layerIndex={layerIndex}
>

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import React, { FC } from 'react';
import type { Embeddable } from '@kbn/lens-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { createFlyout } from '../common/create_flyout';
import { createFlyout, type FlyoutComponentProps } from '../common/create_flyout';
import { LensLayerSelectionFlyout } from './lens_vis_layer_selection_flyout';
export async function showLensVisToADJobFlyout(
@ -19,16 +20,11 @@ export async function showLensVisToADJobFlyout(
coreStart: CoreStart,
share: SharePluginStart,
data: DataPublicPluginStart,
lens: LensPublicStart,
dashboardService: DashboardStart
dashboardService: DashboardStart,
lens: LensPublicStart
): Promise<void> {
return createFlyout(
LensLayerSelectionFlyout,
embeddable,
coreStart,
share,
data,
dashboardService,
lens
const Comp: FC<FlyoutComponentProps> = ({ onClose }) => (
<LensLayerSelectionFlyout embeddable={embeddable} onClose={onClose} />
);
return createFlyout(Comp, coreStart, share, data, dashboardService, lens);
}

View file

@ -25,7 +25,7 @@ import {
QuickGeoJobCreator,
redirectToGeoJobWizard,
} from '../../../../../application/jobs/new_job/job_from_map';
import { useMlFromLensKibanaContext } from '../../../lens/context';
import { useMlFromLensKibanaContext } from '../../../common/context';
import { JobDetails, CreateADJobParams } from '../../../common/job_details';
interface DropDownLabel {
@ -147,6 +147,7 @@ export const CompatibleLayer: FC<Props> = ({ embeddable, layer, layerIndex }) =>
createADJob={createGeoJob}
createADJobInWizard={createGeoJobInWizard}
embeddable={embeddable}
timeRange={embeddable.getInput().timeRange}
incomingCreateError={createError}
>
<>

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import React, { FC } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
@ -12,7 +13,7 @@ import type { MapEmbeddable } from '@kbn/maps-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { GeoJobFlyout } from './flyout';
import { createFlyout } from '../common/create_flyout';
import { createFlyout, type FlyoutComponentProps } from '../common/create_flyout';
export async function showMapVisToADJobFlyout(
embeddable: MapEmbeddable,
@ -21,5 +22,8 @@ export async function showMapVisToADJobFlyout(
data: DataPublicPluginStart,
dashboardService: DashboardStart
): Promise<void> {
return createFlyout(GeoJobFlyout, embeddable, coreStart, share, data, dashboardService);
const Comp: FC<FlyoutComponentProps> = ({ onClose }) => (
<GeoJobFlyout embeddable={embeddable} onClose={onClose} />
);
return createFlyout(Comp, coreStart, share, data, dashboardService);
}

View file

@ -85,6 +85,7 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_LENS:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_MAP:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS:
case ML_PAGES.DATA_VISUALIZER:
case ML_PAGES.DATA_VISUALIZER_FILE:
case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER:

View file

@ -8,9 +8,14 @@
import { CoreSetup } from '@kbn/core/public';
import { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
import { CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER } from '@kbn/ml-ui-actions';
import { createEditSwimlanePanelAction } from './edit_swimlane_panel_action';
import { createOpenInExplorerAction } from './open_in_anomaly_explorer_action';
import { createVisToADJobAction } from './open_vis_in_ml_action';
import {
createCategorizationADJobAction,
createCategorizationADJobTrigger,
} from './open_create_categorization_job_action';
import { MlPluginStart, MlStartDependencies } from '../plugin';
import { createApplyInfluencerFiltersAction } from './apply_influencer_filters_action';
import {
@ -45,6 +50,7 @@ export function registerMlUiActions(
const clearSelectionAction = createClearSelectionAction(core.getStartServices);
const editExplorerPanelAction = createEditAnomalyChartsPanelAction(core.getStartServices);
const visToAdJobAction = createVisToADJobAction(core.getStartServices);
const categorizationADJobAction = createCategorizationADJobAction(core.getStartServices);
// Register actions
uiActions.registerAction(editSwimlanePanelAction);
@ -54,6 +60,7 @@ export function registerMlUiActions(
uiActions.registerAction(applyTimeRangeSelectionAction);
uiActions.registerAction(clearSelectionAction);
uiActions.registerAction(editExplorerPanelAction);
uiActions.registerAction(categorizationADJobAction);
// Assign triggers
uiActions.attachAction(CONTEXT_MENU_TRIGGER, editSwimlanePanelAction.id);
@ -62,6 +69,7 @@ export function registerMlUiActions(
uiActions.registerTrigger(swimLaneSelectionTrigger);
uiActions.registerTrigger(entityFieldSelectionTrigger);
uiActions.registerTrigger(createCategorizationADJobTrigger);
uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, applyInfluencerFiltersAction);
uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, applyTimeRangeSelectionAction);
@ -69,4 +77,8 @@ export function registerMlUiActions(
uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, clearSelectionAction);
uiActions.addTriggerAction(EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER, applyEntityFieldFilterAction);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, visToAdJobAction);
uiActions.addTriggerAction(
CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
categorizationADJobAction
);
}

View file

@ -0,0 +1,70 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { Trigger, UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
import {
CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION,
CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
type CreateCategorizationADJobContext,
} from '@kbn/ml-ui-actions';
import type { MlCoreSetup } from '../plugin';
export const createCategorizationADJobTrigger: Trigger = {
id: CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
title: i18n.translate('xpack.ml.actions.createADJobFromPatternAnalysis', {
defaultMessage: 'Create categorization anomaly detection job',
}),
description: i18n.translate('xpack.ml.actions.createADJobFromPatternAnalysis', {
defaultMessage: 'Create categorization anomaly detection job',
}),
};
export function createCategorizationADJobAction(
getStartServices: MlCoreSetup['getStartServices']
): UiActionsActionDefinition<CreateCategorizationADJobContext> {
return {
id: 'create-ml-categorization-ad-job-action',
type: CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION,
getIconType(context): string {
return 'machineLearningApp';
},
getDisplayName: () =>
i18n.translate('xpack.ml.actions.createADJobFromPatternAnalysis', {
defaultMessage: 'Create categorization anomaly detection job',
}),
async execute({ dataView, field, query, timeRange }: CreateCategorizationADJobContext) {
if (!dataView) {
throw new Error('Not possible to execute an action without the embeddable context');
}
try {
const [{ showPatternAnalysisToADJobFlyout }, [coreStart, { share, data, dashboard }]] =
await Promise.all([import('../embeddables/job_creation/aiops'), getStartServices()]);
await showPatternAnalysisToADJobFlyout(
dataView,
field,
query,
timeRange,
coreStart,
share,
data,
dashboard
);
} catch (e) {
return Promise.reject();
}
},
async isCompatible({ dataView, field }: CreateCategorizationADJobContext) {
return (
dataView.timeFieldName !== undefined &&
dataView.fields.find((f) => f.name === field.name) !== undefined
);
},
};
}

View file

@ -39,7 +39,7 @@ export function createVisToADJobAction(
if (lens === undefined) {
return;
}
await showLensVisToADJobFlyout(embeddable, coreStart, share, data, lens, dashboard);
await showLensVisToADJobFlyout(embeddable, coreStart, share, data, dashboard, lens);
} else if (isMapEmbeddable(embeddable)) {
const [{ showMapVisToADJobFlyout }, [coreStart, { share, data, dashboard }]] =
await Promise.all([import('../embeddables/job_creation/map'), getStartServices()]);

View file

@ -96,6 +96,7 @@
"@kbn/ml-runtime-field-utils",
"@kbn/ml-date-utils",
"@kbn/ml-category-validator",
"@kbn/ml-ui-actions",
"@kbn/deeplinks-ml",
"@kbn/core-notifications-browser-mocks",
"@kbn/unified-field-list",

View file

@ -5069,6 +5069,10 @@
version "0.0.0"
uid ""
"@kbn/ml-ui-actions@link:x-pack/packages/ml/ui_actions":
version "0.0.0"
uid ""
"@kbn/ml-url-state@link:x-pack/packages/ml/url_state":
version "0.0.0"
uid ""