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] Add better UI support for runtime fields Transforms #90363

Merged
merged 44 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7dc3db9
[ML] Add RT support for transforms from index pattern
qn895 Feb 2, 2021
717eaae
[ML] Add support for cloned transform from api
qn895 Feb 3, 2021
008ceab
[ML] Add support for runtime pivot
qn895 Feb 3, 2021
b35e56a
[ML] Add support for api created runtime
qn895 Feb 4, 2021
52a47c3
[ML] Add preview for expanded row
qn895 Feb 4, 2021
e40e94b
[ML] Add runtime fields to dropdown options
qn895 Feb 4, 2021
f1c8052
[ML] Add runtime fields to latest
qn895 Feb 4, 2021
cc4831c
[ML] Fix duplicate columns
qn895 Feb 4, 2021
7995e66
[ML] Update types and test
qn895 Feb 4, 2021
f84ebf3
[ML] Add runtime mappings to index pattern on creation
qn895 Feb 5, 2021
0e1c429
[ML] Add callout to show unsupported fields in dfa
qn895 Feb 5, 2021
9018492
[ML] Update types to RuntimeField
qn895 Feb 5, 2021
f8f8bd5
Merge remote-tracking branch 'upstream/master' into ml-transforms-df-…
qn895 Feb 5, 2021
3cfed18
[ML] Fix runtime fields, remove runtime mappings, fix copy to console
qn895 Feb 8, 2021
b123f88
Merge remote-tracking branch 'upstream/master' into ml-transforms-df-…
qn895 Feb 8, 2021
5db60f1
[ML] Fix incompatible kbn field type
qn895 Feb 8, 2021
fb94187
[ML] Add advanced mappings editor
qn895 Feb 8, 2021
aee8a23
[ML] Add support for filter terms agg control
qn895 Feb 9, 2021
28281c4
[ML] Fix jest tests hanging
qn895 Feb 9, 2021
4f68ebf
[ML] Fix translations
qn895 Feb 9, 2021
e8bec3e
[ML] Fix over-sized buttons for filter range
qn895 Feb 10, 2021
e4e2fb0
Merge remote-tracking branch 'upstream/master' into ml-transforms-df-…
qn895 Feb 10, 2021
341aabb
[ML] Update runtime mappings schema
qn895 Feb 11, 2021
3d3935b
[ML] Update runtime mappings schema
qn895 Feb 11, 2021
08e635e
[ML] Use isRecord for object checks
qn895 Feb 14, 2021
8adf58f
[ML] Fix and more message
qn895 Feb 14, 2021
29778b2
[ML] Update schema to correctly match types
qn895 Feb 14, 2021
5cd5cd1
[ML] Update schema to correctly match types
qn895 Feb 14, 2021
ba04871
[ML] Fix pivot duplicates
qn895 Feb 14, 2021
211937a
Merge remote-tracking branch 'upstream/master' into ml-transforms-df-…
qn895 Feb 14, 2021
bc43513
[ML] Rename isRecord to isPopulatedObject
qn895 Feb 14, 2021
76c9c79
[ML] Remove fit-content
qn895 Feb 14, 2021
2bd6b3c
[ML] Update runtime field type to prevent potential conflicts
qn895 Feb 14, 2021
ad32ea3
Revert "[ML] Remove fit-content"
qn895 Feb 14, 2021
9323382
[ML] Remove misc comment
qn895 Feb 14, 2021
860c26f
[ML] Fix missing typeof
qn895 Feb 15, 2021
7b91ae0
[ML] Add sorts and constants
qn895 Feb 16, 2021
af2e8e4
Merge upstream/master into branch
qn895 Feb 16, 2021
07563e9
[ML] Add i18n to includedFields description
qn895 Feb 16, 2021
8712080
[ML] fix imports
qn895 Feb 16, 2021
4ef577c
Merge upstream/master into branch
qn895 Feb 16, 2021
04219b2
[ML] Only pass runtime mappings if it's latest
qn895 Feb 16, 2021
f23a747
[ML] Fix functional tests
qn895 Feb 16, 2021
f77f6a6
Merge remote-tracking branch 'upstream/master' into ml-transforms-df-…
qn895 Feb 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions x-pack/plugins/ml/common/types/feature_importance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { isPopulatedObject } from '../util/object_utils';

export type FeatureImportanceClassName = string | number | boolean;

export interface ClassFeatureImportance {
Expand Down Expand Up @@ -87,7 +89,7 @@ export function isClassificationFeatureImportanceBaseline(
baselineData: any
): baselineData is ClassificationFeatureImportanceBaseline {
return (
typeof baselineData === 'object' &&
isPopulatedObject(baselineData) &&
baselineData.hasOwnProperty('classes') &&
Array.isArray(baselineData.classes)
);
Expand All @@ -96,5 +98,5 @@ export function isClassificationFeatureImportanceBaseline(
export function isRegressionFeatureImportanceBaseline(
baselineData: any
): baselineData is RegressionFeatureImportanceBaseline {
return typeof baselineData === 'object' && baselineData.hasOwnProperty('baseline');
return isPopulatedObject(baselineData) && baselineData.hasOwnProperty('baseline');
}
15 changes: 15 additions & 0 deletions x-pack/plugins/ml/common/types/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,18 @@ export interface ScriptAggCardinality {
export interface AggCardinality {
cardinality: FieldAggCardinality | ScriptAggCardinality;
}

// Replace this with import once #88995 is merged
const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];

export interface RuntimeField {
type: RuntimeType;
script:
| string
| {
source: string;
};
}

export type RuntimeMappings = Record<string, RuntimeField>;
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/common/util/datafeed_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const getDatafeedAggregations = (
};

export const getAggregationBucketsName = (aggregations: any): string | undefined => {
if (typeof aggregations === 'object') {
if (aggregations !== null && typeof aggregations === 'object') {
const keys = Object.keys(aggregations);
return keys.length > 0 ? keys[0] : undefined;
}
Expand Down
22 changes: 9 additions & 13 deletions x-pack/plugins/ml/common/util/job_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
getDatafeedAggregations,
} from './datafeed_utils';
import { findAggField } from './validation_utils';
import { isPopulatedObject } from './object_utils';

export interface ValidationResults {
valid: boolean;
Expand All @@ -51,17 +52,9 @@ export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: numb
}

export function hasRuntimeMappings(job: CombinedJob): boolean {
const hasDatafeed =
typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0;
const hasDatafeed = isPopulatedObject(job.datafeed_config);
if (hasDatafeed) {
const runtimeMappings =
typeof job.datafeed_config.runtime_mappings === 'object'
? Object.keys(job.datafeed_config.runtime_mappings)
: undefined;

if (Array.isArray(runtimeMappings) && runtimeMappings.length > 0) {
return true;
}
return isPopulatedObject(job.datafeed_config.runtime_mappings);
}
return false;
}
Expand Down Expand Up @@ -114,7 +107,11 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex
// If the datafeed uses script fields, we can only plot the time series if
// model plot is enabled. Without model plot it will be very difficult or impossible
// to invert to a reverse search of the underlying metric data.
if (isSourceDataChartable === true && typeof job.datafeed_config?.script_fields === 'object') {
if (
isSourceDataChartable === true &&
job.datafeed_config?.script_fields !== null &&
typeof job.datafeed_config?.script_fields === 'object'
) {
// Perform extra check to see if the detector is using a scripted field.
const scriptFields = Object.keys(job.datafeed_config.script_fields);
isSourceDataChartable =
Expand All @@ -123,8 +120,7 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex
scriptFields.indexOf(dtr.over_field_name!) === -1;
}

const hasDatafeed =
typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0;
const hasDatafeed = isPopulatedObject(job.datafeed_config);
if (hasDatafeed) {
// We cannot plot the source data for some specific aggregation configurations
const aggs = getDatafeedAggregations(job.datafeed_config);
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/ml/common/util/object_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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 const isPopulatedObject = <T = Record<string, any>>(arg: any): arg is T => {
return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0;
};
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/common/util/validation_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function findAggField(
value = returnParent === true ? aggs : aggs[k];
return true;
}
if (aggs.hasOwnProperty(k) && typeof aggs[k] === 'object') {
if (aggs.hasOwnProperty(k) && aggs[k] !== null && typeof aggs[k] === 'object') {
value = findAggField(aggs[k], fieldName, returnParent);
return value !== undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import { getNestedProperty } from '../../util/object_utils';
import { mlFieldFormatService } from '../../services/field_format_service';

import { DataGridItem, IndexPagination, RenderCellValue } from './types';
import type { RuntimeField } from '../../../../../../../src/plugins/data/common/index_patterns';
import { RuntimeMappings } from '../../../../common/types/fields';
import { isPopulatedObject } from '../../../../common/util/object_utils';

export const INIT_MAX_COLUMNS = 10;

Expand Down Expand Up @@ -82,6 +85,37 @@ export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): str
return indexPatternFields;
};

/**
* Return a map of runtime_mappings for each of the index pattern field provided
* to provide in ES search queries
* @param indexPatternFields
* @param indexPattern
* @param clonedRuntimeMappings
*/
export const getRuntimeFieldsMapping = (
indexPatternFields: string[] | undefined,
indexPattern: IndexPattern | undefined,
clonedRuntimeMappings?: RuntimeMappings
) => {
if (!Array.isArray(indexPatternFields) || indexPattern === undefined) return {};
darnautov marked this conversation as resolved.
Show resolved Hide resolved
const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields;
let combinedRuntimeMappings: RuntimeMappings = {};

if (isPopulatedObject(ipRuntimeMappings)) {
indexPatternFields.forEach((ipField) => {
if (ipRuntimeMappings.hasOwnProperty(ipField)) {
combinedRuntimeMappings[ipField] = ipRuntimeMappings[ipField];
}
});
}
if (isPopulatedObject(clonedRuntimeMappings)) {
combinedRuntimeMappings = { ...combinedRuntimeMappings, ...clonedRuntimeMappings };
}
return Object.keys(combinedRuntimeMappings).length > 0
? { runtime_mappings: combinedRuntimeMappings }
: {};
};

export interface FieldTypes {
[key: string]: ES_FIELD_TYPES;
}
Expand Down Expand Up @@ -131,6 +165,45 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
};

export const NON_AGGREGATABLE = 'non-aggregatable';

export const getDataGridSchemaFromESFieldType = (
fieldType: ES_FIELD_TYPES | undefined | RuntimeField['type']
): string | undefined => {
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
// To fall back to the default string schema it needs to be undefined.
let schema;

switch (fieldType) {
case ES_FIELD_TYPES.GEO_POINT:
case ES_FIELD_TYPES.GEO_SHAPE:
schema = 'json';
break;
case ES_FIELD_TYPES.BOOLEAN:
schema = 'boolean';
break;
case ES_FIELD_TYPES.DATE:
case ES_FIELD_TYPES.DATE_NANOS:
schema = 'datetime';
break;
case ES_FIELD_TYPES.BYTE:
case ES_FIELD_TYPES.DOUBLE:
case ES_FIELD_TYPES.FLOAT:
case ES_FIELD_TYPES.HALF_FLOAT:
case ES_FIELD_TYPES.INTEGER:
case ES_FIELD_TYPES.LONG:
case ES_FIELD_TYPES.SCALED_FLOAT:
case ES_FIELD_TYPES.SHORT:
schema = 'numeric';
break;
// keep schema undefined for text based columns
case ES_FIELD_TYPES.KEYWORD:
case ES_FIELD_TYPES.TEXT:
break;
}

return schema;
};

export const getDataGridSchemaFromKibanaFieldType = (
field: IFieldType | undefined
): string | undefined => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

export {
getDataGridSchemasFromFieldTypes,
getDataGridSchemaFromESFieldType,
getDataGridSchemaFromKibanaFieldType,
getFieldsFromKibanaIndexPattern,
getRuntimeFieldsMapping,
multiColumnSortFactory,
showDataGridColumnChartErrorMessageToast,
useRenderCellValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React, { FC, Fragment, useEffect, useMemo, useRef, useState } from 'react';
import {
EuiBadge,
EuiCallOut,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
Expand All @@ -19,6 +20,7 @@ import {
import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';

import { FormattedMessage } from '@kbn/i18n/react';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { useMlContext } from '../../../../../contexts/ml';

Expand Down Expand Up @@ -314,6 +316,15 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
};
}, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]);

const unsupportedRuntimeFields = useMemo(
() =>
currentIndexPattern.fields
.getAll()
.filter((f) => f.runtimeField)
.map((f) => `'${f.displayName}'`),
[currentIndexPattern.fields]
);

return (
<Fragment>
<Messages messages={requestMessages} />
Expand Down Expand Up @@ -445,6 +456,37 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
>
<Fragment />
</EuiFormRow>
{Array.isArray(unsupportedRuntimeFields) && unsupportedRuntimeFields.length > 0 && (
<>
<EuiCallOut size="s" color="warning">
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.unsupportedRuntimeFieldsCallout"
defaultMessage="The runtime {runtimeFieldsCount, plural, one {field} other {fields}} {unsupportedRuntimeFields} {extraCountMsg} are not supported for analysis."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, in case {extraCountMsg} is empty, the sentence ends up with 2 spaces before are not supported for analysis string. I think and {count} more message should take care of adding a space.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the extra space to extraCountMsg will actually be stripped out. For example, this is what I looks like with the spaces in extraCountMsg.
Screen Shot 2021-02-16 at 10 52 29

And this is what it looks like as is:
Screen Shot 2021-02-16 at 10 32 30

values={{
runtimeFieldsCount: unsupportedRuntimeFields.length,
extraCountMsg:
unsupportedRuntimeFields.length - 5 > 0 ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should avoid magic numbers, worth creating a constant const maxRuntimeFieldsDisplayCount = 5; or something like this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated here 7b91ae0

<FormattedMessage
id="xpack.ml.dataframe.analytics.create.extraUnsupportedRuntimeFieldsMsg"
defaultMessage="and {count} more"
values={{
count: unsupportedRuntimeFields.length - 5,
}}
/>
) : (
''
),
unsupportedRuntimeFields:
unsupportedRuntimeFields.length > 5
? unsupportedRuntimeFields.slice(0, 5).join(', ')
: unsupportedRuntimeFields.join(', '),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need in this check, slice doesn't add extra values to an array

Suggested change
unsupportedRuntimeFields:
unsupportedRuntimeFields.length > 5
? unsupportedRuntimeFields.slice(0, 5).join(', ')
: unsupportedRuntimeFields.join(', '),
unsupportedRuntimeFields: unsupportedRuntimeFields.slice(0, 5).join(', ')

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated here 7b91ae0

}}
/>
</EuiCallOut>
<EuiSpacer />
</>
)}

<AnalysisFieldsTable
dependentVariable={dependentVariable}
includes={includes}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { SearchResponse7 } from '../../../../../../common/types/es_client';
import { extractErrorMessage } from '../../../../../../common/util/errors';
import { INDEX_STATUS } from '../../../common/analytics';
import { ml } from '../../../../services/ml_api_service';
import { getRuntimeFieldsMapping } from '../../../../components/data_grid/common';

type IndexSearchResponse = SearchResponse7;

Expand All @@ -38,7 +39,9 @@ export const useIndexData = (
query: any,
toastNotifications: CoreSetup['notifications']['toasts']
): UseIndexDataReturnType => {
const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern);
const indexPatternFields = useMemo(() => getFieldsFromKibanaIndexPattern(indexPattern), [
indexPattern,
]);

// EuiDataGrid State
const columns: EuiDataGridColumn[] = [
Expand Down Expand Up @@ -75,7 +78,6 @@ export const useIndexData = (
s[column.id] = { order: column.direction };
return s;
}, {} as EsSorting);

const esSearchRequest = {
index: indexPattern.title,
body: {
Expand All @@ -86,6 +88,7 @@ export const useIndexData = (
fields: ['*'],
_source: false,
...(Object.keys(sort).length > 0 ? { sort } : {}),
...getRuntimeFieldsMapping(indexPatternFields, indexPattern),
},
};

Expand All @@ -105,7 +108,7 @@ export const useIndexData = (
useEffect(() => {
getIndexData();
// custom comparison
}, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]);
}, [indexPattern.title, indexPatternFields, JSON.stringify([query, pagination, sortingColumns])]);

const dataLoader = useMemo(() => new DataLoader(indexPattern, toastNotifications), [
indexPattern,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { stringMatch } from '../../../util/string_utils';
import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states';
import { parseInterval } from '../../../../../common/util/parse_interval';
import { mlCalendarService } from '../../../services/calendar_service';
import { isPopulatedObject } from '../../../../../common/util/object_utils';

export function loadFullJob(jobId) {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -379,7 +380,7 @@ export function checkForAutoStartDatafeed() {
mlJobService.tempJobCloningObjects.datafeed = undefined;
mlJobService.tempJobCloningObjects.createdBy = undefined;

const hasDatafeed = typeof datafeed === 'object' && Object.keys(datafeed).length > 0;
const hasDatafeed = isPopulatedObject(datafeed);
const datafeedId = hasDatafeed ? datafeed.datafeed_id : '';
return {
id: job.job_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CombinedJob } from '../../../../../../../../common/types/anomaly_detect
import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor';
import { mlJobService } from '../../../../../../services/job_service';
import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils';
import { isPopulatedObject } from '../../../../../../../../common/util/object_utils';

export const DatafeedPreview: FC<{
combinedJob: CombinedJob | null;
Expand Down Expand Up @@ -64,7 +65,7 @@ export const DatafeedPreview: FC<{
const resp = await mlJobService.searchPreview(combinedJob);
let data = resp.hits.hits;
// the first item under aggregations can be any name
if (typeof resp.aggregations === 'object' && Object.keys(resp.aggregations).length > 0) {
if (isPopulatedObject(resp.aggregations)) {
const accessor = Object.keys(resp.aggregations)[0];
data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { findAggField } from '../../../../common/util/validation_utils';
import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils';
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types';
import { isPopulatedObject } from '../../../../common/util/object_utils';

interface ResultResponse {
success: boolean;
Expand Down Expand Up @@ -175,7 +176,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
// when the field is an aggregation field, because the field doesn't actually exist in the indices
// we need to pass all the sub aggs from the original datafeed config
// so that we can access the aggregated field
if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) {
if (isPopulatedObject(aggFields)) {
// first item under aggregations can be any name, not necessarily 'buckets'
const accessor = Object.keys(aggFields)[0];
const tempAggs = { ...(aggFields[accessor].aggs ?? aggFields[accessor].aggregations) };
Expand Down
Loading