[ML] add geo point combined field to CSV import (#77117)

* [ML] add geo point combined field to CSV import

* remove some geo_point specific logic

* Account for properties layer in find_file_structure mappings

* improve checking of name collision to include combined fields and mappings

* add delete button

* fix function name

* fill in unknowns with defined types

* tslint changes

* get tslint passing

* show readonly combined fields in simple tab

* handle column_names being undefined

* add unit tests for modifying mappings and pipeline

* review feedback

* do not change combinedFields on reset

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-09-18 10:21:52 -06:00 committed by GitHub
parent 3d67eaaed4
commit f3c9d10afc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 986 additions and 3 deletions

View file

@ -29,6 +29,8 @@ export interface FindFileStructureResponse {
count: number;
cardinality: number;
top_hits: Array<{ count: number; value: any }>;
max_value?: number;
min_value?: number;
};
};
sample_start: string;
@ -42,7 +44,7 @@ export interface FindFileStructureResponse {
delimiter: string;
need_client_timezone: boolean;
num_lines_analyzed: number;
column_names: string[];
column_names?: string[];
explanation?: string[];
grok_pattern?: string;
multiline_start_pattern?: string;

View file

@ -0,0 +1,20 @@
/*
* 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 from 'react';
import { EuiText } from '@elastic/eui';
import { CombinedField } from './types';
export function CombinedFieldLabel({ combinedField }: { combinedField: CombinedField }) {
return <EuiText size="s">{getCombinedFieldLabel(combinedField)}</EuiText>;
}
function getCombinedFieldLabel(combinedField: CombinedField) {
return `${combinedField.fieldNames.join(combinedField.delimiter)} => ${
combinedField.combinedFieldName
} (${combinedField.mappingType})`;
}

View file

@ -0,0 +1,237 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import {
EuiFormRow,
EuiPopover,
EuiContextMenu,
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { CombinedField } from './types';
import { GeoPointForm } from './geo_point';
import { CombinedFieldLabel } from './combined_field_label';
import {
addCombinedFieldsToMappings,
addCombinedFieldsToPipeline,
getNameCollisionMsg,
removeCombinedFieldsFromMappings,
removeCombinedFieldsFromPipeline,
} from './utils';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
interface Props {
mappingsString: string;
pipelineString: string;
onMappingsStringChange(): void;
onPipelineStringChange(): void;
combinedFields: CombinedField[];
onCombinedFieldsChange(combinedFields: CombinedField[]): void;
results: FindFileStructureResponse;
isDisabled: boolean;
}
interface State {
isPopoverOpen: boolean;
}
export class CombinedFieldsForm extends Component<Props, State> {
state: State = {
isPopoverOpen: false,
};
togglePopover = () => {
this.setState((prevState) => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};
closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};
addCombinedField = (combinedField: CombinedField) => {
if (this.hasNameCollision(combinedField.combinedFieldName)) {
throw new Error(getNameCollisionMsg(combinedField.combinedFieldName));
}
const mappings = this.parseMappings();
const pipeline = this.parsePipeline();
this.props.onMappingsStringChange(
// @ts-expect-error
JSON.stringify(addCombinedFieldsToMappings(mappings, [combinedField]), null, 2)
);
this.props.onPipelineStringChange(
// @ts-expect-error
JSON.stringify(addCombinedFieldsToPipeline(pipeline, [combinedField]), null, 2)
);
this.props.onCombinedFieldsChange([...this.props.combinedFields, combinedField]);
this.closePopover();
};
removeCombinedField = (index: number) => {
let mappings;
let pipeline;
try {
mappings = this.parseMappings();
pipeline = this.parsePipeline();
} catch (error) {
// how should remove error be surfaced?
return;
}
const updatedCombinedFields = [...this.props.combinedFields];
const removedCombinedFields = updatedCombinedFields.splice(index, 1);
this.props.onMappingsStringChange(
// @ts-expect-error
JSON.stringify(removeCombinedFieldsFromMappings(mappings, removedCombinedFields), null, 2)
);
this.props.onPipelineStringChange(
// @ts-expect-error
JSON.stringify(removeCombinedFieldsFromPipeline(pipeline, removedCombinedFields), null, 2)
);
this.props.onCombinedFieldsChange(updatedCombinedFields);
};
parseMappings() {
try {
return JSON.parse(this.props.mappingsString);
} catch (error) {
throw new Error(
i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.mappingsParseError', {
defaultMessage: 'Error parsing mappings: {error}',
values: { error: error.message },
})
);
}
}
parsePipeline() {
try {
return JSON.parse(this.props.pipelineString);
} catch (error) {
throw new Error(
i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.pipelineParseError', {
defaultMessage: 'Error parsing pipeline: {error}',
values: { error: error.message },
})
);
}
}
hasNameCollision = (name: string) => {
if (this.props.results.column_names?.includes(name)) {
// collision with column name
return true;
}
if (
this.props.combinedFields.some((combinedField) => combinedField.combinedFieldName === name)
) {
// collision with combined field name
return true;
}
const mappings = this.parseMappings();
return mappings.properties.hasOwnProperty(name);
};
render() {
const geoPointLabel = i18n.translate('xpack.ml.fileDatavisualizer.geoPointCombinedFieldLabel', {
defaultMessage: 'Add geo point field',
});
const panels = [
{
id: 0,
items: [
{
name: geoPointLabel,
panel: 1,
},
],
},
{
id: 1,
title: geoPointLabel,
content: (
<GeoPointForm
addCombinedField={this.addCombinedField}
hasNameCollision={this.hasNameCollision}
results={this.props.results}
/>
),
},
];
return (
<EuiFormRow
label={i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsLabel', {
defaultMessage: 'Combined fields',
})}
>
<div>
{this.props.combinedFields.map((combinedField: CombinedField, idx: number) => (
<EuiFlexGroup key={idx} gutterSize="s">
<EuiFlexItem>
<CombinedFieldLabel combinedField={combinedField} />
</EuiFlexItem>
{!this.props.isDisabled && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="trash"
color="danger"
onClick={this.removeCombinedField.bind(null, idx)}
title={i18n.translate('xpack.ml.fileDatavisualizer.removeCombinedFieldsLabel', {
defaultMessage: 'Remove combined field',
})}
aria-label={i18n.translate(
'xpack.ml.fileDatavisualizer.removeCombinedFieldsLabel',
{
defaultMessage: 'Remove combined field',
}
)}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
))}
<EuiPopover
id="combineFieldsPopover"
button={
<EuiButtonEmpty
onClick={this.togglePopover}
size="xs"
iconType="plusInCircleFilled"
isDisabled={this.props.isDisabled}
>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.addCombinedFieldsLabel"
defaultMessage="Add combined field"
/>
</EuiButtonEmpty>
}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
anchorPosition="rightCenter"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</div>
</EuiFormRow>
);
}
}

View file

@ -0,0 +1,36 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiFormRow } from '@elastic/eui';
import { CombinedField } from './types';
import { CombinedFieldLabel } from './combined_field_label';
export function CombinedFieldsReadOnlyForm({
combinedFields,
}: {
combinedFields: CombinedField[];
}) {
return combinedFields.length ? (
<EuiFormRow
label={i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyLabel', {
defaultMessage: 'Combined fields',
})}
helpText={i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyHelpTextLabel', {
defaultMessage: 'Edit combined fields in advanced tab',
})}
>
<div>
{combinedFields.map((combinedField: CombinedField, idx: number) => (
<CombinedFieldLabel key={idx} combinedField={combinedField} />
))}
</div>
</EuiFormRow>
) : null;
}

View file

@ -0,0 +1,189 @@
/*
* 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 { i18n } from '@kbn/i18n';
import debounce from 'lodash/debounce';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { ChangeEvent, Component, Fragment } from 'react';
import {
EuiFormRow,
EuiFieldText,
EuiTextAlign,
EuiSpacer,
EuiButton,
EuiSelect,
EuiSelectOption,
EuiFormErrorText,
} from '@elastic/eui';
import { CombinedField } from './types';
import {
createGeoPointCombinedField,
isWithinLatRange,
isWithinLonRange,
getFieldNames,
getNameCollisionMsg,
} from './utils';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
interface Props {
addCombinedField: (combinedField: CombinedField) => void;
hasNameCollision: (name: string) => boolean;
results: FindFileStructureResponse;
}
interface State {
latField: string;
lonField: string;
geoPointField: string;
geoPointFieldError: string;
latFields: EuiSelectOption[];
lonFields: EuiSelectOption[];
submitError: string;
}
export class GeoPointForm extends Component<Props, State> {
constructor(props: Props) {
super(props);
const latFields: EuiSelectOption[] = [{ value: '', text: '' }];
const lonFields: EuiSelectOption[] = [{ value: '', text: '' }];
getFieldNames(props.results).forEach((columnName: string) => {
if (isWithinLatRange(columnName, props.results.field_stats)) {
latFields.push({ value: columnName, text: columnName });
}
if (isWithinLonRange(columnName, props.results.field_stats)) {
lonFields.push({ value: columnName, text: columnName });
}
});
this.state = {
latField: '',
lonField: '',
geoPointField: '',
geoPointFieldError: '',
submitError: '',
latFields,
lonFields,
};
}
onLatFieldChange = (e: ChangeEvent<HTMLSelectElement>) => {
this.setState({ latField: e.target.value });
};
onLonFieldChange = (e: ChangeEvent<HTMLSelectElement>) => {
this.setState({ lonField: e.target.value });
};
onGeoPointFieldChange = (e: ChangeEvent<HTMLInputElement>) => {
const geoPointField = e.target.value;
this.setState({ geoPointField });
this.hasNameCollision(geoPointField);
};
hasNameCollision = debounce((name: string) => {
try {
const geoPointFieldError = this.props.hasNameCollision(name) ? getNameCollisionMsg(name) : '';
this.setState({ geoPointFieldError });
} catch (error) {
this.setState({ submitError: error.message });
}
}, 200);
onSubmit = () => {
try {
this.props.addCombinedField(
createGeoPointCombinedField(
this.state.latField,
this.state.lonField,
this.state.geoPointField
)
);
this.setState({ submitError: '' });
} catch (error) {
this.setState({ submitError: error.message });
}
};
render() {
let error;
if (this.state.submitError) {
error = <EuiFormErrorText>{this.state.submitError}</EuiFormErrorText>;
}
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.fileDatavisualizer.geoPointForm.latFieldLabel', {
defaultMessage: 'Latitude field',
})}
>
<EuiSelect
options={this.state.latFields}
value={this.state.latField}
onChange={this.onLatFieldChange}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ml.fileDatavisualizer.geoPointForm.lonFieldLabel', {
defaultMessage: 'Longitude field',
})}
>
<EuiSelect
options={this.state.lonFields}
value={this.state.lonField}
onChange={this.onLonFieldChange}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ml.fileDatavisualizer.geoPointForm.geoPointFieldLabel', {
defaultMessage: 'Geo point field',
})}
isInvalid={this.state.geoPointFieldError !== ''}
error={[this.state.geoPointFieldError]}
>
<EuiFieldText
value={this.state.geoPointField}
onChange={this.onGeoPointFieldChange}
isInvalid={this.state.geoPointFieldError !== ''}
aria-label={i18n.translate(
'xpack.ml.fileDatavisualizer.geoPointForm.geoPointFieldAriaLabel',
{
defaultMessage: 'Geo point field, required field',
}
)}
/>
</EuiFormRow>
<EuiSpacer size="s" />
{error}
<EuiTextAlign textAlign="right">
<EuiButton
size="s"
fill
disabled={
!this.state.latField ||
!this.state.lonField ||
!this.state.geoPointField ||
this.state.geoPointFieldError !== ''
}
onClick={this.onSubmit}
>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.geoPointForm.submitButtonLabel"
defaultMessage="Add"
/>
</EuiButton>
</EuiTextAlign>
</Fragment>
);
}
}

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export {
addCombinedFieldsToPipeline,
addCombinedFieldsToMappings,
getDefaultCombinedFields,
} from './utils';
export { CombinedFieldsReadOnlyForm } from './combined_fields_read_only_form';
export { CombinedFieldsForm } from './combined_fields_form';
export { CombinedField } from './types';

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export interface CombinedField {
mappingType: string;
delimiter: string;
combinedFieldName: string;
fieldNames: string[];
}

View file

@ -0,0 +1,235 @@
/*
* 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 {
addCombinedFieldsToMappings,
addCombinedFieldsToPipeline,
createGeoPointCombinedField,
isWithinLatRange,
isWithinLonRange,
removeCombinedFieldsFromMappings,
removeCombinedFieldsFromPipeline,
} from './utils';
const combinedFields = [createGeoPointCombinedField('lat', 'lon', 'location')];
test('addCombinedFieldsToMappings', () => {
const mappings = {
_meta: {
created_by: '',
},
properties: {
lat: {
type: 'number',
},
lon: {
type: 'number',
},
},
};
expect(addCombinedFieldsToMappings(mappings, combinedFields)).toEqual({
_meta: {
created_by: '',
},
properties: {
lat: {
type: 'number',
},
lon: {
type: 'number',
},
location: {
type: 'geo_point',
},
},
});
});
test('removeCombinedFieldsFromMappings', () => {
const mappings = {
_meta: {
created_by: '',
},
properties: {
lat: {
type: 'number',
},
lon: {
type: 'number',
},
location: {
type: 'geo_point',
},
},
};
expect(removeCombinedFieldsFromMappings(mappings, combinedFields)).toEqual({
_meta: {
created_by: '',
},
properties: {
lat: {
type: 'number',
},
lon: {
type: 'number',
},
},
});
});
test('addCombinedFieldsToPipeline', () => {
const pipeline = {
description: '',
processors: [
{
set: {
field: 'anotherfield',
value: '{{value}}',
},
},
],
};
expect(addCombinedFieldsToPipeline(pipeline, combinedFields)).toEqual({
description: '',
processors: [
{
set: {
field: 'anotherfield',
value: '{{value}}',
},
},
{
set: {
field: 'location',
value: '{{lat}},{{lon}}',
},
},
],
});
});
test('removeCombinedFieldsFromPipeline', () => {
const pipeline = {
description: '',
processors: [
{
set: {
field: 'anotherfield',
value: '{{value}}',
},
},
{
set: {
field: 'location',
value: '{{lat}},{{lon}}',
},
},
],
};
expect(removeCombinedFieldsFromPipeline(pipeline, combinedFields)).toEqual({
description: '',
processors: [
{
set: {
field: 'anotherfield',
value: '{{value}}',
},
},
],
});
});
test('isWithinLatRange', () => {
expect(isWithinLatRange('fieldAlpha', {})).toBe(false);
expect(
isWithinLatRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: 1 }],
},
})
).toBe(false);
expect(
isWithinLatRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: 100 }],
max_value: 100,
min_value: 0,
},
})
).toBe(false);
expect(
isWithinLatRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: -100 }],
max_value: 0,
min_value: -100,
},
})
).toBe(false);
expect(
isWithinLatRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: 0 }],
max_value: 0,
min_value: 0,
},
})
).toBe(true);
});
test('isWithinLonRange', () => {
expect(isWithinLonRange('fieldAlpha', {})).toBe(false);
expect(
isWithinLonRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: 1 }],
},
})
).toBe(false);
expect(
isWithinLonRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: 200 }],
max_value: 200,
min_value: 0,
},
})
).toBe(false);
expect(
isWithinLonRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: -200 }],
max_value: 0,
min_value: -200,
},
})
).toBe(false);
expect(
isWithinLonRange('fieldAlpha', {
fieldAlpha: {
count: 1,
cardinality: 1,
top_hits: [{ count: 1, value: 0 }],
max_value: 0,
min_value: 0,
},
})
).toBe(true);
});

View file

@ -0,0 +1,174 @@
/*
* 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 { i18n } from '@kbn/i18n';
import _ from 'lodash';
import uuid from 'uuid/v4';
import { CombinedField } from './types';
import {
FindFileStructureResponse,
IngestPipeline,
Mappings,
} from '../../../../../../common/types/file_datavisualizer';
const COMMON_LAT_NAMES = ['latitude', 'lat'];
const COMMON_LON_NAMES = ['longitude', 'long', 'lon'];
export function getDefaultCombinedFields(results: FindFileStructureResponse) {
const combinedFields: CombinedField[] = [];
const geoPointField = getGeoPointField(results);
if (geoPointField) {
combinedFields.push(geoPointField);
}
return combinedFields;
}
export function addCombinedFieldsToMappings(
mappings: Mappings,
combinedFields: CombinedField[]
): Mappings {
const updatedMappings = { ...mappings };
combinedFields.forEach((combinedField) => {
updatedMappings.properties[combinedField.combinedFieldName] = {
type: combinedField.mappingType,
};
});
return updatedMappings;
}
export function removeCombinedFieldsFromMappings(
mappings: Mappings,
combinedFields: CombinedField[]
) {
const updatedMappings = { ...mappings };
combinedFields.forEach((combinedField) => {
delete updatedMappings.properties[combinedField.combinedFieldName];
});
return updatedMappings;
}
export function addCombinedFieldsToPipeline(
pipeline: IngestPipeline,
combinedFields: CombinedField[]
) {
const updatedPipeline = _.cloneDeep(pipeline);
combinedFields.forEach((combinedField) => {
updatedPipeline.processors.push({
set: {
field: combinedField.combinedFieldName,
value: combinedField.fieldNames
.map((fieldName) => {
return `{{${fieldName}}}`;
})
.join(combinedField.delimiter),
},
});
});
return updatedPipeline;
}
export function removeCombinedFieldsFromPipeline(
pipeline: IngestPipeline,
combinedFields: CombinedField[]
) {
return {
...pipeline,
processors: pipeline.processors.filter((processor) => {
return 'set' in processor
? !combinedFields.some((combinedField) => {
return processor.set.field === combinedField.combinedFieldName;
})
: true;
}),
};
}
export function isWithinLatRange(
fieldName: string,
fieldStats: FindFileStructureResponse['field_stats']
) {
return (
fieldName in fieldStats &&
'max_value' in fieldStats[fieldName] &&
fieldStats[fieldName]!.max_value! <= 90 &&
'min_value' in fieldStats[fieldName] &&
fieldStats[fieldName]!.min_value! >= -90
);
}
export function isWithinLonRange(
fieldName: string,
fieldStats: FindFileStructureResponse['field_stats']
) {
return (
fieldName in fieldStats &&
'max_value' in fieldStats[fieldName] &&
fieldStats[fieldName]!.max_value! <= 180 &&
'min_value' in fieldStats[fieldName] &&
fieldStats[fieldName]!.min_value! >= -180
);
}
export function createGeoPointCombinedField(
latField: string,
lonField: string,
geoPointField: string
): CombinedField {
return {
mappingType: 'geo_point',
delimiter: ',',
combinedFieldName: geoPointField,
fieldNames: [latField, lonField],
};
}
export function getNameCollisionMsg(name: string) {
return i18n.translate('xpack.ml.fileDatavisualizer.nameCollisionMsg', {
defaultMessage: '"{name}" already exists, please provide a unique name',
values: { name },
});
}
export function getFieldNames(results: FindFileStructureResponse): string[] {
return results.column_names !== undefined
? results.column_names
: Object.keys(results.field_stats);
}
function getGeoPointField(results: FindFileStructureResponse) {
const fieldNames = getFieldNames(results);
const latField = fieldNames.find((columnName) => {
return (
COMMON_LAT_NAMES.includes(columnName.toLowerCase()) &&
isWithinLatRange(columnName, results.field_stats)
);
});
const lonField = fieldNames.find((columnName) => {
return (
COMMON_LON_NAMES.includes(columnName.toLowerCase()) &&
isWithinLonRange(columnName, results.field_stats)
);
});
if (!latField || !lonField) {
return null;
}
const combinedFieldNames = [
'location',
'point_location',
`${latField}_${lonField}`,
`location_${uuid()}`,
];
// Use first combinedFieldNames that does not have a naming collision
const geoPointField = combinedFieldNames.find((name) => {
return !fieldNames.includes(name);
});
return geoPointField ? createGeoPointCombinedField(latField, lonField, geoPointField) : null;
}

View file

@ -17,7 +17,9 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { CombinedField, CombinedFieldsForm } from '../combined_fields';
import { MLJobEditor, ML_EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
const EDITOR_HEIGHT = '300px';
interface Props {
@ -36,6 +38,9 @@ interface Props {
onPipelineStringChange(): void;
indexNameError: string;
indexPatternNameError: string;
combinedFields: CombinedField[];
onCombinedFieldsChange(combinedFields: CombinedField[]): void;
results: FindFileStructureResponse;
}
export const AdvancedSettings: FC<Props> = ({
@ -54,6 +59,9 @@ export const AdvancedSettings: FC<Props> = ({
onPipelineStringChange,
indexNameError,
indexPatternNameError,
combinedFields,
onCombinedFieldsChange,
results,
}) => {
return (
<React.Fragment>
@ -123,6 +131,17 @@ export const AdvancedSettings: FC<Props> = ({
/>
</EuiFormRow>
<CombinedFieldsForm
mappingsString={mappingsString}
pipelineString={pipelineString}
onMappingsStringChange={onMappingsStringChange}
onPipelineStringChange={onPipelineStringChange}
combinedFields={combinedFields}
onCombinedFieldsChange={onCombinedFieldsChange}
results={results}
isDisabled={initialized === true}
/>
<EuiFlexGroup>
<EuiFlexItem>
<IndexSettings

View file

@ -11,6 +11,8 @@ import { EuiTabbedContent, EuiSpacer } from '@elastic/eui';
import { SimpleSettings } from './simple';
import { AdvancedSettings } from './advanced';
import { CombinedField } from '../combined_fields';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
interface Props {
index: string;
@ -28,6 +30,9 @@ interface Props {
onPipelineStringChange(): void;
indexNameError: string;
indexPatternNameError: string;
combinedFields: CombinedField[];
onCombinedFieldsChange(combinedFields: CombinedField[]): void;
results: FindFileStructureResponse;
}
export const ImportSettings: FC<Props> = ({
@ -46,6 +51,9 @@ export const ImportSettings: FC<Props> = ({
onPipelineStringChange,
indexNameError,
indexPatternNameError,
combinedFields,
onCombinedFieldsChange,
results,
}) => {
const tabs = [
{
@ -64,6 +72,7 @@ export const ImportSettings: FC<Props> = ({
createIndexPattern={createIndexPattern}
onCreateIndexPatternChange={onCreateIndexPatternChange}
indexNameError={indexNameError}
combinedFields={combinedFields}
/>
</React.Fragment>
),
@ -93,6 +102,9 @@ export const ImportSettings: FC<Props> = ({
onPipelineStringChange={onPipelineStringChange}
indexNameError={indexNameError}
indexPatternNameError={indexPatternNameError}
combinedFields={combinedFields}
onCombinedFieldsChange={onCombinedFieldsChange}
results={results}
/>
</React.Fragment>
),

View file

@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC } from 'react';
import { EuiFieldText, EuiFormRow, EuiCheckbox, EuiSpacer } from '@elastic/eui';
import { CombinedField, CombinedFieldsReadOnlyForm } from '../combined_fields';
interface Props {
index: string;
@ -17,6 +18,7 @@ interface Props {
createIndexPattern: boolean;
onCreateIndexPatternChange(): void;
indexNameError: string;
combinedFields: CombinedField[];
}
export const SimpleSettings: FC<Props> = ({
@ -26,6 +28,7 @@ export const SimpleSettings: FC<Props> = ({
createIndexPattern,
onCreateIndexPatternChange,
indexNameError,
combinedFields,
}) => {
return (
<React.Fragment>
@ -75,6 +78,10 @@ export const SimpleSettings: FC<Props> = ({
onChange={onCreateIndexPatternChange}
data-test-subj="mlFileDataVisCreateIndexPatternCheckbox"
/>
<EuiSpacer size="m" />
<CombinedFieldsReadOnlyForm combinedFields={combinedFields} />
</React.Fragment>
);
};

View file

@ -26,6 +26,11 @@ import { ImportProgress, IMPORT_STATUS } from '../import_progress';
import { ImportErrors } from '../import_errors';
import { ImportSummary } from '../import_summary';
import { ImportSettings } from '../import_settings';
import {
addCombinedFieldsToPipeline,
addCombinedFieldsToMappings,
getDefaultCombinedFields,
} from '../combined_fields';
import { ExperimentalBadge } from '../experimental_badge';
import { getIndexPatternNames, loadIndexPatterns } from '../../../../util/index_utils';
import { ml } from '../../../../services/ml_api_service';
@ -68,6 +73,7 @@ const DEFAULT_STATE = {
timeFieldName: undefined,
isFilebeatFlyoutVisible: false,
checkingValidIndex: false,
combinedFields: [],
};
export class ImportView extends Component {
@ -386,6 +392,10 @@ export class ImportView extends Component {
});
};
onCombinedFieldsChange = (combinedFields) => {
this.setState({ combinedFields });
};
setImportProgress = (progress) => {
this.setState({
uploadProgress: progress,
@ -444,6 +454,7 @@ export class ImportView extends Component {
timeFieldName,
isFilebeatFlyoutVisible,
checkingValidIndex,
combinedFields,
} = this.state;
const createPipeline = pipelineString !== '';
@ -513,6 +524,9 @@ export class ImportView extends Component {
onPipelineStringChange={this.onPipelineStringChange}
indexNameError={indexNameError}
indexPatternNameError={indexPatternNameError}
combinedFields={combinedFields}
onCombinedFieldsChange={this.onCombinedFieldsChange}
results={this.props.results}
/>
<EuiSpacer size="m" />
@ -644,12 +658,22 @@ function getDefaultState(state, results) {
? JSON.stringify(DEFAULT_INDEX_SETTINGS, null, 2)
: state.indexSettingsString;
const combinedFields = state.combinedFields.length
? state.combinedFields
: getDefaultCombinedFields(results);
const mappingsString =
state.mappingsString === '' ? JSON.stringify(results.mappings, null, 2) : state.mappingsString;
state.mappingsString === ''
? JSON.stringify(addCombinedFieldsToMappings(results.mappings, combinedFields), null, 2)
: state.mappingsString;
const pipelineString =
state.pipelineString === '' && results.ingest_pipeline !== undefined
? JSON.stringify(results.ingest_pipeline, null, 2)
? JSON.stringify(
addCombinedFieldsToPipeline(results.ingest_pipeline, combinedFields),
null,
2
)
: state.pipelineString;
const timeFieldName = results.timestamp_field;
@ -660,6 +684,7 @@ function getDefaultState(state, results) {
mappingsString,
pipelineString,
timeFieldName,
combinedFields,
};
}