Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Handle Empty Partition Field Values in Single Metric Viewer #61649

Merged
merged 12 commits into from
Mar 31, 2020
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -68,7 +70,11 @@ export const EntityCell = function EntityCell({
filter,
wrapText = false,
}) {
const valueText = entityName !== 'mlcategory' ? entityValue : `mlcategory ${entityValue}`;
let valueText = entityValue === '' ? <i>{EMPTY_FIELD_VALUE_LABEL}</i> : entityValue;
if (entityName === MLCATEGORY) {
valueText = `${MLCATEGORY} ${valueText}`;
}

const textStyle = { maxWidth: '100%' };
const textWrapperClass = wrapText ? 'field-value-long' : 'field-value-short';
const shouldDisplayIcons =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
}
};
Expand Down Expand Up @@ -376,7 +378,7 @@ export class ExplorerSwimlane extends React.Component {
values: { label: mlEscape(label) },
});
} else {
return mlEscape(label);
return label === '' ? `<i>${EMPTY_FIELD_VALUE_LABEL}</i>` : mlEscape(label);
}
})
.on('click', () => {
Expand All @@ -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',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,15 +30,22 @@ interface EntityControlProps {
isLoading: boolean;
onSearchChange: (entity: Entity, queryTerm: string) => void;
forceSelection: boolean;
options: EuiComboBoxOptionOption[];
options: Array<EuiComboBoxOptionOption<string>>;
}

interface EntityControlState {
selectedOptions: EuiComboBoxOptionOption[] | undefined;
selectedOptions: Array<EuiComboBoxOptionOption<string>> | undefined;
isLoading: boolean;
options: EuiComboBoxOptionOption[] | undefined;
options: Array<EuiComboBoxOptionOption<string>> | undefined;
}

export const EMPTY_FIELD_VALUE_LABEL = i18n.translate(
'xpack.ml.timeSeriesExplorer.emptyPartitionFieldLabel.',
{
defaultMessage: '"" (empty string)',
}
);

export class EntityControl extends Component<EntityControlProps, EntityControlState> {
inputRef: any;

Expand All @@ -53,16 +61,18 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt

const { fieldValue } = entity;

let selectedOptionsUpdate: EuiComboBoxOptionOption[] | undefined = selectedOptions;
let selectedOptionsUpdate: Array<EuiComboBoxOptionOption<string>> | 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;
}

Expand All @@ -84,14 +94,14 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
}
}

onChange = (selectedOptions: EuiComboBoxOptionOption[]) => {
onChange = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
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);
};

Expand All @@ -103,6 +113,11 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
this.props.onSearchChange(this.props.entity, searchValue);
};

renderOption = (option: EuiSelectableOption) => {
const { label } = option;
return label === EMPTY_FIELD_VALUE_LABEL ? <i>{label}</i> : label;
};

render() {
const { entity, forceSelection } = this.props;
const { isLoading, options, selectedOptions } = this.state;
Expand All @@ -126,6 +141,7 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
onChange={this.onChange}
onSearchChange={this.onSearchChange}
isClearable={false}
renderOption={this.renderOption}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 };
});
}

Expand Down Expand Up @@ -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);
};

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -739,15 +740,15 @@ 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,
fieldValue: partitionFieldValue,
});
}
if (overFieldName !== undefined) {
const overFieldValue = get(entitiesState, overFieldName, '');
const overFieldValue = get(entitiesState, overFieldName, null);
entities.push({
fieldType: 'over_field',
fieldName: overFieldName,
Expand All @@ -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 });
}

Expand All @@ -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',
Expand Down Expand Up @@ -1150,7 +1151,7 @@ export class TimeSeriesExplorer extends React.Component {
</EuiFlexItem>
{entityControls.map(entity => {
const entityKey = `${entity.fieldName}`;
const forceSelection = !hasEmptyFieldValues && !entity.fieldValue;
const forceSelection = !hasEmptyFieldValues && entity.fieldValue === null;
hasEmptyFieldValues = !hasEmptyFieldValues && forceSelection;
return (
<EntityControl
Expand Down
Loading