diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts index c029394798463..35f9dea4135e0 100644 --- a/x-pack/plugins/ml/common/constants/new_job.ts +++ b/x-pack/plugins/ml/common/constants/new_job.ts @@ -12,6 +12,7 @@ export enum JOB_TYPE { ADVANCED = 'advanced', CATEGORIZATION = 'categorization', RARE = 'rare', + GEO = 'geo', } export enum CREATED_BY_LABEL { @@ -20,6 +21,7 @@ export enum CREATED_BY_LABEL { POPULATION = 'population-wizard', CATEGORIZATION = 'categorization-wizard', RARE = 'rare-wizard', + GEO = 'geo-wizard', APM_TRANSACTION = 'ml-module-apm-transaction', SINGLE_METRIC_FROM_LENS = 'single-metric-wizard-from-lens', MULTI_METRIC_FROM_LENS = 'multi-metric-wizard-from-lens', diff --git a/x-pack/plugins/ml/common/util/fields_utils.ts b/x-pack/plugins/ml/common/util/fields_utils.ts index d037f722e2fd2..53deb351df160 100644 --- a/x-pack/plugins/ml/common/util/fields_utils.ts +++ b/x-pack/plugins/ml/common/util/fields_utils.ts @@ -127,7 +127,7 @@ function getNumericalFields(fields: Field[]): Field[] { ); } -function getGeoFields(fields: Field[]): Field[] { +export function getGeoFields(fields: Field[]): Field[] { return fields.filter( (f) => f.type === ES_FIELD_TYPES.GEO_POINT || f.type === ES_FIELD_TYPES.GEO_SHAPE ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts index e544f65c09188..6ab5a04fdde65 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -36,7 +36,7 @@ function getAnomalyFeatures( }, properties: { record_score: Math.floor(anomaly.record_score), - [type]: coordinates.map((point: number) => point.toFixed(2)), + [type]: coordinates.map((point: number) => Number(point.toFixed(2))), }, }); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index 3214a9c91bda9..9e7e7df0710e5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -8,7 +8,6 @@ import memoizeOne from 'memoize-one'; import { isEqual } from 'lodash'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { IndexPatternTitle } from '../../../../../../common/types/kibana'; import { IndicesOptions } from '../../../../../../common/types/anomaly_detection_jobs'; import { Field, @@ -32,7 +31,7 @@ export type LineChartData = Record; const eq = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs); export class ChartLoader { - private _indexPatternTitle: IndexPatternTitle = ''; + protected _dataView: DataView; private _timeFieldName: string = ''; private _query: object = {}; @@ -42,7 +41,7 @@ export class ChartLoader { private _getCategoryFields = memoizeOne(getCategoryFieldsOrig, eq); constructor(indexPattern: DataView, query: object) { - this._indexPatternTitle = indexPattern.title; + this._dataView = indexPattern; this._query = query; if (typeof indexPattern.timeFieldName === 'string') { @@ -70,7 +69,7 @@ export class ChartLoader { const aggFieldPairNames = aggFieldPairs.map(getAggFieldPairNames); const resp = await this._newJobLineChart( - this._indexPatternTitle, + this._dataView.getIndexPattern(), this._timeFieldName, start, end, @@ -107,7 +106,7 @@ export class ChartLoader { const aggFieldPairNames = aggFieldPairs.map(getAggFieldPairNames); const resp = await this._newJobPopulationsChart( - this._indexPatternTitle, + this._dataView.getIndexPattern(), this._timeFieldName, start, end, @@ -133,7 +132,7 @@ export class ChartLoader { ): Promise { if (this._timeFieldName !== '') { const resp = await this._getEventRateData( - this._indexPatternTitle, + this._dataView.getIndexPattern(), this._query, this._timeFieldName, start, @@ -160,7 +159,7 @@ export class ChartLoader { indicesOptions?: IndicesOptions ): Promise { const { results } = await this._getCategoryFields( - this._indexPatternTitle, + this._dataView.getIndexPattern(), field.name, 10, this._query, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts new file mode 100644 index 0000000000000..b86d884e766ae --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataView } from '@kbn/data-views-plugin/public'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; +import { JobCreator } from './job_creator'; +import { + Field, + Aggregation, + SplitField, + AggFieldPair, +} from '../../../../../../common/types/fields'; +import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; +import { createBasicDetector } from './util/default_configs'; +import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; +import { getRichDetectors } from './util/general'; +import { isSparseDataJob } from './util/general'; + +export class GeoJobCreator extends JobCreator { + private _geoField: Field | null = null; + private _geoAgg: Aggregation | null = null; + // set partitionField as the default split field for geo jobs + private _splitField: SplitField = null; + + protected _type: JOB_TYPE = JOB_TYPE.GEO; + + constructor(indexPattern: DataView, savedSearch: SavedSearchSavedObject | null, query: object) { + super(indexPattern, savedSearch, query); + this.createdBy = CREATED_BY_LABEL.GEO; + this._wizardInitialized$.next(true); + } + + public setDefaultDetectorProperties(geo: Aggregation | null) { + if (geo === null) { + throw Error('lat_long aggregations missing'); + } + this._geoAgg = geo; + } + + public get geoField() { + return this._geoField; + } + + public get geoAgg() { + return this._geoAgg; + } + + public setGeoField(field: Field | null) { + this._geoField = field; + + if (field === null) { + this.removeSplitField(); + this._removeDetector(0); + this._detectors.length = 0; + this._fields.length = 0; + return; + } + + const agg = this._geoAgg!; + + this.removeAllDetectors(); + const dtr = this._createDetector(agg, field); + this._addDetector(dtr, agg, field); + } + + // set the split field + 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; + } + + // create a new detector object, applying the overall split field + private _createDetector(agg: Aggregation, field: Field) { + const dtr: Detector = createBasicDetector(agg, field); + + if (this._splitField !== null) { + dtr.partition_field_name = this._splitField.id; + } + return dtr; + } + + public get aggFieldPairs(): AggFieldPair[] { + return this.detectors.map((d, i) => ({ + field: this._fields[i], + agg: this._aggs[i], + })); + } + + public cloneFromExistingJob(job: Job, datafeed: Datafeed) { + this._overrideConfigs(job, datafeed); + this.createdBy = CREATED_BY_LABEL.GEO; + this._sparseData = isSparseDataJob(job, datafeed); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); + + this.removeSplitField(); + this.removeAllDetectors(); + this.removeAllDetectors(); + + if (detectors.length) { + this.setGeoField(detectors[0].field); + 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/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts index ccc26a3e0b6ae..dee935928f8b0 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 @@ -12,6 +12,7 @@ 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 { GeoJobCreator } from './geo_job_creator'; export type { JobCreatorType } from './type_guards'; export { isSingleMetricJobCreator, @@ -20,5 +21,6 @@ export { isAdvancedJobCreator, isCategorizationJobCreator, isRareJobCreator, + isGeoJobCreator, } 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_factory.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts index cf4b0be8711f3..3cabc397af026 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 @@ -13,6 +13,7 @@ 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 { GeoJobCreator } from './geo_job_creator'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; @@ -39,6 +40,9 @@ export const jobCreatorFactory = case JOB_TYPE.RARE: jc = RareJobCreator; break; + case JOB_TYPE.GEO: + jc = GeoJobCreator; + break; default: jc = SingleMetricJobCreator; break; 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 902d67b82a9e3..2dbe8f5447f61 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 @@ -11,6 +11,7 @@ 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 { GeoJobCreator } from './geo_job_creator'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export type JobCreatorType = @@ -19,7 +20,8 @@ export type JobCreatorType = | PopulationJobCreator | AdvancedJobCreator | CategorizationJobCreator - | RareJobCreator; + | RareJobCreator + | GeoJobCreator; export function isSingleMetricJobCreator( jobCreator: JobCreatorType @@ -52,3 +54,7 @@ export function isCategorizationJobCreator( export function isRareJobCreator(jobCreator: JobCreatorType): jobCreator is RareJobCreator { return jobCreator.type === JOB_TYPE.RARE; } + +export function isGeoJobCreator(jobCreator: JobCreatorType): jobCreator is GeoJobCreator { + return jobCreator.type === JOB_TYPE.GEO; +} 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 5d48d86a5f1b2..1c8bf04b23931 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 @@ -323,6 +323,10 @@ export function getJobCreatorTitle(jobCreator: JobCreatorType) { return i18n.translate('xpack.ml.newJob.wizard.jobCreatorTitle.rare', { defaultMessage: 'Rare', }); + case JOB_TYPE.GEO: + return i18n.translate('xpack.ml.newJob.wizard.jobCreatorTitle.geo', { + defaultMessage: 'Geo', + }); default: return ''; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/index.ts new file mode 100644 index 0000000000000..ca44d9e590e3f --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/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 { MapLoader } from './map_loader'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts new file mode 100644 index 0000000000000..d0fdd1e6a5562 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/map_loader/map_loader.ts @@ -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 memoizeOne from 'memoize-one'; +import { isEqual } from 'lodash'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '@kbn/maps-plugin/common'; +import type { MapsStartApi } from '@kbn/maps-plugin/public'; +import { ChartLoader } from '../chart_loader'; +import { Field, SplitField } from '../../../../../../common/types/fields'; +const eq = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs); + +export class MapLoader extends ChartLoader { + private _getMapData; + + constructor(indexPattern: DataView, query: object, mapsPlugin: MapsStartApi | undefined) { + super(indexPattern, query); + + this._getMapData = mapsPlugin + ? memoizeOne(mapsPlugin.createLayerDescriptors.createESSearchSourceLayerDescriptor, eq) + : null; + } + + async getMapLayersForGeoJob( + geoField: Field, + splitField: SplitField, + fieldValues: string[], + filters?: any[] + ) { + const layerList: LayerDescriptor[] = []; + if (this._dataView.id !== undefined && geoField) { + const params: any = { + indexPatternId: this._dataView.id, + geoFieldName: geoField.name, + geoFieldType: geoField.type as unknown as ES_GEO_FIELD_TYPE, + filters: filters ?? [], + ...(fieldValues.length && splitField + ? { query: { query: `${splitField.name}:${fieldValues[0]}`, language: 'kuery' } } + : {}), + }; + + const searchLayerDescriptor = this._getMapData ? await this._getMapData(params) : null; + + if (searchLayerDescriptor) { + layerList.push(searchLayerDescriptor); + } + } + return layerList; + } +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts index cc75f6db868fd..5e3051f37d0c2 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts @@ -10,6 +10,7 @@ import { Field, Aggregation } from '../../../../../../common/types/fields'; import { TimeBuckets } from '../../../../util/time_buckets'; import { JobCreatorType, SingleMetricJobCreator } from '../../common/job_creator'; import { ChartLoader } from '../../common/chart_loader'; +import { MapLoader } from '../../common/map_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; @@ -19,6 +20,7 @@ export interface JobCreatorContextValue { jobCreatorUpdate: () => void; jobCreator: JobCreatorType; chartLoader: ChartLoader; + mapLoader: MapLoader; resultsLoader: ResultsLoader; chartInterval: TimeBuckets; jobValidator: JobValidator; @@ -33,6 +35,7 @@ export const JobCreatorContext = createContext({ jobCreatorUpdate: () => {}, jobCreator: {} as SingleMetricJobCreator, chartLoader: {} as ChartLoader, + mapLoader: {} as MapLoader, resultsLoader: {} as ResultsLoader, chartInterval: {} as TimeBuckets, jobValidator: {} as JobValidator, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/description.tsx new file mode 100644 index 0000000000000..1ffa50389622d --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_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.geoField.title', { + defaultMessage: 'Geo field', + }); + return ( + {title}} + description={ + + } + > + + <>{children} + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx new file mode 100644 index 0000000000000..e2525921afe7c --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field.tsx @@ -0,0 +1,40 @@ +/* + * 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 { GeoFieldSelect } from './geo_field_select'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities/new_job_capabilities_service'; +import { GeoJobCreator } from '../../../../../common/job_creator'; +import { Description } from './description'; + +export const GeoField: FC = () => { + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const jobCreator = jc as GeoJobCreator; + const { geoFields } = newJobCapsService; + const [geoField, setGeoField] = useState(jobCreator.geoField); + + useEffect(() => { + jobCreator.setGeoField(geoField); + jobCreatorUpdate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [geoField]); + + useEffect(() => { + if (jobCreator.geoField) { + setGeoField(jobCreator.geoField); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobCreatorUpdated]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx new file mode 100644 index 0000000000000..9c0ee8c023300 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_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, useCallback, useMemo } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { Field } from '../../../../../../../../../common/types/fields'; + +interface DropDownLabel { + label: string; + field: Field; +} + +interface Props { + fields: Field[]; + changeHandler(i: Field | null): void; + selectedField: Field | null; +} + +export const GeoFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { + const options: EuiComboBoxOptionOption[] = useMemo( + () => + fields.map( + (f) => + ({ + label: f.name, + field: f, + } as DropDownLabel) + ), + [fields] + ); + + const selection: EuiComboBoxOptionOption[] = useMemo(() => { + const selectedOptions: EuiComboBoxOptionOption[] = []; + if (selectedField !== null) { + selectedOptions.push({ label: selectedField.name, field: selectedField } as DropDownLabel); + } + return selectedOptions; + }, [selectedField]); + + const onChange = useCallback( + (selectedOptions: EuiComboBoxOptionOption[]) => { + const option = selectedOptions[0] as DropDownLabel; + if (typeof option !== 'undefined') { + changeHandler(option.field); + } else { + changeHandler(null); + } + }, + [changeHandler] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/index.ts new file mode 100644 index 0000000000000..b95be845010f9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_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 { GeoField } from './geo_field'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/geo_map_examples.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/geo_map_examples.tsx new file mode 100644 index 0000000000000..a4ba8c64f464c --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/geo_map_examples.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { LayerDescriptor } from '@kbn/maps-plugin/common'; +import { SplitCards, useAnimateSplit } from '../split_cards'; +import { MlEmbeddedMapComponent } from '../../../../../../../components/ml_embedded_map'; +import { Aggregation, Field, SplitField } from '../../../../../../../../../common/types/fields'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; +import { DetectorTitle } from '../detector_title'; + +interface Props { + dataViewId?: string; + geoField: Field | null; + splitField: SplitField; + fieldValues: string[]; + geoAgg: Aggregation | null; + layerList: LayerDescriptor[]; +} + +export const GeoMapExamples: FC = ({ + geoField, + splitField, + fieldValues, + geoAgg, + layerList, +}) => { + const animateSplit = useAnimateSplit(); + + return ( + + + + <> + {geoAgg && geoField ? : null} + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/geo_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/geo_view.tsx new file mode 100644 index 0000000000000..f9a01d99cbc23 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/geo_view.tsx @@ -0,0 +1,49 @@ +/* + * 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, { Fragment, FC, useEffect, useState } from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import { GeoDetector } from './metric_selection'; +import { GeoDetectorsSummary } from './metric_selection_summary'; +import { GeoSettings } from './settings'; + +interface Props { + isActive: boolean; + setCanProceed?: (proceed: boolean) => void; +} + +export const GeoView: FC = ({ isActive, setCanProceed }) => { + const [geoFieldValid, setGeoFieldValid] = useState(false); + const [settingsValid, setSettingsValid] = useState(false); + + useEffect(() => { + if (typeof setCanProceed === 'function') { + setCanProceed(geoFieldValid && settingsValid); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [geoFieldValid, settingsValid]); + + return ( + + {isActive === false ? ( + + ) : ( + + + + {geoFieldValid && ( + + + + + )} + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/index.ts new file mode 100644 index 0000000000000..42203f5f4ac46 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/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 { GeoView } from './geo_view'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/metric_selection.tsx new file mode 100644 index 0000000000000..11a1d203bff98 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/metric_selection.tsx @@ -0,0 +1,105 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { LayerDescriptor } from '@kbn/maps-plugin/common'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { GeoJobCreator } from '../../../../../common/job_creator'; +import { GeoField } from '../geo_field'; +import { GeoMapExamples } from './geo_map_examples'; +import { useMlKibana } from '../../../../../../../contexts/kibana'; + +interface Props { + setIsValid: (na: boolean) => void; +} + +export const GeoDetector: FC = ({ setIsValid }) => { + const { jobCreator: jc, jobCreatorUpdated, chartLoader } = useContext(JobCreatorContext); + const jobCreator = jc as GeoJobCreator; + + const [fieldValues, setFieldValues] = useState([]); + const [layerList, setLayerList] = useState([]); + + const { + services: { data, notifications: toasts }, + } = useMlKibana(); + const { mapLoader } = useContext(JobCreatorContext); + + useEffect(() => { + let valid = false; + if (jobCreator.geoField !== null) { + valid = true; + } + setIsValid(valid); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobCreatorUpdated]); + + // Load example field values when split field changes + // changes to fieldValues here will trigger the card effect + useEffect(() => { + if (jobCreator.splitField !== null) { + chartLoader + .loadFieldExampleValues( + jobCreator.splitField, + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options + ) + .then(setFieldValues) + .catch((error) => { + // @ts-ignore + toasts.addDanger({ + title: i18n.translate('xpack.ml.newJob.geoWizard.fieldValuesFetchErrorTitle', { + defaultMessage: 'Error fetching field example values: {error}', + values: { error }, + }), + }); + }); + } else { + setFieldValues([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobCreator.splitField]); + + // Update the layer list with updated geo points upon refresh + useEffect(() => { + async function getMapLayersForGeoJob() { + if (jobCreator.geoField) { + const filters = data.query.filterManager.getFilters() ?? []; + const layers = await mapLoader.getMapLayersForGeoJob( + jobCreator.geoField, + jobCreator.splitField, + fieldValues, + filters + ); + setLayerList(layers); + } + } + getMapLayersForGeoJob(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobCreator.geoField, jobCreator.splitField, fieldValues]); + + return ( + <> + {jobCreator.geoField !== null && ( + <> + + + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/metric_selection_summary.tsx new file mode 100644 index 0000000000000..f8ad3171af577 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/metric_selection_summary.tsx @@ -0,0 +1,83 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { LayerDescriptor } from '@kbn/maps-plugin/common'; +import { GeoJobCreator } from '../../../../../common/job_creator'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { useMlKibana } from '../../../../../../../contexts/kibana'; +import { GeoMapExamples } from './geo_map_examples'; + +export const GeoDetectorsSummary: FC = () => { + const [layerList, setLayerList] = useState([]); + const [fieldValues, setFieldValues] = useState([]); + + const { jobCreator: jc, chartLoader, mapLoader } = useContext(JobCreatorContext); + const jobCreator = jc as GeoJobCreator; + const geoField = jobCreator.geoField; + const splitField = jobCreator.splitField; + + const { + services: { data, notifications }, + } = useMlKibana(); + + // Load example field values for summary view + // changes to fieldValues here will trigger the card effect + useEffect(() => { + if (splitField !== null) { + chartLoader + .loadFieldExampleValues( + splitField, + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options + ) + .then(setFieldValues) + .catch((error) => { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.ml.newJob.geoWizard.fieldValuesFetchErrorTitle', { + defaultMessage: 'Error fetching field example values: {error}', + values: { error }, + }), + }); + }); + } else { + setFieldValues([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Update the layer list once the example fields have been + useEffect(() => { + async function getMapLayersForGeoJob() { + if (geoField) { + const filters = data.query.filterManager.getFilters() ?? []; + const layers = await mapLoader.getMapLayersForGeoJob( + geoField, + splitField, + fieldValues, + filters + ); + setLayerList(layers); + } + } + getMapLayersForGeoJob(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fieldValues]); + + if (jobCreator.geoField === null) return null; + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_view/settings.tsx new file mode 100644 index 0000000000000..d5ad0488cec2f --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_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 GeoSettings: FC = ({ setIsValid }) => { + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index 597645d2fa87e..6df9ba9f26c6b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -94,7 +94,7 @@ export const SplitCards: FC = memo( {(fieldValues.length === 0 || numberOfDetectors === 0) && <>{children}} {fieldValues.length > 0 && numberOfDetectors > 0 && splitField !== null && ( - {jobType === JOB_TYPE.MULTI_METRIC && ( + {(jobType === JOB_TYPE.MULTI_METRIC || jobType === JOB_TYPE.GEO) && (
= ({ setCurrentStep, isCurrentStep }) => { @@ -57,6 +59,9 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) {isRareJobCreator(jobCreator) && ( )} + {isGeoJobCreator(jobCreator) && ( + + )} setCurrentStep( 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 266c779e1e644..6e19177a78471 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 @@ -14,6 +14,7 @@ import { PopulationView } from '../../../pick_fields_step/components/population_ 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'; +import { GeoView } from '../../../pick_fields_step/components/geo_view'; export const DetectorChart: FC = () => { const { jobCreator } = useContext(JobCreatorContext); @@ -26,6 +27,7 @@ export const DetectorChart: FC = () => { {jobCreator.type === JOB_TYPE.ADVANCED && } {jobCreator.type === JOB_TYPE.CATEGORIZATION && } {jobCreator.type === JOB_TYPE.RARE && } + {jobCreator.type === JOB_TYPE.GEO && } ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 1f8e6247a4fa8..a3e608454df01 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -60,6 +60,9 @@ async function getWizardUrlFromCloningJob(createdBy: string | undefined, dataVie case CREATED_BY_LABEL.RARE: page = JOB_TYPE.RARE; break; + case CREATED_BY_LABEL.GEO: + page = JOB_TYPE.GEO; + break; default: page = JOB_TYPE.ADVANCED; break; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/geo_job_icon.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/geo_job_icon.tsx new file mode 100644 index 0000000000000..d3296b65200bf --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/geo_job_icon.tsx @@ -0,0 +1,30 @@ +/* + * 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 GeoIcon = ( + + + + + + + + + + + +); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 93e968f6da2a8..b80f946d56d5c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTitle, @@ -17,6 +17,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; import { useMlContext } from '../../../../contexts/ml'; @@ -28,6 +29,7 @@ import { LinkCard } from '../../../../components/link_card'; import { CategorizationIcon } from './categorization_job_icon'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../../common/constants/locator'; import { RareIcon } from './rare_job_icon'; +import { GeoIcon } from './geo_job_icon'; import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; import { MlPageHeader } from '../../../../components/page_header'; @@ -47,6 +49,14 @@ export const Page: FC = () => { const { currentSavedSearch, currentDataView } = mlContext; const isTimeBasedIndex = timeBasedIndexCheck(currentDataView); + const hasGeoFields = useMemo( + () => + [ + ...currentDataView.fields.getByType(ES_FIELD_TYPES.GEO_POINT), + ...currentDataView.fields.getByType(ES_FIELD_TYPES.GEO_SHAPE), + ].length > 0, + [currentDataView] + ); const indexWarningTitle = !isTimeBasedIndex && isSavedSearchSavedObject(currentSavedSearch) ? i18n.translate( @@ -212,6 +222,25 @@ export const Page: FC = () => { }, ]; + if (hasGeoFields) { + jobTypes.push({ + onClick: () => navigateToPath(`/jobs/new_job/geo${getUrlParams()}`), + icon: { + type: GeoIcon, + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.geoAriaLabel', { + defaultMessage: 'Geo job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.geoTitle', { + defaultMessage: 'Geo', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.geoDescription', { + defaultMessage: 'Detect anomalies in the geographic location of the input data.', + }), + id: 'mlJobTypeLinkGeoJob', + }); + } + return (
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 7650391973c65..daf04edaba4ed 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 @@ -21,6 +21,7 @@ import { isAdvancedJobCreator, isCategorizationJobCreator, isRareJobCreator, + isGeoJobCreator, } from '../../common/job_creator'; import { JOB_TYPE, @@ -28,9 +29,11 @@ import { DEFAULT_BUCKET_SPAN, } from '../../../../../../common/constants/new_job'; import { ChartLoader } from '../../common/chart_loader'; +import { MapLoader } from '../../common/map_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; import { useMlContext } from '../../../../contexts/ml'; +import { useMlKibana } from '../../../../contexts/kibana'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service'; import { newJobCapsService } from '../../../../services/new_job_capabilities/new_job_capabilities_service'; @@ -50,6 +53,9 @@ export interface PageProps { export const Page: FC = ({ existingJobsAndGroups, jobType }) => { const mlContext = useMlContext(); + const { + services: { maps: mapsPlugin }, + } = useMlKibana(); const chartInterval = useTimeBuckets(); @@ -184,6 +190,9 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { const rare = newJobCapsService.getAggById('rare'); const freqRare = newJobCapsService.getAggById('freq_rare'); jobCreator.setDefaultDetectorProperties(rare, freqRare); + } else if (isGeoJobCreator(jobCreator)) { + const geo = newJobCapsService.getAggById('lat_long'); + jobCreator.setDefaultDetectorProperties(geo); } } @@ -196,6 +205,11 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { [mlContext.currentDataView, jobCreator.query] ); + const mapLoader = useMemo( + () => new MapLoader(mlContext.currentDataView, jobCreator.query, mapsPlugin), + [mlContext.currentDataView, jobCreator.query, mapsPlugin] + ); + const resultsLoader = useMemo( () => new ResultsLoader(jobCreator, chartInterval, chartLoader), [jobCreator, chartInterval, chartLoader] @@ -230,6 +244,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { = ({ jobCreator, chartLoader, + mapLoader, resultsLoader, chartInterval, jobValidator, @@ -57,6 +60,7 @@ export const Wizard: FC = ({ jobCreatorUpdate, jobCreator, chartLoader, + mapLoader, resultsLoader, chartInterval, jobValidator, 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 16012970d9fdd..2f0ef09e06cf6 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 @@ -96,6 +96,16 @@ const getRareBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => }, ]; +const getGeoBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.geoLabel', { + defaultMessage: 'Geo', + }), + href: '', + }, +]; + export const singleMetricRouteFactory = ( navigateToPath: NavigateToPath, basePath: string @@ -161,6 +171,12 @@ export const rareRouteFactory = (navigateToPath: NavigateToPath, basePath: strin breadcrumbs: getRareBreadcrumbs(navigateToPath, basePath), }); +export const geoRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ + path: '/jobs/new_job/geo', + render: (props, deps) => , + breadcrumbs: getGeoBreadcrumbs(navigateToPath, basePath), +}); + const PageWrapper: FC = ({ location, jobType, deps }) => { const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts index bd49d7cd0a9e1..12602c042958d 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts @@ -8,13 +8,14 @@ import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, AggId, FieldId } from '../../../../common/types/fields'; import { EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; -import { filterCategoryFields } from '../../../../common/util/fields_utils'; +import { getGeoFields, filterCategoryFields } from '../../../../common/util/fields_utils'; import { ml } from '../ml_api_service'; import { processTextAndKeywordFields, NewJobCapabilitiesServiceBase } from './new_job_capabilities'; class NewJobCapsService extends NewJobCapabilitiesServiceBase { private _catFields: Field[] = []; private _dateFields: Field[] = []; + private _geoFields: Field[] = []; private _includeEventRateField: boolean = true; private _removeTextFields: boolean = true; @@ -26,6 +27,10 @@ class NewJobCapsService extends NewJobCapabilitiesServiceBase { return this._dateFields; } + public get geoFields(): Field[] { + return this._geoFields; + } + public get categoryFields(): Field[] { return filterCategoryFields(this._fields); } @@ -52,6 +57,7 @@ class NewJobCapsService extends NewJobCapabilitiesServiceBase { (f) => f.type === ES_FIELD_TYPES.KEYWORD || f.type === ES_FIELD_TYPES.TEXT ); const dateFields = fieldsPreferringText.filter((f) => f.type === ES_FIELD_TYPES.DATE); + const geoFields = getGeoFields(allFields); const fields = this._removeTextFields ? fieldsPreferringKeyword : allFields; // set the main fields list to contain fields which have been filtered to prefer @@ -61,6 +67,7 @@ class NewJobCapsService extends NewJobCapabilitiesServiceBase { // set the category fields to contain fields which have been filtered to prefer text fields. this._catFields = catFields; this._dateFields = dateFields; + this._geoFields = geoFields; this._aggs = aggs; } catch (error) { console.error('Unable to load new job capabilities', error); // eslint-disable-line no-console