diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts index 3a3b31c8e58c..800b81d47372 100644 --- a/x-pack/plugins/ml/common/constants/new_job.ts +++ b/x-pack/plugins/ml/common/constants/new_job.ts @@ -11,6 +11,7 @@ export enum JOB_TYPE { POPULATION = 'population', ADVANCED = 'advanced', CATEGORIZATION = 'categorization', + RARE = 'rare', } export enum CREATED_BY_LABEL { @@ -18,6 +19,7 @@ export enum CREATED_BY_LABEL { MULTI_METRIC = 'multi-metric-wizard', POPULATION = 'population-wizard', CATEGORIZATION = 'categorization-wizard', + RARE = 'rare-wizard', APM_TRANSACTION = 'ml-module-apm-transaction', } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts index eba5a954fadc..ccea4ddf52ea 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts @@ -66,7 +66,7 @@ export class CategorizationJobCreator extends JobCreator { eventRate: Field | null ) { if (count === null || rare === null || eventRate === null) { - return; + throw Error('event_rate field or count or rare aggregations missing'); } this._createCountDetector = () => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts index 0d4900e80b9f..ec4e4c0e0c7e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts @@ -11,6 +11,7 @@ export { MultiMetricJobCreator } from './multi_metric_job_creator'; export { PopulationJobCreator } from './population_job_creator'; export { AdvancedJobCreator } from './advanced_job_creator'; export { CategorizationJobCreator } from './categorization_job_creator'; +export { RareJobCreator } from './rare_job_creator'; export { JobCreatorType, isSingleMetricJobCreator, @@ -18,5 +19,6 @@ export { isPopulationJobCreator, isAdvancedJobCreator, isCategorizationJobCreator, + isRareJobCreator, } from './type_guards'; export { jobCreatorFactory } from './job_creator_factory'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index d4a410bcda24..45e7247f0bd8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -395,6 +395,9 @@ export class JobCreator { // change the detector to be a non-zer or non-null count or sum. // note, the aggregations will always be a standard count or sum and not a non-null or non-zero version this._detectors.forEach((d, i) => { + if (this._aggs[i] === undefined) { + return; + } switch (this._aggs[i].id) { case ML_JOB_AGGREGATION.COUNT: d.function = this._sparseData diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts index e815ddfcd97c..7b9305e42dfd 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts @@ -12,6 +12,7 @@ import { PopulationJobCreator } from './population_job_creator'; import { AdvancedJobCreator } from './advanced_job_creator'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CategorizationJobCreator } from './categorization_job_creator'; +import { RareJobCreator } from './rare_job_creator'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; @@ -37,6 +38,9 @@ export const jobCreatorFactory = (jobType: JOB_TYPE) => ( case JOB_TYPE.CATEGORIZATION: jc = CategorizationJobCreator; break; + case JOB_TYPE.RARE: + jc = RareJobCreator; + break; default: jc = SingleMetricJobCreator; break; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 14570fa039cc..24b319223121 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -22,7 +22,7 @@ import { IndexPattern } from '../../../../../../../../../src/plugins/data/public export class PopulationJobCreator extends JobCreator { // a population job has one overall over (split) field, which is the same for all detectors // each detector has an optional by field - private _splitField: SplitField = null; + private _populatonField: SplitField = null; private _byFields: SplitField[] = []; protected _type: JOB_TYPE = JOB_TYPE.POPULATION; @@ -65,27 +65,27 @@ export class PopulationJobCreator extends JobCreator { } // add an over field to all detectors - public setSplitField(field: SplitField) { - this._splitField = field; + public setPopulationField(field: SplitField) { + this._populatonField = field; - if (this._splitField === null) { - this.removeSplitField(); + if (this._populatonField === null) { + this.removePopulationField(); } else { for (let i = 0; i < this._detectors.length; i++) { - this._detectors[i].over_field_name = this._splitField.id; + this._detectors[i].over_field_name = this._populatonField.id; } } } // remove over field from all detectors - public removeSplitField() { + public removePopulationField() { this._detectors.forEach((d) => { delete d.over_field_name; }); } - public get splitField(): SplitField { - return this._splitField; + public get populationField(): SplitField { + return this._populatonField; } public addDetector(agg: Aggregation, field: Field) { @@ -112,8 +112,8 @@ export class PopulationJobCreator extends JobCreator { private _createDetector(agg: Aggregation, field: Field) { const dtr: Detector = createBasicDetector(agg, field); - if (this._splitField !== null) { - dtr.over_field_name = this._splitField.id; + if (this._populatonField !== null) { + dtr.over_field_name = this._populatonField.id; } return dtr; } @@ -143,7 +143,7 @@ export class PopulationJobCreator extends JobCreator { if (detectors.length) { if (detectors[0].overField !== null) { - this.setSplitField(detectors[0].overField); + this.setPopulationField(detectors[0].overField); } } detectors.forEach((d, i) => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts new file mode 100644 index 000000000000..73050dc4b783 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts @@ -0,0 +1,173 @@ +/* + * 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 { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; +import { JobCreator } from './job_creator'; +import { Field, SplitField, Aggregation } from '../../../../../../common/types/fields'; +import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; +import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; +import { getRichDetectors } from './util/general'; +import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { isSparseDataJob } from './util/general'; +import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; + +export class RareJobCreator extends JobCreator { + private _rareField: Field | null = null; + private _populationField: SplitField = null; + private _splitField: SplitField = null; + + protected _type: JOB_TYPE = JOB_TYPE.RARE; + private _rareInPopulation: boolean = false; + private _frequentlyRare: boolean = false; + private _rareAgg: Aggregation; + private _freqRareAgg: Aggregation; + + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { + super(indexPattern, savedSearch, query); + this.createdBy = CREATED_BY_LABEL.RARE; + this._wizardInitialized$.next(true); + this._rareAgg = {} as Aggregation; + this._freqRareAgg = {} as Aggregation; + } + + public setDefaultDetectorProperties(rare: Aggregation | null, freqRare: Aggregation | null) { + if (rare === null || freqRare === null) { + throw Error('rare or freq_rare aggregations missing'); + } + this._rareAgg = rare; + this._freqRareAgg = freqRare; + } + + public setRareField(field: Field | null) { + this._rareField = field; + + if (field === null) { + this.removePopulationField(); + this.removeSplitField(); + this._removeDetector(0); + this._detectors.length = 0; + this._fields.length = 0; + return; + } + + const agg = this._frequentlyRare ? this._freqRareAgg : this._rareAgg; + + const dtr: Detector = { + function: agg.id, + }; + if (this._detectors.length === 0) { + this._addDetector(dtr, agg, field); + } else { + this._editDetector(dtr, agg, field, 0); + } + + this._detectors[0].by_field_name = field.id; + } + + public get rareField() { + return this._rareField; + } + + public get rareInPopulation() { + return this._rareInPopulation; + } + + public set rareInPopulation(bool: boolean) { + this._rareInPopulation = bool; + if (bool === false) { + this.removePopulationField(); + } + } + + public get frequentlyRare() { + return this._frequentlyRare; + } + + public set frequentlyRare(bool: boolean) { + this._frequentlyRare = bool; + if (this._detectors.length) { + const agg = bool ? this._freqRareAgg : this._rareAgg; + this._detectors[0].function = agg.id; + this._aggs[0] = agg; + } + } + + // set the population field, applying it to each detector + public setPopulationField(field: SplitField) { + this._populationField = field; + + if (this._populationField === null) { + this.removePopulationField(); + } else { + for (let i = 0; i < this._detectors.length; i++) { + this._detectors[i].over_field_name = this._populationField.id; + } + } + } + + public removePopulationField() { + this._populationField = null; + this._detectors.forEach((d) => { + delete d.over_field_name; + }); + } + + public get populationField(): SplitField { + return this._populationField; + } + + // set the split field, applying it to each detector + public setSplitField(field: SplitField) { + this._splitField = field; + + if (this._splitField === null) { + this.removeSplitField(); + } else { + for (let i = 0; i < this._detectors.length; i++) { + this._detectors[i].partition_field_name = this._splitField.id; + } + } + } + + public removeSplitField() { + this._detectors.forEach((d) => { + delete d.partition_field_name; + }); + } + + public get splitField(): SplitField { + return this._splitField; + } + + public cloneFromExistingJob(job: Job, datafeed: Datafeed) { + this._overrideConfigs(job, datafeed); + this.createdBy = CREATED_BY_LABEL.RARE; + this._sparseData = isSparseDataJob(job, datafeed); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + + this.removeSplitField(); + this.removePopulationField(); + this.removeAllDetectors(); + + if (detectors.length) { + this.setRareField(detectors[0].byField); + this.frequentlyRare = detectors[0].agg?.id === ML_JOB_AGGREGATION.FREQ_RARE; + + if (detectors[0].overField !== null) { + this.setPopulationField(detectors[0].overField); + this.rareInPopulation = true; + } + if (detectors[0].partitionField !== null) { + this.setSplitField(detectors[0].partitionField); + } + } + } +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts index a3998a000548..902d67b82a9e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts @@ -10,6 +10,7 @@ import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; import { AdvancedJobCreator } from './advanced_job_creator'; import { CategorizationJobCreator } from './categorization_job_creator'; +import { RareJobCreator } from './rare_job_creator'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export type JobCreatorType = @@ -17,7 +18,8 @@ export type JobCreatorType = | MultiMetricJobCreator | PopulationJobCreator | AdvancedJobCreator - | CategorizationJobCreator; + | CategorizationJobCreator + | RareJobCreator; export function isSingleMetricJobCreator( jobCreator: JobCreatorType @@ -46,3 +48,7 @@ export function isCategorizationJobCreator( ): jobCreator is CategorizationJobCreator { return jobCreator.type === JOB_TYPE.CATEGORIZATION; } + +export function isRareJobCreator(jobCreator: JobCreatorType): jobCreator is RareJobCreator { + return jobCreator.type === JOB_TYPE.RARE; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index bab6800c0833..78903e64686f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -311,6 +311,10 @@ export function getJobCreatorTitle(jobCreator: JobCreatorType) { return i18n.translate('xpack.ml.newJob.wizard.jobCreatorTitle.categorization', { defaultMessage: 'Categorization', }); + case JOB_TYPE.RARE: + return i18n.translate('xpack.ml.newJob.wizard.jobCreatorTitle.rare', { + defaultMessage: 'Rare', + }); default: return ''; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx index 8f53e1283faa..36fd27aaba27 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC, useContext, useState } from 'react'; +import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; @@ -33,12 +33,16 @@ const emptyRichDetector: RichDetector = { }; export const AdvancedDetectors: FC = ({ setIsValid }) => { - const { jobCreator: jc, jobCreatorUpdate } = useContext(JobCreatorContext); + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const jobCreator = jc as AdvancedJobCreator; const { fields, aggs } = newJobCapsService; const [modalPayload, setModalPayload] = useState(null); + useEffect(() => { + setIsValid(jobCreator.detectors.length > 0); + }, [jobCreatorUpdated]); + function closeModal() { setModalPayload(null); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index 85083146c137..67673901494c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -14,6 +14,7 @@ import { isMultiMetricJobCreator, isPopulationJobCreator, isAdvancedJobCreator, + isRareJobCreator, } from '../../../../../common/job_creator'; import { ml } from '../../../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../../../contexts/ml'; @@ -45,11 +46,17 @@ export function useEstimateBucketSpan() { indicesOptions: jobCreator.datafeedConfig.indices_options, }; - if ( - (isMultiMetricJobCreator(jobCreator) || isPopulationJobCreator(jobCreator)) && - jobCreator.splitField !== null - ) { + if (isMultiMetricJobCreator(jobCreator) && jobCreator.splitField !== null) { data.splitField = jobCreator.splitField.id; + } else if (isPopulationJobCreator(jobCreator) && jobCreator.populationField !== null) { + data.splitField = jobCreator.populationField.id; + } else if (isRareJobCreator(jobCreator)) { + data.fields = [null]; + if (jobCreator.populationField) { + data.splitField = jobCreator.populationField.id; + } else { + data.splitField = jobCreator.rareField?.id; + } } else if (isAdvancedJobCreator(jobCreator)) { jobCreator.richDetectors.some((d) => { if (d.partitionField !== null) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx similarity index 85% rename from x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx rename to x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx index b197b950bbe2..17577641fe18 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/by_field.tsx @@ -8,14 +8,14 @@ import React, { FC, useContext, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { SplitFieldSelect } from './split_field_select'; +import { SplitFieldSelect } from '../split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; import { newJobCapsService, filterCategoryFields, } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; -import { MultiMetricJobCreator, PopulationJobCreator } from '../../../../../common/job_creator'; +import { PopulationJobCreator } from '../../../../../common/job_creator'; interface Props { detectorIndex: number; @@ -69,18 +69,18 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { ); }; -// remove the split (over) field from the by field options +// remove the population (over) field from the by field options function useFilteredCategoryFields( allCategoryFields: Field[], - jobCreator: MultiMetricJobCreator | PopulationJobCreator, + jobCreator: PopulationJobCreator, jobCreatorUpdated: number ) { const [fields, setFields] = useState(allCategoryFields); useEffect(() => { - const sf = jobCreator.splitField; - if (sf !== null) { - setFields(allCategoryFields.filter((f) => f.name !== sf.name)); + const pf = jobCreator.populationField; + if (pf !== null) { + setFields(allCategoryFields.filter(({ name }) => name !== pf.name)); } else { setFields(allCategoryFields); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/index.ts new file mode 100644 index 000000000000..542a483c374b --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/by_field/index.ts @@ -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 { ByFieldSelector } from './by_field'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx new file mode 100644 index 000000000000..6abbe77fc35a --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx @@ -0,0 +1,32 @@ +/* + * 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, { memo, FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +export const Description: FC = memo(({ children }) => { + const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.populationField.title', { + defaultMessage: 'Population field', + }); + return ( + {title}} + description={ + + } + > + + <>{children} + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/index.ts new file mode 100644 index 000000000000..02e5236fd54e --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/index.ts @@ -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 { PopulationFieldSelector } from './population_field'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx new file mode 100644 index 000000000000..5b69d5b63b53 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/population_field.tsx @@ -0,0 +1,93 @@ +/* + * 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, useContext, useEffect, useState, useMemo } from 'react'; + +import { SplitFieldSelect } from '../split_field_select'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { Field } from '../../../../../../../../../common/types/fields'; +import { + newJobCapsService, + filterCategoryFields, +} from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { Description } from './description'; +import { + PopulationJobCreator, + RareJobCreator, + isPopulationJobCreator, +} from '../../../../../common/job_creator'; + +export const PopulationFieldSelector: FC = () => { + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const jobCreator = jc as PopulationJobCreator | RareJobCreator; + + const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); + const allCategoryFields = useMemo( + () => [...newJobCapsService.categoryFields, ...runtimeCategoryFields], + [] + ); + const categoryFields = useFilteredCategoryFields( + allCategoryFields, + jobCreator, + jobCreatorUpdated + ); + + const [populationField, setPopulationField] = useState(jobCreator.populationField); + + useEffect(() => { + jobCreator.setPopulationField(populationField); + // add the split field to the influencers + if ( + populationField !== null && + jobCreator.influencers.includes(populationField.name) === false + ) { + jobCreator.addInfluencer(populationField.name); + } + jobCreatorUpdate(); + }, [populationField]); + + useEffect(() => { + setPopulationField(jobCreator.populationField); + }, [jobCreatorUpdated]); + + return ( + + + + ); +}; + +// remove the rare (by) field from the by field options in the rare wizard +function useFilteredCategoryFields( + allCategoryFields: Field[], + jobCreator: PopulationJobCreator | RareJobCreator, + jobCreatorUpdated: number +) { + const [fields, setFields] = useState(allCategoryFields); + + useEffect(() => { + if (isPopulationJobCreator(jobCreator)) { + setFields(allCategoryFields); + } else { + const rf = jobCreator.rareField; + const sf = jobCreator.splitField; + if (rf !== null || sf !== null) { + setFields(allCategoryFields.filter(({ name }) => name !== rf?.name && name !== sf?.name)); + } else { + setFields(allCategoryFields); + } + } + }, [jobCreatorUpdated]); + + return fields; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx index 6fa90fa06a1e..1e355b0fb2dd 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx @@ -15,7 +15,7 @@ import { ModelItem, Anomaly } from '../../../../../common/results_loader'; import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SplitCards, useAnimateSplit } from '../split_cards'; import { DetectorTitle } from '../detector_title'; -import { ByFieldSelector } from '../split_field'; +import { ByFieldSelector } from '../by_field'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; type DetectorFieldValues = Record; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 1be487d5b7ee..1f669fea655b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -17,7 +17,7 @@ import { Field, AggFieldPair } from '../../../../../../../../../common/types/fie import { sortFields } from '../../../../../../../../../common/util/fields_utils'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; -import { SplitFieldSelector } from '../split_field'; +import { PopulationFieldSelector } from '../population_field'; import { ChartGrid } from './chart_grid'; import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; @@ -51,7 +51,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { const [end, setEnd] = useState(jobCreator.end); const [bucketSpanMs, setBucketSpanMs] = useState(jobCreator.bucketSpanMs); const [chartSettings, setChartSettings] = useState(defaultChartSettings); - const [splitField, setSplitField] = useState(jobCreator.splitField); + const [populationField, setPopulationField] = useState(jobCreator.populationField); const [fieldValuesPerDetector, setFieldValuesPerDetector] = useState({}); const [byFieldsUpdated, setByFieldsUpdated] = useReducer<(s: number, action: any) => number>( (s) => s + 1, @@ -108,7 +108,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { // if the split field or by fields have changed useEffect(() => { loadCharts(); - }, [JSON.stringify(fieldValuesPerDetector), splitField, pageReady]); + }, [JSON.stringify(fieldValuesPerDetector), populationField, pageReady]); // watch for change in jobCreator useEffect(() => { @@ -123,7 +123,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { loadCharts(); } - setSplitField(jobCreator.splitField); + setPopulationField(jobCreator.populationField); // update by fields and their by fields let update = false; @@ -146,7 +146,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { // changes to fieldValues here will trigger the card effect via setFieldValuesPerDetector useEffect(() => { loadFieldExamples(); - }, [splitField, byFieldsUpdated]); + }, [populationField, byFieldsUpdated]); async function loadCharts() { if (allDataReady()) { @@ -158,7 +158,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { jobCreator.start, jobCreator.end, aggFieldPairList, - jobCreator.splitField, + jobCreator.populationField, cs.intervalMs, jobCreator.runtimeMappings, jobCreator.datafeedConfig.indices_options @@ -225,14 +225,14 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { return ( - - {splitField !== null && } + + {populationField !== null && } - {splitField !== null && ( + {populationField !== null && ( = ({ setIsValid }) => { loading={loadingData} /> )} - {splitField !== null && ( + {populationField !== null && ( { if (allDataReady()) { loadCharts(); } - }, [JSON.stringify(fieldValuesPerDetector), jobCreator.splitField]); + }, [JSON.stringify(fieldValuesPerDetector), jobCreator.populationField]); // watch for changes in split field or by fields. // load example field values // changes to fieldValues here will trigger the card effect via setFieldValuesPerDetector useEffect(() => { loadFieldExamples(); - }, [jobCreator.splitField]); + }, [jobCreator.populationField]); async function loadCharts() { if (allDataReady()) { @@ -76,7 +76,7 @@ export const PopulationDetectorsSummary: FC = () => { jobCreator.start, jobCreator.end, aggFieldPairList, - jobCreator.splitField, + jobCreator.populationField, cs.intervalMs, jobCreator.runtimeMappings, jobCreator.datafeedConfig.indices_options @@ -143,18 +143,18 @@ export const PopulationDetectorsSummary: FC = () => { return ( - {jobCreator.splitField !== null && ( + {jobCreator.populationField !== null && ( void; + isSelected: boolean; +} + +export const RareCard: FC = ({ onClick, isSelected }) => ( + + + + + } + selectable={{ onClick, isSelected }} + /> + +); + +export const RareInPopulationCard: FC = ({ onClick, isSelected }) => ( + + + + + } + selectable={{ onClick, isSelected }} + /> + +); + +export const FrequentlyRareInPopulationCard: FC = ({ onClick, isSelected }) => ( + + + + + } + selectable={{ onClick, isSelected }} + /> + +); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_detector/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_detector/index.ts new file mode 100644 index 000000000000..2e5ca9576652 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_detector/index.ts @@ -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 { RareDetector } from './rare_detector'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_detector/rare_detector.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_detector/rare_detector.tsx new file mode 100644 index 000000000000..a7e5b22d2eac --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_detector/rare_detector.tsx @@ -0,0 +1,90 @@ +/* + * 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, useContext, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { RareJobCreator } from '../../../../../common/job_creator'; +import { RareCard, RareInPopulationCard, FrequentlyRareInPopulationCard } from './detector_cards'; +import { RARE_DETECTOR_TYPE } from '../rare_view'; + +interface Props { + onChange(d: RARE_DETECTOR_TYPE): void; +} + +export const RareDetector: FC = ({ onChange }) => { + const { jobCreator: jc, jobCreatorUpdate } = useContext(JobCreatorContext); + const jobCreator = jc as RareJobCreator; + const [rareDetectorType, setRareDetectorType] = useState(null); + + useEffect(() => { + if (jobCreator.rareField !== null) { + if (jobCreator.populationField === null) { + setRareDetectorType(RARE_DETECTOR_TYPE.RARE); + } else { + setRareDetectorType( + jobCreator.frequentlyRare + ? RARE_DETECTOR_TYPE.FREQ_RARE_POPULATION + : RARE_DETECTOR_TYPE.RARE_POPULATION + ); + } + } else { + setRareDetectorType(RARE_DETECTOR_TYPE.RARE); + } + }, []); + + useEffect(() => { + if (rareDetectorType !== null) { + onChange(rareDetectorType); + if (rareDetectorType === RARE_DETECTOR_TYPE.RARE && jobCreator.populationField !== null) { + jobCreator.removePopulationField(); + } + jobCreator.frequentlyRare = rareDetectorType === RARE_DETECTOR_TYPE.FREQ_RARE_POPULATION; + jobCreatorUpdate(); + } + }, [rareDetectorType]); + + function onRareSelection() { + setRareDetectorType(RARE_DETECTOR_TYPE.RARE); + } + function onRareInPopulationSelection() { + setRareDetectorType(RARE_DETECTOR_TYPE.RARE_POPULATION); + } + function onFreqRareInPopulationSelection() { + setRareDetectorType(RARE_DETECTOR_TYPE.FREQ_RARE_POPULATION); + } + + return ( + <> + +

+ +

+
+ + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx new file mode 100644 index 000000000000..4c3b547d580d --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx @@ -0,0 +1,32 @@ +/* + * 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, { memo, FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +export const Description: FC = memo(({ children }) => { + const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitRareField.title', { + defaultMessage: 'Rare field', + }); + return ( + {title}} + description={ + + } + > + + <>{children} + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/index.ts new file mode 100644 index 000000000000..bd467f22c1c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/index.ts @@ -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 { RareFieldSelector } from './rare_field'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx new file mode 100644 index 000000000000..200d58f3b017 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field.tsx @@ -0,0 +1,81 @@ +/* + * 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, useContext, useEffect, useState, useMemo } from 'react'; + +import { RareFieldSelect } from './rare_field_select'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { + newJobCapsService, + filterCategoryFields, +} from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { Description } from './description'; +import { Field } from '../../../../../../../../../common/types/fields'; +import { RareJobCreator } from '../../../../../common/job_creator'; + +export const RareFieldSelector: FC = () => { + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const jobCreator = jc as RareJobCreator; + + const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); + const allCategoryFields = useMemo( + () => [...newJobCapsService.categoryFields, ...runtimeCategoryFields], + [] + ); + const categoryFields = useFilteredCategoryFields( + allCategoryFields, + jobCreator, + jobCreatorUpdated + ); + + const [rareField, setRareField] = useState(jobCreator.rareField); + + useEffect(() => { + jobCreator.setRareField(rareField); + // add the split field to the influencers + if (rareField !== null && jobCreator.influencers.includes(rareField.name) === false) { + jobCreator.addInfluencer(rareField.name); + } + jobCreatorUpdate(); + }, [rareField]); + + useEffect(() => { + setRareField(jobCreator.rareField); + }, [jobCreatorUpdated]); + + return ( + + + + ); +}; + +// remove the rare (by) field from the by field options in the rare wizard +function useFilteredCategoryFields( + allCategoryFields: Field[], + jobCreator: RareJobCreator, + jobCreatorUpdated: number +) { + const [fields, setFields] = useState(allCategoryFields); + + useEffect(() => { + const pf = jobCreator.populationField; + const sf = jobCreator.splitField; + if (pf !== null || sf !== null) { + setFields(allCategoryFields.filter(({ name }) => name !== pf?.name && name !== sf?.name)); + } else { + setFields(allCategoryFields); + } + }, [jobCreatorUpdated]); + + return fields; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx new file mode 100644 index 000000000000..3868554215d8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { Field, SplitField } from '../../../../../../../../../common/types/fields'; + +interface DropDownLabel { + label: string; + field: Field; +} + +interface Props { + fields: Field[]; + changeHandler(f: SplitField): void; + selectedField: SplitField; + testSubject?: string; + placeholder?: string; +} + +export const RareFieldSelect: FC = ({ + fields, + changeHandler, + selectedField, + testSubject, + placeholder, +}) => { + const options: EuiComboBoxOptionOption[] = fields.map( + (f) => + ({ + label: f.name, + field: f, + } as DropDownLabel) + ); + + const selection: EuiComboBoxOptionOption[] = []; + if (selectedField !== null) { + selection.push({ label: selectedField.name, field: selectedField } as DropDownLabel); + } + + function onChange(selectedOptions: EuiComboBoxOptionOption[]) { + const option = selectedOptions[0] as DropDownLabel; + if (typeof option !== 'undefined') { + changeHandler(option.field); + } else { + changeHandler(null); + } + } + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/detector_description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/detector_description.tsx new file mode 100644 index 000000000000..dcd6d859ff86 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/detector_description.tsx @@ -0,0 +1,112 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, useContext, useEffect, useState } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { RareJobCreator } from '../../../../../common/job_creator'; +import { RARE_DETECTOR_TYPE } from './rare_view'; + +interface Props { + detectorType: RARE_DETECTOR_TYPE; +} + +export const DetectorDescription: FC = ({ detectorType }) => { + const { jobCreator: jc, jobCreatorUpdated } = useContext(JobCreatorContext); + const jobCreator = jc as RareJobCreator; + const [description, setDescription] = useState(null); + + useEffect(() => { + const desc = createDetectorDescription(jobCreator, detectorType); + setDescription(desc); + }, [jobCreatorUpdated]); + + if (description === null) { + return null; + } + + return ( + + +
    + {description.map((d) => ( +
  • {d}
  • + ))} +
+
+ ); +}; + +function createDetectorDescription(jobCreator: RareJobCreator, detectorType: RARE_DETECTOR_TYPE) { + if (jobCreator.rareField === null) { + return null; + } + + const rareFieldName = jobCreator.rareField.id; + const populationFieldName = jobCreator.populationField?.id; + const splitFieldName = jobCreator.splitField?.id; + + const beginningSummary = i18n.translate( + 'xpack.ml.newJob.wizard.pickFieldsStep.rareField.plainText.beginningSummary', + { + defaultMessage: 'detects rare values of {rareFieldName}', + values: { rareFieldName }, + } + ); + + const beginningSummaryFreq = i18n.translate( + 'xpack.ml.newJob.wizard.pickFieldsStep.rareField.plainText.beginningSummaryFreq', + { + defaultMessage: 'detects frequently rare values of {rareFieldName}', + values: { rareFieldName }, + } + ); + + const population = i18n.translate( + 'xpack.ml.newJob.wizard.pickFieldsStep.rareField.plainText.population', + { + defaultMessage: 'compared to the population of {populationFieldName}', + values: { populationFieldName }, + } + ); + + const split = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.rareField.plainText.split', { + defaultMessage: 'for each value of {splitFieldName}', + values: { splitFieldName }, + }); + + const desc = []; + + if (detectorType === RARE_DETECTOR_TYPE.FREQ_RARE_POPULATION) { + desc.push(beginningSummaryFreq); + } else { + desc.push(beginningSummary); + } + + if (populationFieldName !== undefined) { + desc.push(population); + } + + if (splitFieldName !== undefined) { + desc.push(split); + } + + return desc; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/index.ts new file mode 100644 index 000000000000..285adba0a414 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { RareView } from './rare_view'; +export { RARE_DETECTOR_TYPE } from './rare_view'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection.tsx new file mode 100644 index 000000000000..1c1a8e83c478 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection.tsx @@ -0,0 +1,67 @@ +/* + * 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, useContext, useEffect, useState } from 'react'; +import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { RareFieldSelector } from '../rare_field'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { RareJobCreator } from '../../../../../common/job_creator'; +import { RareDetector } from '../rare_detector'; +import { PopulationFieldSelector } from '../population_field'; +import { DetectorDescription } from './detector_description'; +import { RARE_DETECTOR_TYPE } from './rare_view'; + +interface Props { + setIsValid: (na: boolean) => void; + setRareDetectorType(t: RARE_DETECTOR_TYPE): void; + rareDetectorType: RARE_DETECTOR_TYPE; +} + +export const RareDetectors: FC = ({ setIsValid, rareDetectorType, setRareDetectorType }) => { + const { jobCreator: jc, jobCreatorUpdated } = useContext(JobCreatorContext); + const jobCreator = jc as RareJobCreator; + const [detectorValid, setDetectorValid] = useState(false); + + useEffect(() => { + let valid = false; + if (jobCreator.rareField !== null) { + if (rareDetectorType === RARE_DETECTOR_TYPE.RARE) { + // Rare only requires a rare field to be set + valid = true; + } else if (jobCreator.populationField !== null) { + // all others need a need the population field to be set + valid = true; + } + } + setIsValid(valid); + setDetectorValid(valid); + }, [jobCreatorUpdated]); + + return ( + <> + + <> + + + + + + + {rareDetectorType !== RARE_DETECTOR_TYPE.RARE && } + + + {detectorValid && ( + <> + + + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx new file mode 100644 index 000000000000..02566474512e --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx @@ -0,0 +1,88 @@ +/* + * 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, useContext, useEffect, useState } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { RareJobCreator } from '../../../../../common/job_creator'; +import { Results, Anomaly } from '../../../../../common/results_loader'; +import { LineChartPoint } from '../../../../../common/chart_loader'; +import { EventRateChart } from '../../../charts/event_rate_chart'; + +import { RARE_DETECTOR_TYPE } from './rare_view'; +import { DetectorDescription } from './detector_description'; + +const DTR_IDX = 0; +interface Props { + rareDetectorType: RARE_DETECTOR_TYPE; +} + +export const RareDetectorsSummary: FC = ({ rareDetectorType }) => { + const { jobCreator: jc, chartLoader, resultsLoader, chartInterval } = useContext( + JobCreatorContext + ); + const jobCreator = jc as RareJobCreator; + + const [loadingData, setLoadingData] = useState(false); + const [anomalyData, setAnomalyData] = useState([]); + const [eventRateChartData, setEventRateChartData] = useState([]); + const [jobIsRunning, setJobIsRunning] = useState(false); + + function setResultsWrapper(results: Results) { + const anomalies = results.anomalies[DTR_IDX]; + if (anomalies !== undefined) { + setAnomalyData(anomalies); + } + } + + function watchProgress(progress: number) { + setJobIsRunning(progress > 0); + } + + useEffect(() => { + // subscribe to progress and results + const resultsSubscription = resultsLoader.subscribeToResults(setResultsWrapper); + jobCreator.subscribeToProgress(watchProgress); + loadChart(); + return () => { + resultsSubscription.unsubscribe(); + }; + }, []); + + async function loadChart() { + setLoadingData(true); + try { + const resp = await chartLoader.loadEventRateChart( + jobCreator.start, + jobCreator.end, + chartInterval.getInterval().asMilliseconds(), + jobCreator.runtimeMappings ?? undefined, + jobCreator.datafeedConfig.indices_options + ); + setEventRateChartData(resp); + } catch (error) { + setEventRateChartData([]); + } + setLoadingData(false); + } + + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx new file mode 100644 index 000000000000..d67cac8d0fc5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx @@ -0,0 +1,54 @@ +/* + * 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, useEffect, useState } from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import { RareDetectors } from './metric_selection'; +import { RareDetectorsSummary } from './metric_selection_summary'; +import { RareSettings } from './settings'; + +export enum RARE_DETECTOR_TYPE { + RARE, + RARE_POPULATION, + FREQ_RARE_POPULATION, +} + +interface Props { + isActive: boolean; + setCanProceed?: (proceed: boolean) => void; +} + +export const RareView: FC = ({ isActive, setCanProceed }) => { + const [rareFieldValid, setRareFieldValid] = useState(false); + const [settingsValid, setSettingsValid] = useState(false); + const [rareDetectorType, setRareDetectorType] = useState(RARE_DETECTOR_TYPE.RARE); + + useEffect(() => { + if (typeof setCanProceed === 'function') { + setCanProceed(rareFieldValid && settingsValid); + } + }, [rareFieldValid, settingsValid]); + + return isActive === false ? ( + + ) : ( + <> + + {rareFieldValid && ( + <> + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/settings.tsx new file mode 100644 index 000000000000..88bf04322d18 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/settings.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { BucketSpan } from '../bucket_span'; +import { SplitFieldSelector } from '../split_field'; +import { Influencers } from '../influencers'; + +interface Props { + setIsValid: (proceed: boolean) => void; +} + +export const RareSettings: FC = ({ setIsValid }) => { + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx index 9f569de09864..0d5cd8bda3d0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx @@ -10,52 +10,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; - -interface Props { - jobType: JOB_TYPE; -} - -export const Description: FC = memo(({ children, jobType }) => { - if (jobType === JOB_TYPE.MULTI_METRIC) { - const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitField.title', { - defaultMessage: 'Split field', - }); - return ( - {title}} - description={ - - } - > - - <>{children} - - - ); - } else if (jobType === JOB_TYPE.POPULATION) { - const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.populationField.title', { - defaultMessage: 'Population field', - }); - return ( - {title}} - description={ - - } - > - - <>{children} - - - ); - } else { - return null; - } +export const Description: FC = memo(({ children }) => { + const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitField.title', { + defaultMessage: 'Split field', + }); + return ( + {title}} + description={ + + } + > + + <>{children} + + + ); }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts index d8d37e84c2c8..9e1698053f29 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { ByFieldSelector } from './by_field'; export { SplitFieldSelector } from './split_field'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx index 9837fe924fb0..a007a6ee826c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx @@ -7,30 +7,34 @@ import React, { FC, useContext, useEffect, useState, useMemo } from 'react'; -import { SplitFieldSelect } from './split_field_select'; +import { SplitFieldSelect } from '../split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { newJobCapsService, filterCategoryFields, } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { Description } from './description'; +import { Field } from '../../../../../../../../../common/types/fields'; import { MultiMetricJobCreator, + RareJobCreator, isMultiMetricJobCreator, - PopulationJobCreator, - isPopulationJobCreator, } from '../../../../../common/job_creator'; export const SplitFieldSelector: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); - const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator; - const canClearSelection = isMultiMetricJobCreator(jc); + const jobCreator = jc as MultiMetricJobCreator | RareJobCreator; const runtimeCategoryFields = useMemo(() => filterCategoryFields(jobCreator.runtimeFields), []); - const categoryFields = useMemo( + const allCategoryFields = useMemo( () => [...newJobCapsService.categoryFields, ...runtimeCategoryFields], [] ); + const categoryFields = useFilteredCategoryFields( + allCategoryFields, + jobCreator, + jobCreatorUpdated + ); const [splitField, setSplitField] = useState(jobCreator.splitField); useEffect(() => { @@ -47,20 +51,39 @@ export const SplitFieldSelector: FC = () => { }, [jobCreatorUpdated]); return ( - + ); }; + +// remove the rare (by) and population (over) fields from the by field options in the rare wizard +function useFilteredCategoryFields( + allCategoryFields: Field[], + jobCreator: MultiMetricJobCreator | RareJobCreator, + jobCreatorUpdated: number +) { + const [fields, setFields] = useState(allCategoryFields); + + useEffect(() => { + if (isMultiMetricJobCreator(jobCreator)) { + setFields(allCategoryFields); + } else { + const rf = jobCreator.rareField; + const pf = jobCreator.populationField; + if (rf !== null || pf !== null) { + setFields(allCategoryFields.filter(({ name }) => name !== rf?.name && name !== pf?.name)); + } else { + setFields(allCategoryFields); + } + } + }, [jobCreatorUpdated]); + + return fields; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/index.ts new file mode 100644 index 000000000000..216af994ed06 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/index.ts @@ -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 { SplitFieldSelect } from './split_field_select'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx rename to x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index 2d5f5e8a7663..2461cfc9d9d0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -15,6 +15,7 @@ import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; import { AdvancedView } from './components/advanced_view'; import { CategorizationView } from './components/categorization_view'; +import { RareView } from './components/rare_view'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; import { isSingleMetricJobCreator, @@ -22,34 +23,39 @@ import { isPopulationJobCreator, isCategorizationJobCreator, isAdvancedJobCreator, + isRareJobCreator, } from '../../../common/job_creator'; export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); + const [selectionValid, setSelectionValid] = useState(false); useEffect(() => { - setNextActive(jobValidator.isPickFieldsStepValid); - }, [jobValidatorUpdated]); + setNextActive(selectionValid && jobValidator.isPickFieldsStepValid); + }, [jobValidatorUpdated, selectionValid]); return ( {isCurrentStep && ( {isSingleMetricJobCreator(jobCreator) && ( - + )} {isMultiMetricJobCreator(jobCreator) && ( - + )} {isPopulationJobCreator(jobCreator) && ( - + )} {isAdvancedJobCreator(jobCreator) && ( - + )} {isCategorizationJobCreator(jobCreator) && ( - + + )} + {isRareJobCreator(jobCreator) && ( + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx index c1c8c5949692..266c779e1e64 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx @@ -13,6 +13,7 @@ import { MultiMetricView } from '../../../pick_fields_step/components/multi_metr import { PopulationView } from '../../../pick_fields_step/components/population_view'; import { AdvancedView } from '../../../pick_fields_step/components/advanced_view'; import { CategorizationView } from '../../../pick_fields_step/components/categorization_view'; +import { RareView } from '../../../pick_fields_step/components/rare_view'; export const DetectorChart: FC = () => { const { jobCreator } = useContext(JobCreatorContext); @@ -24,6 +25,7 @@ export const DetectorChart: FC = () => { {jobCreator.type === JOB_TYPE.POPULATION && } {jobCreator.type === JOB_TYPE.ADVANCED && } {jobCreator.type === JOB_TYPE.CATEGORIZATION && } + {jobCreator.type === JOB_TYPE.RARE && } ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx index 64f419be7979..f6145ef81298 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx @@ -111,10 +111,10 @@ export const JobDetails: FC = () => { defaultMessage: 'Population field', }), description: - isPopulationJobCreator(jobCreator) && jobCreator.splitField !== null ? ( - jobCreator.splitField.name + isPopulationJobCreator(jobCreator) && jobCreator.populationField !== null ? ( + jobCreator.populationField.name ) : ( - + { }), id: 'mlJobTypeLinkCategorizationJob', }, + { + onClick: () => navigateToPath(`/jobs/new_job/rare${getUrlParams()}`), + icon: { + type: RareIcon, + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.rareAriaLabel', { + defaultMessage: 'Rare job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.rareTitle', { + defaultMessage: 'Rare', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.rareDescription', { + defaultMessage: 'Detect rare values in time series data.', + }), + id: 'mlJobTypeLinkrareJob', + }, ]; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/rare_job_icon.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/rare_job_icon.tsx new file mode 100644 index 000000000000..f7ab7ff14d09 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/rare_job_icon.tsx @@ -0,0 +1,23 @@ +/* + * 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 from 'react'; + +export const RareIcon = ( + + + + +); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index 442bdba717f2..c5d9acce1ee9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -24,6 +24,7 @@ import { jobCreatorFactory, isAdvancedJobCreator, isCategorizationJobCreator, + isRareJobCreator, } from '../../common/job_creator'; import { JOB_TYPE, @@ -171,6 +172,10 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { const { anomaly_detectors: anomalyDetectors } = getNewJobDefaults(); jobCreator.categorizationAnalyzer = anomalyDetectors.categorization_analyzer!; + } else if (isRareJobCreator(jobCreator)) { + const rare = newJobCapsService.getAggById('rare'); + const freqRare = newJobCapsService.getAggById('freq_rare'); + jobCreator.setDefaultDetectorProperties(rare, freqRare); } } diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 726ec328d1cb..56554e79fe95 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -86,6 +86,16 @@ const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: }, ]; +const getRareBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.rareLabel', { + defaultMessage: 'Rare', + }), + href: '', + }, +]; + export const singleMetricRouteFactory = ( navigateToPath: NavigateToPath, basePath: string @@ -131,6 +141,12 @@ export const categorizationRouteFactory = ( breadcrumbs: getCategorizationBreadcrumbs(navigateToPath, basePath), }); +export const rareRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ + path: '/jobs/new_job/rare', + render: (props, deps) => , + breadcrumbs: getRareBreadcrumbs(navigateToPath, basePath), +}); + const PageWrapper: FC = ({ location, jobType, deps }) => { const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE