From 4257a61269eebc7f52ad04a13bb7f8a0d41e7369 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 7 Jan 2020 06:14:32 -0800 Subject: [PATCH] [ML] Support search for partitions on Single Metric Viewer (#53879) * [ML] agg for partition field values * [ML] change api * [ML] load entity values * [ML] check for partition field names * [ML] wip * [ML] refactor api * [ML] debounce input * [ML] remove Record, improve types, fix typo * [ML] jobId as dedicated param, jsdoc comments * [ML] result_type term based on model plot config * [ML] remove redundant criteria for job id * [ML] refactor getPartitionFieldsValues to TS Co-authored-by: Elastic Machine --- .../services/ml_api_service/index.d.ts | 10 +- .../services/ml_api_service/results.js | 13 ++ .../services/results_service/index.ts | 2 + .../results_service/result_service_rx.ts | 35 ++++ .../entity_control/entity_control.js | 120 ------------- .../entity_control/entity_control.tsx | 156 +++++++++++++++++ .../entity_control/{index.js => index.ts} | 0 .../timeseriesexplorer/timeseriesexplorer.js | 107 +++++++----- .../get_partition_fields_values.ts | 161 ++++++++++++++++++ .../models/results_service/results_service.js | 2 + .../ml/server/routes/results_service.js | 20 +++ 11 files changed, 458 insertions(+), 168 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/{index.js => index.ts} (100%) create mode 100644 x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index ad600ad2cbd710..39c69d4bbbeba5 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -16,9 +16,10 @@ import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analyt import { JobMessage } from '../../../../common/types/audit_message'; import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; import { DeepPartial } from '../../../../common/types/common'; +import { PartitionFieldsDefinition } from '../results_service/result_service_rx'; import { annotations } from './annotations'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; -import { CombinedJob } from '../../jobs/new_job/common/job_creator/configs'; +import { CombinedJob, JobId } from '../../jobs/new_job/common/job_creator/configs'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -124,6 +125,13 @@ declare interface Ml { results: { getMaxAnomalyScore: (jobIds: string[], earliestMs: number, latestMs: number) => Promise; + fetchPartitionFieldsValues: ( + jobId: JobId, + searchTerm: Record, + criteriaFields: Array<{ fieldName: string; fieldValue: any }>, + earliestMs: number, + latestMs: number + ) => Observable; }; jobs: { diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index ed01fa268500f2..38ae777106680b 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -75,4 +75,17 @@ export const results = { }, }); }, + + fetchPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs) { + return http$(`${basePath}/results/partition_fields_values`, { + method: 'POST', + body: { + jobId, + searchTerm, + criteriaFields, + earliestMs, + latestMs, + }, + }); + }, }; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts index 9ab14aa7495a70..9d21cbc76ba3a4 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts @@ -9,6 +9,7 @@ import { getModelPlotOutput, getRecordsForCriteria, getScheduledEventsByBucket, + fetchPartitionFieldsValues, } from './result_service_rx'; import { getEventDistributionData, @@ -42,6 +43,7 @@ export const mlResultsService = { getEventDistributionData, getModelPlotOutput, getRecordMaxScoreByTime, + fetchPartitionFieldsValues, }; type time = string; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts index 2341ae15a3378c..299dfe01676947 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -14,7 +14,9 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import _ from 'lodash'; +import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { JobId } from '../../jobs/new_job/common/job_creator/configs'; import { ml } from '../ml_api_service'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; import { CriteriaField } from './index'; @@ -27,6 +29,23 @@ export interface MetricData extends ResultResponse { results: Record; } +export interface FieldDefinition { + /** + * Partition field name. + */ + name: string | number; + /** + * Partitions field distinct values. + */ + values: any[]; +} + +type FieldTypes = 'partition_field' | 'over_field' | 'by_field'; + +export type PartitionFieldsDefinition = { + [field in FieldTypes]: FieldDefinition; +}; + export function getMetricData( index: string, entityFields: any[], @@ -532,3 +551,19 @@ export function getScheduledEventsByBucket( }) ); } + +export function fetchPartitionFieldsValues( + jobId: JobId, + searchTerm: Dictionary, + criteriaFields: Array<{ fieldName: string; fieldValue: any }>, + earliestMs: number, + latestMs: number +) { + return ml.results.fetchPartitionFieldsValues( + jobId, + searchTerm, + criteriaFields, + earliestMs, + latestMs + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js deleted file mode 100644 index 97d6e417927874..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; - -import { EuiComboBox, EuiFlexItem, EuiFormRow, EuiToolTip } from '@elastic/eui'; - -function getEntityControlOptions(entity) { - if (!Array.isArray(entity.fieldValues)) { - return []; - } - - return entity.fieldValues.map(value => { - return { label: value }; - }); -} - -export const EntityControl = injectI18n( - class EntityControl extends React.Component { - static propTypes = { - entity: PropTypes.object.isRequired, - entityFieldValueChanged: PropTypes.func.isRequired, - forceSelection: PropTypes.bool.isRequired, - }; - - state = { - selectedOptions: undefined, - }; - - constructor(props) { - super(props); - } - - componentDidUpdate() { - const { entity, forceSelection } = this.props; - const { selectedOptions } = this.state; - - const fieldValue = entity.fieldValue; - - if ( - (selectedOptions === undefined && fieldValue.length > 0) || - (Array.isArray(selectedOptions) && - fieldValue.length > 0 && - selectedOptions[0].label !== fieldValue) - ) { - this.setState({ - selectedOptions: [{ label: fieldValue }], - }); - } else if (Array.isArray(selectedOptions) && fieldValue.length === 0) { - this.setState({ - selectedOptions: undefined, - }); - } - - if (forceSelection && this.inputRef) { - this.inputRef.focus(); - } - } - - onChange = selectedOptions => { - const options = selectedOptions.length > 0 ? selectedOptions : undefined; - this.setState({ - selectedOptions: options, - }); - - const fieldValue = - Array.isArray(options) && options[0].label.length > 0 ? options[0].label : ''; - this.props.entityFieldValueChanged(this.props.entity, fieldValue); - }; - - render() { - const { entity, intl, forceSelection } = this.props; - const { selectedOptions } = this.state; - const options = getEntityControlOptions(entity); - - const control = ( - { - if (input) { - this.inputRef = input; - } - }} - style={{ minWidth: '300px' }} - placeholder={intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.enterValuePlaceholder', - defaultMessage: 'Enter value', - })} - singleSelection={{ asPlainText: true }} - options={options} - selectedOptions={selectedOptions} - onChange={this.onChange} - isClearable={false} - /> - ); - - const selectMessage = ( - - ); - - return ( - - - - {control} - - - - ); - } - } -); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx new file mode 100644 index 00000000000000..bc6896a1a66ba0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiComboBox, + EuiComboBoxOptionProps, + EuiFlexItem, + EuiFormRow, + EuiToolTip, +} from '@elastic/eui'; + +export interface Entity { + fieldName: string; + fieldValue: any; + fieldValues: any; +} + +function getEntityControlOptions(entity: Entity): EuiComboBoxOptionProps[] { + if (!Array.isArray(entity.fieldValues)) { + return []; + } + + return entity.fieldValues.map(value => { + return { label: value }; + }); +} + +interface EntityControlProps { + entity: Entity; + entityFieldValueChanged: (entity: Entity, fieldValue: any) => void; + onSearchChange: (entity: Entity, queryTerm: string) => void; + forceSelection: boolean; +} + +interface EntityControlState { + selectedOptions: EuiComboBoxOptionProps[] | undefined; + isLoading: boolean; + options: EuiComboBoxOptionProps[] | undefined; +} + +export class EntityControl extends Component { + inputRef: any; + + state = { + selectedOptions: undefined, + options: undefined, + isLoading: false, + }; + + componentDidUpdate(prevProps: EntityControlProps) { + const { entity, forceSelection } = this.props; + const { selectedOptions } = this.state; + + if (prevProps.entity === entity) { + return; + } + + const { fieldValue } = entity; + + const options = getEntityControlOptions(entity); + + let selectedOptionsUpdate: EuiComboBoxOptionProps[] | undefined = selectedOptions; + if ( + (selectedOptions === undefined && fieldValue.length > 0) || + (Array.isArray(selectedOptions) && + // @ts-ignore + selectedOptions[0].label !== fieldValue && + fieldValue.length > 0) + ) { + selectedOptionsUpdate = [{ label: fieldValue }]; + } else if (Array.isArray(selectedOptions) && fieldValue.length === 0) { + selectedOptionsUpdate = undefined; + } + + this.setState({ + options, + isLoading: false, + selectedOptions: selectedOptionsUpdate, + }); + + if (forceSelection && this.inputRef) { + this.inputRef.focus(); + } + } + + onChange = (selectedOptions: EuiComboBoxOptionProps[]) => { + const options = selectedOptions.length > 0 ? selectedOptions : undefined; + this.setState({ + selectedOptions: options, + }); + + const fieldValue = + Array.isArray(options) && options[0].label.length > 0 ? options[0].label : ''; + this.props.entityFieldValueChanged(this.props.entity, fieldValue); + }; + + onSearchChange = (searchValue: string) => { + this.setState({ + isLoading: true, + options: [], + }); + this.props.onSearchChange(this.props.entity, searchValue); + }; + + render() { + const { entity, forceSelection } = this.props; + const { selectedOptions, isLoading, options } = this.state; + + const control = ( + { + if (input) { + this.inputRef = input; + } + }} + style={{ minWidth: '300px' }} + placeholder={i18n.translate('xpack.ml.timeSeriesExplorer.enterValuePlaceholder', { + defaultMessage: 'Enter value', + })} + singleSelection={{ asPlainText: true }} + options={options} + selectedOptions={selectedOptions} + onChange={this.onChange} + onSearchChange={this.onSearchChange} + isClearable={false} + /> + ); + + const selectMessage = ( + + ); + + return ( + + + + {control} + + + + ); + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 0f9ef2b54fdc23..202448340f526d 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,7 +8,7 @@ * React component for rendering Single Metric Viewer. */ -import { chain, difference, each, find, first, get, has, isEqual, without } from 'lodash'; +import { debounce, difference, each, find, first, get, has, isEqual, without } from 'lodash'; import moment from 'moment-timezone'; import { Subject, Subscription, forkJoin } from 'rxjs'; import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; @@ -362,6 +362,12 @@ export class TimeSeriesExplorer extends React.Component { }); }; + entityFieldSearchChanged = debounce((entity, queryTerm) => { + this.loadEntityValues({ + [entity.fieldType]: queryTerm, + }); + }, 500); + loadAnomaliesTableData = (earliestMs, latestMs) => { const { dateFormatTz } = this.props; const { selectedJob } = this.state; @@ -419,58 +425,56 @@ export class TimeSeriesExplorer extends React.Component { ); }; - loadEntityValues = (callback = () => {}) => { + /** + * Loads available entity values. + * @param {Object} searchTerm - Search term for partition, e.g. { partition_field: 'partition' } + * @param callback - Callback to execute after component state update. + */ + loadEntityValues = async (searchTerm = {}, callback = () => {}) => { const { timefilter } = this.props; const { detectorId, entities, selectedJob } = this.state; - // Populate the entity input datalists with the values from the top records by score - // for the selected detector across the full time range. No need to pass through finish(). + // Populate the entity input datalists with aggregated values. No need to pass through finish(). const bounds = timefilter.getActiveBounds(); const detectorIndex = +detectorId; - mlResultsService - .getRecordsForCriteria( - [selectedJob.job_id], - [{ fieldName: 'detector_index', fieldValue: detectorIndex }], - 0, + const { + partition_field: partitionField, + over_field: overField, + by_field: byField, + } = await mlResultsService + .fetchPartitionFieldsValues( + selectedJob.job_id, + searchTerm, + [ + { + fieldName: 'detector_index', + fieldValue: detectorIndex, + }, + ], bounds.min.valueOf(), - bounds.max.valueOf(), - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE + bounds.max.valueOf() ) - .toPromise() - .then(resp => { - if (resp.records && resp.records.length > 0) { - const firstRec = resp.records[0]; + .toPromise(); - this.setState( - { - entities: entities.map(entity => { - const newEntity = { ...entity }; - if (firstRec.partition_field_name === newEntity.fieldName) { - newEntity.fieldValues = chain(resp.records) - .pluck('partition_field_value') - .uniq() - .value(); - } - if (firstRec.over_field_name === newEntity.fieldName) { - newEntity.fieldValues = chain(resp.records) - .pluck('over_field_value') - .uniq() - .value(); - } - if (firstRec.by_field_name === newEntity.fieldName) { - newEntity.fieldValues = chain(resp.records) - .pluck('by_field_value') - .uniq() - .value(); - } - return newEntity; - }), - }, - callback - ); - } - }); + this.setState( + { + entities: entities.map(entity => { + const newEntity = { ...entity }; + if (partitionField?.name === entity.fieldName) { + newEntity.fieldValues = partitionField.values; + } + if (overField?.name === entity.fieldName) { + newEntity.fieldValues = overField.values; + } + if (byField?.name === entity.fieldName) { + newEntity.fieldValues = byField.values; + } + return newEntity; + }), + }, + callback + ); }; loadForForecastId = forecastId => { @@ -796,11 +800,19 @@ export class TimeSeriesExplorer extends React.Component { const byFieldName = get(detector, 'by_field_name'); if (partitionFieldName !== undefined) { const partitionFieldValue = get(entitiesState, partitionFieldName, ''); - entities.push({ fieldName: partitionFieldName, fieldValue: partitionFieldValue }); + entities.push({ + fieldType: 'partition_field', + fieldName: partitionFieldName, + fieldValue: partitionFieldValue, + }); } if (overFieldName !== undefined) { const overFieldValue = get(entitiesState, overFieldName, ''); - entities.push({ fieldName: overFieldName, fieldValue: overFieldValue }); + entities.push({ + fieldType: 'over_field', + fieldName: overFieldName, + fieldValue: overFieldValue, + }); } // For jobs with by and over fields, don't add the 'by' field as this @@ -810,7 +822,7 @@ export class TimeSeriesExplorer extends React.Component { // from filter for the anomaly records. if (byFieldName !== undefined && overFieldName === undefined) { const byFieldValue = get(entitiesState, byFieldName, ''); - entities.push({ fieldName: byFieldName, fieldValue: byFieldValue }); + entities.push({ fieldType: 'by_field', fieldName: byFieldName, fieldValue: byFieldValue }); } this.updateCriteriaFields(detectorIndex, entities); @@ -1338,6 +1350,7 @@ export class TimeSeriesExplorer extends React.Component { diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts new file mode 100644 index 00000000000000..00e3002a7fc71e --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { callWithRequestType } from '../../../common/types/kibana'; + +interface CriteriaField { + fieldName: string; + fieldValue: any; +} + +const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const; + +type PartitionFieldsType = typeof PARTITION_FIELDS[number]; + +type SearchTerm = + | { + [key in PartitionFieldsType]?: string; + } + | undefined; + +/** + * Gets an object for aggregation query to retrieve field name and values. + * @param fieldType - Field type + * @param query - Optional query string for partition value + * @returns {Object} + */ +function getFieldAgg(fieldType: PartitionFieldsType, query?: string) { + const AGG_SIZE = 100; + + const fieldNameKey = `${fieldType}_name`; + const fieldValueKey = `${fieldType}_value`; + + return { + [fieldNameKey]: { + terms: { + field: fieldNameKey, + }, + }, + [fieldValueKey]: { + filter: { + wildcard: { + [fieldValueKey]: { + value: query ? `*${query}*` : '*', + }, + }, + }, + aggs: { + values: { + terms: { + size: AGG_SIZE, + field: fieldValueKey, + }, + }, + }, + }, + }; +} + +/** + * Gets formatted result for particular field from aggregation response. + * @param fieldType - Field type + * @param aggs - Aggregation response + */ +function getFieldObject(fieldType: PartitionFieldsType, aggs: any) { + const fieldNameKey = `${fieldType}_name`; + const fieldValueKey = `${fieldType}_value`; + + return aggs[fieldNameKey].buckets.length > 0 + ? { + [fieldType]: { + name: aggs[fieldNameKey].buckets[0].key, + values: aggs[fieldValueKey].values.buckets.map(({ key }: any) => key), + }, + } + : {}; +} + +export const getPartitionFieldsValuesFactory = (callWithRequest: callWithRequestType) => + /** + * Gets the record of partition fields with possible values that fit the provided queries. + * @param jobId - Job ID + * @param searchTerm - object of queries for partition fields, e.g. { partition_field: 'query' } + * @param criteriaFields - key - value pairs of the term field, e.g. { detector_index: 0 } + * @param earliestMs + * @param latestMs + */ + async function getPartitionFieldsValues( + jobId: string, + searchTerm: SearchTerm = {}, + criteriaFields: CriteriaField[], + earliestMs: number, + latestMs: number + ) { + const jobsResponse = await callWithRequest('ml.jobs', { jobId: [jobId] }); + if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { + throw Boom.notFound(`Job with the id "${jobId}" not found`); + } + + const job = jobsResponse.jobs[0]; + + const isModelPlotEnabled = job?.model_plot_config?.enabled; + + const resp = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + ...criteriaFields.map(({ fieldName, fieldValue }) => { + return { + term: { + [fieldName]: fieldValue, + }, + }; + }), + { + term: { + job_id: jobId, + }, + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + term: { + result_type: isModelPlotEnabled ? 'model_plot' : 'record', + }, + }, + ], + }, + }, + aggs: { + ...PARTITION_FIELDS.reduce((acc, key) => { + return { + ...acc, + ...getFieldAgg(key, searchTerm[key]), + }; + }, {}), + }, + }, + }); + + return PARTITION_FIELDS.reduce((acc, key) => { + return { + ...acc, + ...getFieldObject(key, resp.aggregations), + }; + }, {}); + }; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js b/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js index c779664978f630..3ee5d04186ff10 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js +++ b/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js @@ -10,6 +10,7 @@ import moment from 'moment'; import { buildAnomalyTableItems } from './build_anomaly_table_items'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; +import { getPartitionFieldsValuesFactory } from './get_partition_fields_values'; // Service for carrying out Elasticsearch queries to obtain data for the // ML Results dashboards. @@ -408,5 +409,6 @@ export function resultsServiceProvider(callWithRequest) { getCategoryExamples, getLatestBucketTimestampByJob, getMaxAnomalyScore, + getPartitionFieldsValues: getPartitionFieldsValuesFactory(callWithRequest), }; } diff --git a/x-pack/legacy/plugins/ml/server/routes/results_service.js b/x-pack/legacy/plugins/ml/server/routes/results_service.js index 6d456e6a0412ea..a658729e850831 100644 --- a/x-pack/legacy/plugins/ml/server/routes/results_service.js +++ b/x-pack/legacy/plugins/ml/server/routes/results_service.js @@ -55,6 +55,12 @@ function getMaxAnomalyScore(callWithRequest, payload) { return rs.getMaxAnomalyScore(jobIds, earliestMs, latestMs); } +function getPartitionFieldsValues(callWithRequest, payload) { + const rs = resultsServiceProvider(callWithRequest); + const { jobId, searchTerm, criteriaFields, earliestMs, latestMs } = payload; + return rs.getPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs); +} + export function resultsServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { route({ method: 'POST', @@ -103,4 +109,18 @@ export function resultsServiceRoutes({ commonRouteConfig, elasticsearchPlugin, r ...commonRouteConfig, }, }); + + route({ + method: 'POST', + path: '/api/ml/results/partition_fields_values', + handler(request) { + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); + return getPartitionFieldsValues(callWithRequest, request.payload).catch(resp => + wrapError(resp) + ); + }, + config: { + ...commonRouteConfig, + }, + }); }