diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js
index 02a9e569f28a4..d3917412bfb7b 100644
--- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js
+++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js
@@ -10,6 +10,8 @@ import React from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control';
+import { MLCATEGORY } from '../../../../common/constants/field_types';
function getAddFilter({ entityName, entityValue, filter }) {
return (
@@ -68,7 +70,11 @@ export const EntityCell = function EntityCell({
filter,
wrapText = false,
}) {
- const valueText = entityName !== 'mlcategory' ? entityValue : `mlcategory ${entityValue}`;
+ let valueText = entityValue === '' ? {EMPTY_FIELD_VALUE_LABEL} : entityValue;
+ if (entityName === MLCATEGORY) {
+ valueText = `${MLCATEGORY} ${valueText}`;
+ }
+
const textStyle = { maxWidth: '100%' };
const textWrapperClass = wrapText ? 'field-value-long' : 'field-value-short';
const shouldDisplayIcons =
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js
index e8cb8377a656d..d7333f00c89cd 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js
@@ -25,6 +25,7 @@ import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip
import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service';
import { DRAG_SELECT_ACTION } from './explorer_constants';
import { i18n } from '@kbn/i18n';
+import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control';
const SCSS = {
mlDragselectDragging: 'mlDragselectDragging',
@@ -309,6 +310,7 @@ export class ExplorerSwimlane extends React.Component {
return function(lane) {
const bucketScore = getBucketScore(lane, time);
if (bucketScore !== 0) {
+ lane = lane === '' ? EMPTY_FIELD_VALUE_LABEL : lane;
cellMouseover(this, lane, bucketScore, i, time);
}
};
@@ -376,7 +378,7 @@ export class ExplorerSwimlane extends React.Component {
values: { label: mlEscape(label) },
});
} else {
- return mlEscape(label);
+ return label === '' ? `${EMPTY_FIELD_VALUE_LABEL}` : mlEscape(label);
}
})
.on('click', () => {
@@ -393,7 +395,7 @@ export class ExplorerSwimlane extends React.Component {
{ skipHeader: true },
{
label: swimlaneData.fieldName,
- value,
+ value: value === '' ? EMPTY_FIELD_VALUE_LABEL : value,
seriesIdentifier: { key: value },
valueAccessor: 'fieldName',
},
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index 4dec066a7f325..b7aa5edc88638 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -1259,39 +1259,13 @@ export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, lates
},
{ term: { job_id: jobId } },
];
- const shouldCriteria = [];
_.each(criteriaFields, criteria => {
- if (criteria.fieldValue.length !== 0) {
- mustCriteria.push({
- term: {
- [criteria.fieldName]: criteria.fieldValue,
- },
- });
- } else {
- // Add special handling for blank entity field values, checking for either
- // an empty string or the field not existing.
- const emptyFieldCondition = {
- bool: {
- must: [
- {
- term: {},
- },
- ],
- },
- };
- emptyFieldCondition.bool.must[0].term[criteria.fieldName] = '';
- shouldCriteria.push(emptyFieldCondition);
- shouldCriteria.push({
- bool: {
- must_not: [
- {
- exists: { field: criteria.fieldName },
- },
- ],
- },
- });
- }
+ mustCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
});
ml.esSearch({
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx
index 8911ed53e74d0..7bb0b27472c88 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx
@@ -16,6 +16,7 @@ import {
EuiFormRow,
EuiToolTip,
} from '@elastic/eui';
+import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
export interface Entity {
fieldName: string;
@@ -29,15 +30,22 @@ interface EntityControlProps {
isLoading: boolean;
onSearchChange: (entity: Entity, queryTerm: string) => void;
forceSelection: boolean;
- options: EuiComboBoxOptionOption[];
+ options: Array>;
}
interface EntityControlState {
- selectedOptions: EuiComboBoxOptionOption[] | undefined;
+ selectedOptions: Array> | undefined;
isLoading: boolean;
- options: EuiComboBoxOptionOption[] | undefined;
+ options: Array> | undefined;
}
+export const EMPTY_FIELD_VALUE_LABEL = i18n.translate(
+ 'xpack.ml.timeSeriesExplorer.emptyPartitionFieldLabel.',
+ {
+ defaultMessage: '"" (empty string)',
+ }
+);
+
export class EntityControl extends Component {
inputRef: any;
@@ -53,16 +61,18 @@ export class EntityControl extends Component> | undefined = selectedOptions;
if (
- (selectedOptions === undefined && fieldValue.length > 0) ||
+ (selectedOptions === undefined && fieldValue !== null) ||
(Array.isArray(selectedOptions) &&
// @ts-ignore
- selectedOptions[0].label !== fieldValue &&
- fieldValue.length > 0)
+ selectedOptions[0].value !== fieldValue &&
+ fieldValue !== null)
) {
- selectedOptionsUpdate = [{ label: fieldValue }];
- } else if (Array.isArray(selectedOptions) && fieldValue.length === 0) {
+ selectedOptionsUpdate = [
+ { label: fieldValue === '' ? EMPTY_FIELD_VALUE_LABEL : fieldValue, value: fieldValue },
+ ];
+ } else if (Array.isArray(selectedOptions) && fieldValue === null) {
selectedOptionsUpdate = undefined;
}
@@ -84,14 +94,14 @@ export class EntityControl extends Component {
+ onChange = (selectedOptions: Array>) => {
const options = selectedOptions.length > 0 ? selectedOptions : undefined;
this.setState({
selectedOptions: options,
});
const fieldValue =
- Array.isArray(options) && options[0].label.length > 0 ? options[0].label : '';
+ Array.isArray(options) && options[0].value !== null ? options[0].value : null;
this.props.entityFieldValueChanged(this.props.entity, fieldValue);
};
@@ -103,6 +113,11 @@ export class EntityControl extends Component {
+ const { label } = option;
+ return label === EMPTY_FIELD_VALUE_LABEL ? {label} : label;
+ };
+
render() {
const { entity, forceSelection } = this.props;
const { isLoading, options, selectedOptions } = this.state;
@@ -126,6 +141,7 @@ export class EntityControl extends Component
);
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
index db5ff2ad91910..f973d41ad7754 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
@@ -128,7 +128,7 @@ function getChartDetails(
obj.results.functionLabel = functionLabel;
const blankEntityFields = _.filter(entityFields, entity => {
- return entity.fieldValue.length === 0;
+ return entity.fieldValue === null;
});
// Look to see if any of the entity fields have defined values
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index 1a26540709f34..5e505757dd2aa 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -78,6 +78,7 @@ import {
processRecordScoreResults,
getFocusData,
} from './timeseriesexplorer_utils';
+import { EMPTY_FIELD_VALUE_LABEL } from './components/entity_control/entity_control';
// Used to indicate the chart is being plotted across
// all partition field values, where the cardinality of the field cannot be
@@ -94,7 +95,7 @@ function getEntityControlOptions(fieldValues) {
fieldValues.sort();
return fieldValues.map(value => {
- return { label: value };
+ return { label: value === '' ? EMPTY_FIELD_VALUE_LABEL : value, value };
});
}
@@ -192,7 +193,7 @@ export class TimeSeriesExplorer extends React.Component {
getFieldNamesWithEmptyValues = () => {
const latestEntityControls = this.getControlsForDetector();
return latestEntityControls
- .filter(({ fieldValue }) => !fieldValue)
+ .filter(({ fieldValue }) => fieldValue === null)
.map(({ fieldName }) => fieldName);
};
@@ -249,7 +250,7 @@ export class TimeSeriesExplorer extends React.Component {
if (operator === '+' && entity.fieldValue !== value) {
resultValue = value;
} else if (operator === '-' && entity.fieldValue === value) {
- resultValue = '';
+ resultValue = null;
} else {
return;
}
@@ -302,7 +303,7 @@ export class TimeSeriesExplorer extends React.Component {
focusAggregationInterval,
selectedForecastId,
modelPlotEnabled,
- entityControls.filter(entity => entity.fieldValue.length > 0),
+ entityControls.filter(entity => entity.fieldValue !== null),
searchBounds,
selectedJob,
TIME_FIELD_NAME
@@ -576,7 +577,7 @@ export class TimeSeriesExplorer extends React.Component {
};
const nonBlankEntities = entityControls.filter(entity => {
- return entity.fieldValue.length > 0;
+ return entity.fieldValue !== null;
});
if (
@@ -739,7 +740,7 @@ export class TimeSeriesExplorer extends React.Component {
const overFieldName = get(detector, 'over_field_name');
const byFieldName = get(detector, 'by_field_name');
if (partitionFieldName !== undefined) {
- const partitionFieldValue = get(entitiesState, partitionFieldName, '');
+ const partitionFieldValue = get(entitiesState, partitionFieldName, null);
entities.push({
fieldType: 'partition_field',
fieldName: partitionFieldName,
@@ -747,7 +748,7 @@ export class TimeSeriesExplorer extends React.Component {
});
}
if (overFieldName !== undefined) {
- const overFieldValue = get(entitiesState, overFieldName, '');
+ const overFieldValue = get(entitiesState, overFieldName, null);
entities.push({
fieldType: 'over_field',
fieldName: overFieldName,
@@ -761,7 +762,7 @@ export class TimeSeriesExplorer extends React.Component {
// TODO - metric data can be filtered by this field, so should only exclude
// from filter for the anomaly records.
if (byFieldName !== undefined && overFieldName === undefined) {
- const byFieldValue = get(entitiesState, byFieldName, '');
+ const byFieldValue = get(entitiesState, byFieldName, null);
entities.push({ fieldType: 'by_field', fieldName: byFieldName, fieldValue: byFieldValue });
}
@@ -775,7 +776,7 @@ export class TimeSeriesExplorer extends React.Component {
*/
getCriteriaFields(detectorIndex, entities) {
// Only filter on the entity if the field has a value.
- const nonBlankEntities = entities.filter(entity => entity.fieldValue.length > 0);
+ const nonBlankEntities = entities.filter(entity => entity.fieldValue !== null);
return [
{
fieldName: 'detector_index',
@@ -1150,7 +1151,7 @@ export class TimeSeriesExplorer extends React.Component {
{entityControls.map(entity => {
const entityKey = `${entity.fieldName}`;
- const forceSelection = !hasEmptyFieldValues && !entity.fieldValue;
+ const forceSelection = !hasEmptyFieldValues && entity.fieldValue === null;
hasEmptyFieldValues = !hasEmptyFieldValues && forceSelection;
return (
{
if (!interval) {
- throw new Error('Interval is required to retrieve max bucket cardinalities.');
+ throw Boom.badRequest('Interval is required to retrieve max bucket cardinalities.');
}
const aggregatableFields = await getAggregatableFields(index, fieldNames);
@@ -243,12 +260,17 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) {
return {};
}
+ const { start, end } = getSafeTimeRangeForInterval(
+ interval,
+ ...Object.values(getSafeTimeRange(earliestMs, latestMs))
+ );
+
const cachedValues =
fieldsAggsCache.getValues(
index,
timeFieldName,
- earliestMs,
- latestMs,
+ start,
+ end,
'maxBucketCardinality',
fieldNames
) ?? {};
@@ -260,8 +282,6 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) {
return cachedValues;
}
- const { start, end } = getSafeTimeRange(earliestMs, latestMs, interval);
-
const mustCriteria = [
{
range: {
@@ -334,6 +354,10 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) {
return obj;
}, {} as { [field: string]: number });
+ fieldsAggsCache.updateValues(index, timeFieldName, start, end, {
+ maxBucketCardinality: aggResult,
+ });
+
return {
...cachedValues,
...aggResult,