[ML] Rare anomaly detection job wizard (#100390)
* [ML] Rare anomaly detection job wizard * fixing fields selection * small improvements * adding event rate chart to summary step * [ML] Changes UI text for rare wizard. * improving detector summary * fixing translations * removing comments * fixing field selection * fixing advanced wizard * updating detector text * fixing bucketspan estimator * bug fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: István Zoltán Szabó <istvan.szabo@elastic.co>
This commit is contained in:
parent
824463ace5
commit
2e00e9c11b
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
@ -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<Props> = ({ 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<ModalPayload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid(jobCreator.detectors.length > 0);
|
||||
}, [jobCreatorUpdated]);
|
||||
|
||||
function closeModal() {
|
||||
setModalPayload(null);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Props> = ({ 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);
|
||||
}
|
|
@ -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';
|
|
@ -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 (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{title}</h3>}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.populationField.description"
|
||||
defaultMessage="All values in the selected field will be modeled together as a population. This analysis type is recommended for high cardinality data."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow label={title}>
|
||||
<>{children}</>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
});
|
|
@ -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';
|
|
@ -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 (
|
||||
<Description>
|
||||
<SplitFieldSelect
|
||||
fields={categoryFields}
|
||||
changeHandler={setPopulationField}
|
||||
selectedField={populationField}
|
||||
isClearable={false}
|
||||
testSubject="mlPopulationSplitFieldSelect"
|
||||
/>
|
||||
</Description>
|
||||
);
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
|
@ -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<number, string[]>;
|
||||
|
|
|
@ -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<Props> = ({ 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<DetectorFieldValues>({});
|
||||
const [byFieldsUpdated, setByFieldsUpdated] = useReducer<(s: number, action: any) => number>(
|
||||
(s) => s + 1,
|
||||
|
@ -108,7 +108,7 @@ export const PopulationDetectors: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ setIsValid }) => {
|
|||
|
||||
return (
|
||||
<Fragment>
|
||||
<SplitFieldSelector />
|
||||
{splitField !== null && <EuiHorizontalRule margin="l" />}
|
||||
<PopulationFieldSelector />
|
||||
{populationField !== null && <EuiHorizontalRule margin="l" />}
|
||||
|
||||
{splitField !== null && (
|
||||
{populationField !== null && (
|
||||
<ChartGrid
|
||||
aggFieldPairList={aggFieldPairList}
|
||||
chartSettings={chartSettings}
|
||||
splitField={splitField}
|
||||
splitField={populationField}
|
||||
lineChartsData={lineChartsData}
|
||||
modelData={[]}
|
||||
anomalyData={[]}
|
||||
|
@ -242,7 +242,7 @@ export const PopulationDetectors: FC<Props> = ({ setIsValid }) => {
|
|||
loading={loadingData}
|
||||
/>
|
||||
)}
|
||||
{splitField !== null && (
|
||||
{populationField !== null && (
|
||||
<MetricSelector
|
||||
fields={fields}
|
||||
detectorChangeHandler={detectorChangeHandler}
|
||||
|
|
|
@ -57,14 +57,14 @@ export const PopulationDetectorsSummary: FC = () => {
|
|||
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 (
|
||||
<Fragment>
|
||||
{jobCreator.splitField !== null && (
|
||||
{jobCreator.populationField !== null && (
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.populationView.splitFieldTitle"
|
||||
defaultMessage="Population split by {field}"
|
||||
values={{ field: jobCreator.splitField.name }}
|
||||
values={{ field: jobCreator.populationField.name }}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<ChartGrid
|
||||
aggFieldPairList={jobCreator.aggFieldPairs}
|
||||
chartSettings={chartSettings}
|
||||
splitField={jobCreator.splitField}
|
||||
splitField={jobCreator.populationField}
|
||||
lineChartsData={lineChartsData}
|
||||
modelData={modelData}
|
||||
anomalyData={anomalyData}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexItem, EuiCard } from '@elastic/eui';
|
||||
|
||||
interface CardProps {
|
||||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export const RareCard: FC<CardProps> = ({ onClick, isSelected }) => (
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
data-test-subj={`mlJobWizardCategorizationDetectorRareCard${isSelected ? ' selected' : ''}`}
|
||||
title={i18n.translate(
|
||||
'xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.rareCard.title',
|
||||
{
|
||||
defaultMessage: 'Rare',
|
||||
}
|
||||
)}
|
||||
description={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.rareCard.description"
|
||||
defaultMessage="Look for distinct rare values in data over time."
|
||||
/>
|
||||
</>
|
||||
}
|
||||
selectable={{ onClick, isSelected }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
export const RareInPopulationCard: FC<CardProps> = ({ onClick, isSelected }) => (
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
data-test-subj={`mlJobWizardCategorizationDetectorRareCard${isSelected ? ' selected' : ''}`}
|
||||
title={i18n.translate(
|
||||
'xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.rarePopulationCard.title',
|
||||
{
|
||||
defaultMessage: 'Rare in population',
|
||||
}
|
||||
)}
|
||||
description={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.rarePopulationCard.description"
|
||||
defaultMessage="Look for rare values in a population."
|
||||
/>
|
||||
</>
|
||||
}
|
||||
selectable={{ onClick, isSelected }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
export const FrequentlyRareInPopulationCard: FC<CardProps> = ({ onClick, isSelected }) => (
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
data-test-subj={`mlJobWizardCategorizationDetectorRareCard${isSelected ? ' selected' : ''}`}
|
||||
title={i18n.translate(
|
||||
'xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.title',
|
||||
{
|
||||
defaultMessage: 'Frequently rare in population',
|
||||
}
|
||||
)}
|
||||
description={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.freqRareCard.description"
|
||||
defaultMessage="Look for frequently rare values in a population."
|
||||
/>
|
||||
</>
|
||||
}
|
||||
selectable={{ onClick, isSelected }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
|
@ -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';
|
|
@ -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<Props> = ({ onChange }) => {
|
||||
const { jobCreator: jc, jobCreatorUpdate } = useContext(JobCreatorContext);
|
||||
const jobCreator = jc as RareJobCreator;
|
||||
const [rareDetectorType, setRareDetectorType] = useState<RARE_DETECTOR_TYPE | null>(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 (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.rareDetectorSelect.title"
|
||||
defaultMessage="Rare detector"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="l" style={{ maxWidth: '824px' }}>
|
||||
<RareCard
|
||||
onClick={onRareSelection}
|
||||
isSelected={rareDetectorType === RARE_DETECTOR_TYPE.RARE}
|
||||
/>
|
||||
<RareInPopulationCard
|
||||
onClick={onRareInPopulationSelection}
|
||||
isSelected={rareDetectorType === RARE_DETECTOR_TYPE.RARE_POPULATION}
|
||||
/>
|
||||
<FrequentlyRareInPopulationCard
|
||||
onClick={onFreqRareInPopulationSelection}
|
||||
isSelected={rareDetectorType === RARE_DETECTOR_TYPE.FREQ_RARE_POPULATION}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{title}</h3>}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.rareField.description"
|
||||
defaultMessage="Select a field in which to detect rare values."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow label={title}>
|
||||
<>{children}</>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
});
|
|
@ -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';
|
|
@ -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 (
|
||||
<Description>
|
||||
<RareFieldSelect
|
||||
fields={categoryFields}
|
||||
changeHandler={setRareField}
|
||||
selectedField={rareField}
|
||||
testSubject="mlRareFieldSelect"
|
||||
/>
|
||||
</Description>
|
||||
);
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
|
@ -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<Props> = ({
|
||||
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 (
|
||||
<EuiComboBox
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={selection}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
data-test-subj={testSubject}
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<Props> = ({ detectorType }) => {
|
||||
const { jobCreator: jc, jobCreatorUpdated } = useContext(JobCreatorContext);
|
||||
const jobCreator = jc as RareJobCreator;
|
||||
const [description, setDescription] = useState<string[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const desc = createDetectorDescription(jobCreator, detectorType);
|
||||
setDescription(desc);
|
||||
}, [jobCreatorUpdated]);
|
||||
|
||||
if (description === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.ml.newJob.wizard.pickFieldsStep.rareField.plainText.calloutTitle',
|
||||
{
|
||||
defaultMessage: 'Detector summary',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.rareField.plainText.title"
|
||||
defaultMessage="This job:"
|
||||
/>
|
||||
<ul>
|
||||
{description.map((d) => (
|
||||
<li>{d}</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
|
@ -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';
|
|
@ -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<Props> = ({ 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 (
|
||||
<>
|
||||
<RareDetector onChange={setRareDetectorType} />
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<RareFieldSelector />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{rareDetectorType !== RARE_DETECTOR_TYPE.RARE && <PopulationFieldSelector />}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{detectorValid && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<DetectorDescription detectorType={rareDetectorType} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<Props> = ({ rareDetectorType }) => {
|
||||
const { jobCreator: jc, chartLoader, resultsLoader, chartInterval } = useContext(
|
||||
JobCreatorContext
|
||||
);
|
||||
const jobCreator = jc as RareJobCreator;
|
||||
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [anomalyData, setAnomalyData] = useState<Anomaly[]>([]);
|
||||
const [eventRateChartData, setEventRateChartData] = useState<LineChartPoint[]>([]);
|
||||
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 (
|
||||
<>
|
||||
<DetectorDescription detectorType={rareDetectorType} />
|
||||
<EuiSpacer size="s" />
|
||||
<EventRateChart
|
||||
eventRateChartData={eventRateChartData}
|
||||
anomalyData={anomalyData}
|
||||
height="300px"
|
||||
width="100%"
|
||||
showAxis={true}
|
||||
loading={loadingData}
|
||||
fadeChart={jobIsRunning}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<Props> = ({ 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 ? (
|
||||
<RareDetectorsSummary rareDetectorType={rareDetectorType} />
|
||||
) : (
|
||||
<>
|
||||
<RareDetectors
|
||||
setIsValid={setRareFieldValid}
|
||||
rareDetectorType={rareDetectorType}
|
||||
setRareDetectorType={setRareDetectorType}
|
||||
/>
|
||||
{rareFieldValid && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="l" />
|
||||
<RareSettings setIsValid={setSettingsValid} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<Props> = ({ setIsValid }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<SplitFieldSelector />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Influencers />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<BucketSpan setIsValid={setIsValid} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<Props> = memo(({ children, jobType }) => {
|
||||
if (jobType === JOB_TYPE.MULTI_METRIC) {
|
||||
const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitField.title', {
|
||||
defaultMessage: 'Split field',
|
||||
});
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{title}</h3>}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.splitField.description"
|
||||
defaultMessage="Select a field to partition analysis by. Each value of this field will be modeled independently individually."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow label={title}>
|
||||
<>{children}</>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
} else if (jobType === JOB_TYPE.POPULATION) {
|
||||
const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.populationField.title', {
|
||||
defaultMessage: 'Population field',
|
||||
});
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{title}</h3>}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.populationField.description"
|
||||
defaultMessage="All values in the selected field will be modeled together as a population. This analysis type is recommended for high cardinality data."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow label={title}>
|
||||
<>{children}</>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
export const Description: FC = memo(({ children }) => {
|
||||
const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitField.title', {
|
||||
defaultMessage: 'Split field',
|
||||
});
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={<h3>{title}</h3>}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.pickFieldsStep.splitField.description"
|
||||
defaultMessage="Select a field to split analysis by. Each value of this field will be modeled independently."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow label={title}>
|
||||
<>{children}</>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ByFieldSelector } from './by_field';
|
||||
export { SplitFieldSelector } from './split_field';
|
||||
|
|
|
@ -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 (
|
||||
<Description jobType={jobCreator.type}>
|
||||
<Description>
|
||||
<SplitFieldSelect
|
||||
fields={categoryFields}
|
||||
changeHandler={setSplitField}
|
||||
selectedField={splitField}
|
||||
isClearable={canClearSelection}
|
||||
testSubject={
|
||||
isMultiMetricJobCreator(jc)
|
||||
? 'mlMultiMetricSplitFieldSelect'
|
||||
: isPopulationJobCreator(jc)
|
||||
? 'mlPopulationSplitFieldSelect'
|
||||
: undefined
|
||||
}
|
||||
isClearable={true}
|
||||
testSubject="mlMultiMetricSplitFieldSelect"
|
||||
/>
|
||||
</Description>
|
||||
);
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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<StepProps> = ({ 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 (
|
||||
<Fragment>
|
||||
{isCurrentStep && (
|
||||
<Fragment>
|
||||
{isSingleMetricJobCreator(jobCreator) && (
|
||||
<SingleMetricView isActive={isCurrentStep} setCanProceed={setNextActive} />
|
||||
<SingleMetricView isActive={isCurrentStep} setCanProceed={setSelectionValid} />
|
||||
)}
|
||||
{isMultiMetricJobCreator(jobCreator) && (
|
||||
<MultiMetricView isActive={isCurrentStep} setCanProceed={setNextActive} />
|
||||
<MultiMetricView isActive={isCurrentStep} setCanProceed={setSelectionValid} />
|
||||
)}
|
||||
{isPopulationJobCreator(jobCreator) && (
|
||||
<PopulationView isActive={isCurrentStep} setCanProceed={setNextActive} />
|
||||
<PopulationView isActive={isCurrentStep} setCanProceed={setSelectionValid} />
|
||||
)}
|
||||
{isAdvancedJobCreator(jobCreator) && (
|
||||
<AdvancedView isActive={isCurrentStep} setCanProceed={setNextActive} />
|
||||
<AdvancedView isActive={isCurrentStep} setCanProceed={setSelectionValid} />
|
||||
)}
|
||||
{isCategorizationJobCreator(jobCreator) && (
|
||||
<CategorizationView isActive={isCurrentStep} setCanProceed={setNextActive} />
|
||||
<CategorizationView isActive={isCurrentStep} setCanProceed={setSelectionValid} />
|
||||
)}
|
||||
{isRareJobCreator(jobCreator) && (
|
||||
<RareView isActive={isCurrentStep} setCanProceed={setSelectionValid} />
|
||||
)}
|
||||
<WizardNav
|
||||
previous={() =>
|
||||
|
|
|
@ -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 && <PopulationView isActive={false} />}
|
||||
{jobCreator.type === JOB_TYPE.ADVANCED && <AdvancedView isActive={false} />}
|
||||
{jobCreator.type === JOB_TYPE.CATEGORIZATION && <CategorizationView isActive={false} />}
|
||||
{jobCreator.type === JOB_TYPE.RARE && <RareView isActive={false} />}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
) : (
|
||||
<span style={{ fontStyle: jobCreator.splitField !== null ? 'inherit' : 'italic' }}>
|
||||
<span style={{ fontStyle: jobCreator.populationField !== null ? 'inherit' : 'italic' }}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.newJob.wizard.summaryStep.jobDetails.populationField.placeholder"
|
||||
defaultMessage="No population field selected"
|
||||
|
|
|
@ -51,6 +51,9 @@ function getWizardUrlFromCloningJob(createdBy: string | undefined, job: Job, dat
|
|||
case CREATED_BY_LABEL.CATEGORIZATION:
|
||||
page = JOB_TYPE.CATEGORIZATION;
|
||||
break;
|
||||
case CREATED_BY_LABEL.RARE:
|
||||
page = JOB_TYPE.RARE;
|
||||
break;
|
||||
default:
|
||||
page = JOB_TYPE.ADVANCED;
|
||||
break;
|
||||
|
|
|
@ -28,6 +28,7 @@ import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed';
|
|||
import { timeBasedIndexCheck } from '../../../../util/index_utils';
|
||||
import { LinkCard } from '../../../../components/link_card';
|
||||
import { CategorizationIcon } from './categorization_job_icon';
|
||||
import { RareIcon } from './rare_job_icon';
|
||||
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
|
||||
import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
|
||||
|
||||
|
@ -176,6 +177,22 @@ export const Page: FC = () => {
|
|||
}),
|
||||
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 (
|
||||
|
|
|
@ -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 = (
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16 32C7.16344 32 0 24.8366 0 16C0 7.16344 7.16344 0 16 0C24.8366 0 32 7.16344 32 16H30C30 8.26801 23.732 2 16 2C8.26801 2 2 8.26801 2 16C2 23.732 8.26801 30 16 30V32Z"
|
||||
fill="#343741"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17 15H23V17H17V23H15V17H9V15H15V9H17V15ZM32 30V32H20V30H32ZM32 22L20 22V24L32 24V22ZM32 26V28H20V26H32Z"
|
||||
fill="#017D73"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -24,6 +24,7 @@ import {
|
|||
jobCreatorFactory,
|
||||
isAdvancedJobCreator,
|
||||
isCategorizationJobCreator,
|
||||
isRareJobCreator,
|
||||
} from '../../common/job_creator';
|
||||
import {
|
||||
JOB_TYPE,
|
||||
|
@ -171,6 +172,10 @@ export const Page: FC<PageProps> = ({ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => <PageWrapper {...props} jobType={JOB_TYPE.RARE} deps={deps} />,
|
||||
breadcrumbs: getRareBreadcrumbs(navigateToPath, basePath),
|
||||
});
|
||||
|
||||
const PageWrapper: FC<WizardPageProps> = ({ location, jobType, deps }) => {
|
||||
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
|
||||
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
|
||||
|
|
Loading…
Reference in a new issue