Skip to content

Commit 55fd8cd

Browse files
[ML] Support search for partitions on Single Metric Viewer (#53879) (#54140)
* [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 <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 7374f79 commit 55fd8cd

File tree

11 files changed

+458
-168
lines changed

11 files changed

+458
-168
lines changed

x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analyt
1616
import { JobMessage } from '../../../../common/types/audit_message';
1717
import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics';
1818
import { DeepPartial } from '../../../../common/types/common';
19+
import { PartitionFieldsDefinition } from '../results_service/result_service_rx';
1920
import { annotations } from './annotations';
2021
import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars';
21-
import { CombinedJob } from '../../jobs/new_job/common/job_creator/configs';
22+
import { CombinedJob, JobId } from '../../jobs/new_job/common/job_creator/configs';
2223

2324
// TODO This is not a complete representation of all methods of `ml.*`.
2425
// It just satisfies needs for other parts of the code area which use
@@ -122,6 +123,13 @@ declare interface Ml {
122123

123124
results: {
124125
getMaxAnomalyScore: (jobIds: string[], earliestMs: number, latestMs: number) => Promise<any>;
126+
fetchPartitionFieldsValues: (
127+
jobId: JobId,
128+
searchTerm: Record<string, string>,
129+
criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
130+
earliestMs: number,
131+
latestMs: number
132+
) => Observable<PartitionFieldsDefinition>;
125133
};
126134

127135
jobs: {

x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,17 @@ export const results = {
7575
},
7676
});
7777
},
78+
79+
fetchPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs) {
80+
return http$(`${basePath}/results/partition_fields_values`, {
81+
method: 'POST',
82+
body: {
83+
jobId,
84+
searchTerm,
85+
criteriaFields,
86+
earliestMs,
87+
latestMs,
88+
},
89+
});
90+
},
7891
};

x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getModelPlotOutput,
1010
getRecordsForCriteria,
1111
getScheduledEventsByBucket,
12+
fetchPartitionFieldsValues,
1213
} from './result_service_rx';
1314
import {
1415
getEventDistributionData,
@@ -42,6 +43,7 @@ export const mlResultsService = {
4243
getEventDistributionData,
4344
getModelPlotOutput,
4445
getRecordMaxScoreByTime,
46+
fetchPartitionFieldsValues,
4547
};
4648

4749
type time = string;

x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
import { Observable } from 'rxjs';
1515
import { map } from 'rxjs/operators';
1616
import _ from 'lodash';
17+
import { Dictionary } from '../../../../common/types/common';
1718
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
19+
import { JobId } from '../../jobs/new_job/common/job_creator/configs';
1820
import { ml } from '../ml_api_service';
1921
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
2022
import { CriteriaField } from './index';
@@ -27,6 +29,23 @@ export interface MetricData extends ResultResponse {
2729
results: Record<string, any>;
2830
}
2931

32+
export interface FieldDefinition {
33+
/**
34+
* Partition field name.
35+
*/
36+
name: string | number;
37+
/**
38+
* Partitions field distinct values.
39+
*/
40+
values: any[];
41+
}
42+
43+
type FieldTypes = 'partition_field' | 'over_field' | 'by_field';
44+
45+
export type PartitionFieldsDefinition = {
46+
[field in FieldTypes]: FieldDefinition;
47+
};
48+
3049
export function getMetricData(
3150
index: string,
3251
entityFields: any[],
@@ -532,3 +551,19 @@ export function getScheduledEventsByBucket(
532551
})
533552
);
534553
}
554+
555+
export function fetchPartitionFieldsValues(
556+
jobId: JobId,
557+
searchTerm: Dictionary<string>,
558+
criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
559+
earliestMs: number,
560+
latestMs: number
561+
) {
562+
return ml.results.fetchPartitionFieldsValues(
563+
jobId,
564+
searchTerm,
565+
criteriaFields,
566+
earliestMs,
567+
latestMs
568+
);
569+
}

x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js

Lines changed: 0 additions & 120 deletions
This file was deleted.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { Component } from 'react';
8+
import { FormattedMessage } from '@kbn/i18n/react';
9+
import { i18n } from '@kbn/i18n';
10+
11+
import {
12+
EuiComboBox,
13+
EuiComboBoxOptionProps,
14+
EuiFlexItem,
15+
EuiFormRow,
16+
EuiToolTip,
17+
} from '@elastic/eui';
18+
19+
export interface Entity {
20+
fieldName: string;
21+
fieldValue: any;
22+
fieldValues: any;
23+
}
24+
25+
function getEntityControlOptions(entity: Entity): EuiComboBoxOptionProps[] {
26+
if (!Array.isArray(entity.fieldValues)) {
27+
return [];
28+
}
29+
30+
return entity.fieldValues.map(value => {
31+
return { label: value };
32+
});
33+
}
34+
35+
interface EntityControlProps {
36+
entity: Entity;
37+
entityFieldValueChanged: (entity: Entity, fieldValue: any) => void;
38+
onSearchChange: (entity: Entity, queryTerm: string) => void;
39+
forceSelection: boolean;
40+
}
41+
42+
interface EntityControlState {
43+
selectedOptions: EuiComboBoxOptionProps[] | undefined;
44+
isLoading: boolean;
45+
options: EuiComboBoxOptionProps[] | undefined;
46+
}
47+
48+
export class EntityControl extends Component<EntityControlProps, EntityControlState> {
49+
inputRef: any;
50+
51+
state = {
52+
selectedOptions: undefined,
53+
options: undefined,
54+
isLoading: false,
55+
};
56+
57+
componentDidUpdate(prevProps: EntityControlProps) {
58+
const { entity, forceSelection } = this.props;
59+
const { selectedOptions } = this.state;
60+
61+
if (prevProps.entity === entity) {
62+
return;
63+
}
64+
65+
const { fieldValue } = entity;
66+
67+
const options = getEntityControlOptions(entity);
68+
69+
let selectedOptionsUpdate: EuiComboBoxOptionProps[] | undefined = selectedOptions;
70+
if (
71+
(selectedOptions === undefined && fieldValue.length > 0) ||
72+
(Array.isArray(selectedOptions) &&
73+
// @ts-ignore
74+
selectedOptions[0].label !== fieldValue &&
75+
fieldValue.length > 0)
76+
) {
77+
selectedOptionsUpdate = [{ label: fieldValue }];
78+
} else if (Array.isArray(selectedOptions) && fieldValue.length === 0) {
79+
selectedOptionsUpdate = undefined;
80+
}
81+
82+
this.setState({
83+
options,
84+
isLoading: false,
85+
selectedOptions: selectedOptionsUpdate,
86+
});
87+
88+
if (forceSelection && this.inputRef) {
89+
this.inputRef.focus();
90+
}
91+
}
92+
93+
onChange = (selectedOptions: EuiComboBoxOptionProps[]) => {
94+
const options = selectedOptions.length > 0 ? selectedOptions : undefined;
95+
this.setState({
96+
selectedOptions: options,
97+
});
98+
99+
const fieldValue =
100+
Array.isArray(options) && options[0].label.length > 0 ? options[0].label : '';
101+
this.props.entityFieldValueChanged(this.props.entity, fieldValue);
102+
};
103+
104+
onSearchChange = (searchValue: string) => {
105+
this.setState({
106+
isLoading: true,
107+
options: [],
108+
});
109+
this.props.onSearchChange(this.props.entity, searchValue);
110+
};
111+
112+
render() {
113+
const { entity, forceSelection } = this.props;
114+
const { selectedOptions, isLoading, options } = this.state;
115+
116+
const control = (
117+
<EuiComboBox
118+
async
119+
isLoading={isLoading}
120+
inputRef={input => {
121+
if (input) {
122+
this.inputRef = input;
123+
}
124+
}}
125+
style={{ minWidth: '300px' }}
126+
placeholder={i18n.translate('xpack.ml.timeSeriesExplorer.enterValuePlaceholder', {
127+
defaultMessage: 'Enter value',
128+
})}
129+
singleSelection={{ asPlainText: true }}
130+
options={options}
131+
selectedOptions={selectedOptions}
132+
onChange={this.onChange}
133+
onSearchChange={this.onSearchChange}
134+
isClearable={false}
135+
/>
136+
);
137+
138+
const selectMessage = (
139+
<FormattedMessage
140+
id="xpack.ml.timeSeriesExplorer.selectFieldMessage"
141+
defaultMessage="Select {fieldName}"
142+
values={{ fieldName: entity.fieldName }}
143+
/>
144+
);
145+
146+
return (
147+
<EuiFlexItem grow={false}>
148+
<EuiFormRow label={entity.fieldName} helpText={forceSelection ? selectMessage : null}>
149+
<EuiToolTip position="right" content={forceSelection ? selectMessage : null}>
150+
{control}
151+
</EuiToolTip>
152+
</EuiFormRow>
153+
</EuiFlexItem>
154+
);
155+
}
156+
}

0 commit comments

Comments
 (0)