Skip to content

Commit ee4b2d0

Browse files
[ML] add geo point combined field to CSV import (#77117) (#77921)
* [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 <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
1 parent 6ced05b commit ee4b2d0

File tree

13 files changed

+986
-3
lines changed

13 files changed

+986
-3
lines changed

x-pack/plugins/ml/common/types/file_datavisualizer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface FindFileStructureResponse {
2929
count: number;
3030
cardinality: number;
3131
top_hits: Array<{ count: number; value: any }>;
32+
max_value?: number;
33+
min_value?: number;
3234
};
3335
};
3436
sample_start: string;
@@ -42,7 +44,7 @@ export interface FindFileStructureResponse {
4244
delimiter: string;
4345
need_client_timezone: boolean;
4446
num_lines_analyzed: number;
45-
column_names: string[];
47+
column_names?: string[];
4648
explanation?: string[];
4749
grok_pattern?: string;
4850
multiline_start_pattern?: string;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 from 'react';
8+
import { EuiText } from '@elastic/eui';
9+
10+
import { CombinedField } from './types';
11+
12+
export function CombinedFieldLabel({ combinedField }: { combinedField: CombinedField }) {
13+
return <EuiText size="s">{getCombinedFieldLabel(combinedField)}</EuiText>;
14+
}
15+
16+
function getCombinedFieldLabel(combinedField: CombinedField) {
17+
return `${combinedField.fieldNames.join(combinedField.delimiter)} => ${
18+
combinedField.combinedFieldName
19+
} (${combinedField.mappingType})`;
20+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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 { i18n } from '@kbn/i18n';
8+
import { FormattedMessage } from '@kbn/i18n/react';
9+
import React, { Component } from 'react';
10+
11+
import {
12+
EuiFormRow,
13+
EuiPopover,
14+
EuiContextMenu,
15+
EuiButtonEmpty,
16+
EuiButtonIcon,
17+
EuiFlexGroup,
18+
EuiFlexItem,
19+
} from '@elastic/eui';
20+
21+
import { CombinedField } from './types';
22+
import { GeoPointForm } from './geo_point';
23+
import { CombinedFieldLabel } from './combined_field_label';
24+
import {
25+
addCombinedFieldsToMappings,
26+
addCombinedFieldsToPipeline,
27+
getNameCollisionMsg,
28+
removeCombinedFieldsFromMappings,
29+
removeCombinedFieldsFromPipeline,
30+
} from './utils';
31+
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
32+
33+
interface Props {
34+
mappingsString: string;
35+
pipelineString: string;
36+
onMappingsStringChange(): void;
37+
onPipelineStringChange(): void;
38+
combinedFields: CombinedField[];
39+
onCombinedFieldsChange(combinedFields: CombinedField[]): void;
40+
results: FindFileStructureResponse;
41+
isDisabled: boolean;
42+
}
43+
44+
interface State {
45+
isPopoverOpen: boolean;
46+
}
47+
48+
export class CombinedFieldsForm extends Component<Props, State> {
49+
state: State = {
50+
isPopoverOpen: false,
51+
};
52+
53+
togglePopover = () => {
54+
this.setState((prevState) => ({
55+
isPopoverOpen: !prevState.isPopoverOpen,
56+
}));
57+
};
58+
59+
closePopover = () => {
60+
this.setState({
61+
isPopoverOpen: false,
62+
});
63+
};
64+
65+
addCombinedField = (combinedField: CombinedField) => {
66+
if (this.hasNameCollision(combinedField.combinedFieldName)) {
67+
throw new Error(getNameCollisionMsg(combinedField.combinedFieldName));
68+
}
69+
70+
const mappings = this.parseMappings();
71+
const pipeline = this.parsePipeline();
72+
73+
this.props.onMappingsStringChange(
74+
// @ts-expect-error
75+
JSON.stringify(addCombinedFieldsToMappings(mappings, [combinedField]), null, 2)
76+
);
77+
this.props.onPipelineStringChange(
78+
// @ts-expect-error
79+
JSON.stringify(addCombinedFieldsToPipeline(pipeline, [combinedField]), null, 2)
80+
);
81+
this.props.onCombinedFieldsChange([...this.props.combinedFields, combinedField]);
82+
83+
this.closePopover();
84+
};
85+
86+
removeCombinedField = (index: number) => {
87+
let mappings;
88+
let pipeline;
89+
try {
90+
mappings = this.parseMappings();
91+
pipeline = this.parsePipeline();
92+
} catch (error) {
93+
// how should remove error be surfaced?
94+
return;
95+
}
96+
97+
const updatedCombinedFields = [...this.props.combinedFields];
98+
const removedCombinedFields = updatedCombinedFields.splice(index, 1);
99+
100+
this.props.onMappingsStringChange(
101+
// @ts-expect-error
102+
JSON.stringify(removeCombinedFieldsFromMappings(mappings, removedCombinedFields), null, 2)
103+
);
104+
this.props.onPipelineStringChange(
105+
// @ts-expect-error
106+
JSON.stringify(removeCombinedFieldsFromPipeline(pipeline, removedCombinedFields), null, 2)
107+
);
108+
this.props.onCombinedFieldsChange(updatedCombinedFields);
109+
};
110+
111+
parseMappings() {
112+
try {
113+
return JSON.parse(this.props.mappingsString);
114+
} catch (error) {
115+
throw new Error(
116+
i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.mappingsParseError', {
117+
defaultMessage: 'Error parsing mappings: {error}',
118+
values: { error: error.message },
119+
})
120+
);
121+
}
122+
}
123+
124+
parsePipeline() {
125+
try {
126+
return JSON.parse(this.props.pipelineString);
127+
} catch (error) {
128+
throw new Error(
129+
i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.pipelineParseError', {
130+
defaultMessage: 'Error parsing pipeline: {error}',
131+
values: { error: error.message },
132+
})
133+
);
134+
}
135+
}
136+
137+
hasNameCollision = (name: string) => {
138+
if (this.props.results.column_names?.includes(name)) {
139+
// collision with column name
140+
return true;
141+
}
142+
143+
if (
144+
this.props.combinedFields.some((combinedField) => combinedField.combinedFieldName === name)
145+
) {
146+
// collision with combined field name
147+
return true;
148+
}
149+
150+
const mappings = this.parseMappings();
151+
return mappings.properties.hasOwnProperty(name);
152+
};
153+
154+
render() {
155+
const geoPointLabel = i18n.translate('xpack.ml.fileDatavisualizer.geoPointCombinedFieldLabel', {
156+
defaultMessage: 'Add geo point field',
157+
});
158+
const panels = [
159+
{
160+
id: 0,
161+
items: [
162+
{
163+
name: geoPointLabel,
164+
panel: 1,
165+
},
166+
],
167+
},
168+
{
169+
id: 1,
170+
title: geoPointLabel,
171+
content: (
172+
<GeoPointForm
173+
addCombinedField={this.addCombinedField}
174+
hasNameCollision={this.hasNameCollision}
175+
results={this.props.results}
176+
/>
177+
),
178+
},
179+
];
180+
return (
181+
<EuiFormRow
182+
label={i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsLabel', {
183+
defaultMessage: 'Combined fields',
184+
})}
185+
>
186+
<div>
187+
{this.props.combinedFields.map((combinedField: CombinedField, idx: number) => (
188+
<EuiFlexGroup key={idx} gutterSize="s">
189+
<EuiFlexItem>
190+
<CombinedFieldLabel combinedField={combinedField} />
191+
</EuiFlexItem>
192+
{!this.props.isDisabled && (
193+
<EuiFlexItem grow={false}>
194+
<EuiButtonIcon
195+
iconType="trash"
196+
color="danger"
197+
onClick={this.removeCombinedField.bind(null, idx)}
198+
title={i18n.translate('xpack.ml.fileDatavisualizer.removeCombinedFieldsLabel', {
199+
defaultMessage: 'Remove combined field',
200+
})}
201+
aria-label={i18n.translate(
202+
'xpack.ml.fileDatavisualizer.removeCombinedFieldsLabel',
203+
{
204+
defaultMessage: 'Remove combined field',
205+
}
206+
)}
207+
/>
208+
</EuiFlexItem>
209+
)}
210+
</EuiFlexGroup>
211+
))}
212+
<EuiPopover
213+
id="combineFieldsPopover"
214+
button={
215+
<EuiButtonEmpty
216+
onClick={this.togglePopover}
217+
size="xs"
218+
iconType="plusInCircleFilled"
219+
isDisabled={this.props.isDisabled}
220+
>
221+
<FormattedMessage
222+
id="xpack.ml.fileDatavisualizer.addCombinedFieldsLabel"
223+
defaultMessage="Add combined field"
224+
/>
225+
</EuiButtonEmpty>
226+
}
227+
isOpen={this.state.isPopoverOpen}
228+
closePopover={this.closePopover}
229+
anchorPosition="rightCenter"
230+
>
231+
<EuiContextMenu initialPanelId={0} panels={panels} />
232+
</EuiPopover>
233+
</div>
234+
</EuiFormRow>
235+
);
236+
}
237+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 { i18n } from '@kbn/i18n';
8+
import React from 'react';
9+
10+
import { EuiFormRow } from '@elastic/eui';
11+
12+
import { CombinedField } from './types';
13+
import { CombinedFieldLabel } from './combined_field_label';
14+
15+
export function CombinedFieldsReadOnlyForm({
16+
combinedFields,
17+
}: {
18+
combinedFields: CombinedField[];
19+
}) {
20+
return combinedFields.length ? (
21+
<EuiFormRow
22+
label={i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyLabel', {
23+
defaultMessage: 'Combined fields',
24+
})}
25+
helpText={i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsReadOnlyHelpTextLabel', {
26+
defaultMessage: 'Edit combined fields in advanced tab',
27+
})}
28+
>
29+
<div>
30+
{combinedFields.map((combinedField: CombinedField, idx: number) => (
31+
<CombinedFieldLabel key={idx} combinedField={combinedField} />
32+
))}
33+
</div>
34+
</EuiFormRow>
35+
) : null;
36+
}

0 commit comments

Comments
 (0)