Skip to content

Commit 8f456ba

Browse files
Wylie Conlonelasticmachine
andauthored
[Lens] Fieldless operations (#78080) (#78620)
* [Lens] Fieldless operations * Overhaul types * Fix invalid state and add tests * Fix types * Small cleanup * Add additional error message * Reset field selector to empty state when invalid Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 2e3e63d commit 8f456ba

File tree

20 files changed

+569
-558
lines changed

20 files changed

+569
-558
lines changed

x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx

Lines changed: 133 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@elastic/eui';
1818
import { EuiFormLabel } from '@elastic/eui';
1919
import { IndexPatternColumn, OperationType } from '../indexpattern';
20-
import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel';
20+
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from './dimension_panel';
2121
import {
2222
operationDefinitionMap,
2323
getOperationDisplay,
@@ -36,7 +36,7 @@ const operationPanels = getOperationDisplay();
3636

3737
export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
3838
selectedColumn?: IndexPatternColumn;
39-
operationFieldSupportMatrix: OperationFieldSupportMatrix;
39+
operationSupportMatrix: OperationSupportMatrix;
4040
currentIndexPattern: IndexPattern;
4141
}
4242

@@ -90,22 +90,24 @@ const LabelInput = ({ value, onChange }: { value: string; onChange: (value: stri
9090
export function DimensionEditor(props: DimensionEditorProps) {
9191
const {
9292
selectedColumn,
93-
operationFieldSupportMatrix,
93+
operationSupportMatrix,
9494
state,
9595
columnId,
9696
setState,
9797
layerId,
9898
currentIndexPattern,
9999
hideGrouping,
100100
} = props;
101-
const { operationByField, fieldByOperation } = operationFieldSupportMatrix;
101+
const { operationByField, fieldByOperation } = operationSupportMatrix;
102102
const [
103103
incompatibleSelectedOperationType,
104104
setInvalidOperationType,
105105
] = useState<OperationType | null>(null);
106106

107-
const ParamEditor =
108-
selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor;
107+
const selectedOperationDefinition =
108+
selectedColumn && operationDefinitionMap[selectedColumn.operationType];
109+
110+
const ParamEditor = selectedOperationDefinition?.paramEditor;
109111

110112
const fieldMap: Record<string, IndexPatternField> = useMemo(() => {
111113
const fields: Record<string, IndexPatternField> = {};
@@ -129,6 +131,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
129131
[
130132
...asOperationOptions(validOperationTypes, true),
131133
...asOperationOptions(possibleOperationTypes, false),
134+
...asOperationOptions(
135+
operationSupportMatrix.operationWithoutField,
136+
!selectedColumn || !hasField(selectedColumn)
137+
),
132138
],
133139
'operationType'
134140
);
@@ -166,12 +172,30 @@ export function DimensionEditor(props: DimensionEditorProps) {
166172
compatibleWithCurrentField ? '' : ' incompatible'
167173
}`,
168174
onClick() {
169-
// todo: when moving from terms agg to filters, we want to create a filter `$field.name : *`
170-
// it probably has to be re-thought when removing the field name.
171-
const isTermsToFilters =
172-
selectedColumn?.operationType === 'terms' && operationType === 'filters';
173-
174-
if (!selectedColumn || !compatibleWithCurrentField) {
175+
if (operationDefinitionMap[operationType].input === 'none') {
176+
// Clear invalid state because we are creating a valid column
177+
setInvalidOperationType(null);
178+
if (selectedColumn?.operationType === operationType) {
179+
return;
180+
}
181+
setState(
182+
changeColumn({
183+
state,
184+
layerId,
185+
columnId,
186+
newColumn: buildColumn({
187+
columns: props.state.layers[props.layerId].columns,
188+
suggestedPriority: props.suggestedPriority,
189+
layerId: props.layerId,
190+
op: operationType,
191+
indexPattern: currentIndexPattern,
192+
previousColumn: selectedColumn,
193+
}),
194+
})
195+
);
196+
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
197+
return;
198+
} else if (!selectedColumn || !compatibleWithCurrentField) {
175199
const possibleFields = fieldByOperation[operationType] || [];
176200

177201
if (possibleFields.length === 1) {
@@ -197,19 +221,20 @@ export function DimensionEditor(props: DimensionEditorProps) {
197221
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
198222
return;
199223
}
200-
if (incompatibleSelectedOperationType && !isTermsToFilters) {
201-
setInvalidOperationType(null);
202-
}
203-
if (selectedColumn.operationType === operationType) {
224+
225+
setInvalidOperationType(null);
226+
227+
if (selectedColumn?.operationType === operationType) {
204228
return;
205229
}
230+
206231
const newColumn: IndexPatternColumn = buildColumn({
207232
columns: props.state.layers[props.layerId].columns,
208233
suggestedPriority: props.suggestedPriority,
209234
layerId: props.layerId,
210235
op: operationType,
211236
indexPattern: currentIndexPattern,
212-
field: fieldMap[selectedColumn.sourceField],
237+
field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined,
213238
previousColumn: selectedColumn,
214239
});
215240

@@ -244,93 +269,101 @@ export function DimensionEditor(props: DimensionEditorProps) {
244269
</div>
245270
<EuiSpacer size="s" />
246271
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--shaded">
247-
<EuiFormRow
248-
data-test-subj="indexPattern-field-selection-row"
249-
label={i18n.translate('xpack.lens.indexPattern.chooseField', {
250-
defaultMessage: 'Choose a field',
251-
})}
252-
fullWidth
253-
isInvalid={Boolean(incompatibleSelectedOperationType)}
254-
error={
255-
selectedColumn
256-
? i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
257-
defaultMessage: 'To use this function, select a different field.',
258-
})
259-
: undefined
260-
}
261-
>
262-
<FieldSelect
263-
currentIndexPattern={currentIndexPattern}
264-
existingFields={state.existingFields}
265-
fieldMap={fieldMap}
266-
operationFieldSupportMatrix={operationFieldSupportMatrix}
267-
selectedColumnOperationType={selectedColumn && selectedColumn.operationType}
268-
selectedColumnSourceField={
269-
selectedColumn && hasField(selectedColumn) ? selectedColumn.sourceField : undefined
272+
{!selectedColumn ||
273+
selectedOperationDefinition?.input === 'field' ||
274+
(incompatibleSelectedOperationType &&
275+
operationDefinitionMap[incompatibleSelectedOperationType].input === 'field') ? (
276+
<EuiFormRow
277+
data-test-subj="indexPattern-field-selection-row"
278+
label={i18n.translate('xpack.lens.indexPattern.chooseField', {
279+
defaultMessage: 'Choose a field',
280+
})}
281+
fullWidth
282+
isInvalid={Boolean(incompatibleSelectedOperationType)}
283+
error={
284+
selectedColumn && incompatibleSelectedOperationType
285+
? selectedOperationDefinition?.input === 'field'
286+
? i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
287+
defaultMessage: 'To use this function, select a different field.',
288+
})
289+
: i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
290+
defaultMessage: 'To use this function, select a field.',
291+
})
292+
: undefined
270293
}
271-
incompatibleSelectedOperationType={incompatibleSelectedOperationType}
272-
onDeleteColumn={() => {
273-
setState(
274-
deleteColumn({
275-
state,
276-
layerId,
277-
columnId,
278-
})
279-
);
280-
}}
281-
onChoose={(choice) => {
282-
let column: IndexPatternColumn;
283-
if (
284-
!incompatibleSelectedOperationType &&
285-
selectedColumn &&
286-
'field' in choice &&
287-
choice.operationType === selectedColumn.operationType
288-
) {
289-
// If we just changed the field are not in an error state and the operation didn't change,
290-
// we use the operations onFieldChange method to calculate the new column.
291-
column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]);
292-
} else {
293-
// Otherwise we'll use the buildColumn method to calculate a new column
294-
const compatibleOperations =
295-
('field' in choice &&
296-
operationFieldSupportMatrix.operationByField[choice.field]) ||
297-
[];
298-
let operation;
299-
if (compatibleOperations.length > 0) {
300-
operation =
301-
incompatibleSelectedOperationType &&
302-
compatibleOperations.includes(incompatibleSelectedOperationType)
303-
? incompatibleSelectedOperationType
304-
: compatibleOperations[0];
305-
} else if ('field' in choice) {
306-
operation = choice.operationType;
307-
}
308-
column = buildColumn({
309-
columns: props.state.layers[props.layerId].columns,
310-
field: fieldMap[choice.field],
311-
indexPattern: currentIndexPattern,
312-
layerId: props.layerId,
313-
suggestedPriority: props.suggestedPriority,
314-
op: operation as OperationType,
315-
previousColumn: selectedColumn,
316-
});
294+
>
295+
<FieldSelect
296+
currentIndexPattern={currentIndexPattern}
297+
existingFields={state.existingFields}
298+
fieldMap={fieldMap}
299+
operationSupportMatrix={operationSupportMatrix}
300+
selectedColumnOperationType={selectedColumn && selectedColumn.operationType}
301+
selectedColumnSourceField={
302+
selectedColumn && hasField(selectedColumn) ? selectedColumn.sourceField : undefined
317303
}
304+
incompatibleSelectedOperationType={incompatibleSelectedOperationType}
305+
onDeleteColumn={() => {
306+
setState(
307+
deleteColumn({
308+
state,
309+
layerId,
310+
columnId,
311+
})
312+
);
313+
}}
314+
onChoose={(choice) => {
315+
let column: IndexPatternColumn;
316+
if (
317+
!incompatibleSelectedOperationType &&
318+
selectedColumn &&
319+
'field' in choice &&
320+
choice.operationType === selectedColumn.operationType
321+
) {
322+
// If we just changed the field are not in an error state and the operation didn't change,
323+
// we use the operations onFieldChange method to calculate the new column.
324+
column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]);
325+
} else {
326+
// Otherwise we'll use the buildColumn method to calculate a new column
327+
const compatibleOperations =
328+
('field' in choice && operationSupportMatrix.operationByField[choice.field]) ||
329+
[];
330+
let operation;
331+
if (compatibleOperations.length > 0) {
332+
operation =
333+
incompatibleSelectedOperationType &&
334+
compatibleOperations.includes(incompatibleSelectedOperationType)
335+
? incompatibleSelectedOperationType
336+
: compatibleOperations[0];
337+
} else if ('field' in choice) {
338+
operation = choice.operationType;
339+
}
340+
column = buildColumn({
341+
columns: props.state.layers[props.layerId].columns,
342+
field: fieldMap[choice.field],
343+
indexPattern: currentIndexPattern,
344+
layerId: props.layerId,
345+
suggestedPriority: props.suggestedPriority,
346+
op: operation as OperationType,
347+
previousColumn: selectedColumn,
348+
});
349+
}
318350

319-
setState(
320-
changeColumn({
321-
state,
322-
layerId,
323-
columnId,
324-
newColumn: column,
325-
keepParams: false,
326-
})
327-
);
328-
setInvalidOperationType(null);
329-
}}
330-
/>
331-
</EuiFormRow>
351+
setState(
352+
changeColumn({
353+
state,
354+
layerId,
355+
columnId,
356+
newColumn: column,
357+
keepParams: false,
358+
})
359+
);
360+
setInvalidOperationType(null);
361+
}}
362+
/>
363+
</EuiFormRow>
364+
) : null}
332365

333-
{!incompatibleSelectedOperationType && ParamEditor && (
366+
{!incompatibleSelectedOperationType && selectedColumn && ParamEditor && (
334367
<>
335368
<ParamEditor
336369
state={state}

0 commit comments

Comments
 (0)