mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Lens] Fieldless operations (#78080)
* [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>
This commit is contained in:
parent
db78d70df3
commit
0ebaf92a6a
20 changed files with 568 additions and 557 deletions
|
@ -17,7 +17,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { EuiFormLabel } from '@elastic/eui';
|
||||
import { IndexPatternColumn, OperationType } from '../indexpattern';
|
||||
import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel';
|
||||
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from './dimension_panel';
|
||||
import {
|
||||
operationDefinitionMap,
|
||||
getOperationDisplay,
|
||||
|
@ -36,7 +36,7 @@ const operationPanels = getOperationDisplay();
|
|||
|
||||
export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
|
||||
selectedColumn?: IndexPatternColumn;
|
||||
operationFieldSupportMatrix: OperationFieldSupportMatrix;
|
||||
operationSupportMatrix: OperationSupportMatrix;
|
||||
currentIndexPattern: IndexPattern;
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ const LabelInput = ({ value, onChange }: { value: string; onChange: (value: stri
|
|||
export function DimensionEditor(props: DimensionEditorProps) {
|
||||
const {
|
||||
selectedColumn,
|
||||
operationFieldSupportMatrix,
|
||||
operationSupportMatrix,
|
||||
state,
|
||||
columnId,
|
||||
setState,
|
||||
|
@ -98,14 +98,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
currentIndexPattern,
|
||||
hideGrouping,
|
||||
} = props;
|
||||
const { operationByField, fieldByOperation } = operationFieldSupportMatrix;
|
||||
const { operationByField, fieldByOperation } = operationSupportMatrix;
|
||||
const [
|
||||
incompatibleSelectedOperationType,
|
||||
setInvalidOperationType,
|
||||
] = useState<OperationType | null>(null);
|
||||
|
||||
const ParamEditor =
|
||||
selectedColumn && operationDefinitionMap[selectedColumn.operationType].paramEditor;
|
||||
const selectedOperationDefinition =
|
||||
selectedColumn && operationDefinitionMap[selectedColumn.operationType];
|
||||
|
||||
const ParamEditor = selectedOperationDefinition?.paramEditor;
|
||||
|
||||
const fieldMap: Record<string, IndexPatternField> = useMemo(() => {
|
||||
const fields: Record<string, IndexPatternField> = {};
|
||||
|
@ -129,6 +131,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
[
|
||||
...asOperationOptions(validOperationTypes, true),
|
||||
...asOperationOptions(possibleOperationTypes, false),
|
||||
...asOperationOptions(
|
||||
operationSupportMatrix.operationWithoutField,
|
||||
!selectedColumn || !hasField(selectedColumn)
|
||||
),
|
||||
],
|
||||
'operationType'
|
||||
);
|
||||
|
@ -166,12 +172,30 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
compatibleWithCurrentField ? '' : ' incompatible'
|
||||
}`,
|
||||
onClick() {
|
||||
// todo: when moving from terms agg to filters, we want to create a filter `$field.name : *`
|
||||
// it probably has to be re-thought when removing the field name.
|
||||
const isTermsToFilters =
|
||||
selectedColumn?.operationType === 'terms' && operationType === 'filters';
|
||||
|
||||
if (!selectedColumn || !compatibleWithCurrentField) {
|
||||
if (operationDefinitionMap[operationType].input === 'none') {
|
||||
// Clear invalid state because we are creating a valid column
|
||||
setInvalidOperationType(null);
|
||||
if (selectedColumn?.operationType === operationType) {
|
||||
return;
|
||||
}
|
||||
setState(
|
||||
changeColumn({
|
||||
state,
|
||||
layerId,
|
||||
columnId,
|
||||
newColumn: buildColumn({
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
layerId: props.layerId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
previousColumn: selectedColumn,
|
||||
}),
|
||||
})
|
||||
);
|
||||
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
|
||||
return;
|
||||
} else if (!selectedColumn || !compatibleWithCurrentField) {
|
||||
const possibleFields = fieldByOperation[operationType] || [];
|
||||
|
||||
if (possibleFields.length === 1) {
|
||||
|
@ -197,19 +221,20 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
|
||||
return;
|
||||
}
|
||||
if (incompatibleSelectedOperationType && !isTermsToFilters) {
|
||||
setInvalidOperationType(null);
|
||||
}
|
||||
if (selectedColumn.operationType === operationType) {
|
||||
|
||||
setInvalidOperationType(null);
|
||||
|
||||
if (selectedColumn?.operationType === operationType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newColumn: IndexPatternColumn = buildColumn({
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
layerId: props.layerId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field: fieldMap[selectedColumn.sourceField],
|
||||
field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined,
|
||||
previousColumn: selectedColumn,
|
||||
});
|
||||
|
||||
|
@ -244,93 +269,101 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
</div>
|
||||
<EuiSpacer size="s" />
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--shaded">
|
||||
<EuiFormRow
|
||||
data-test-subj="indexPattern-field-selection-row"
|
||||
label={i18n.translate('xpack.lens.indexPattern.chooseField', {
|
||||
defaultMessage: 'Choose a field',
|
||||
})}
|
||||
fullWidth
|
||||
isInvalid={Boolean(incompatibleSelectedOperationType)}
|
||||
error={
|
||||
selectedColumn
|
||||
? i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
|
||||
defaultMessage: 'To use this function, select a different field.',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<FieldSelect
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
fieldMap={fieldMap}
|
||||
operationFieldSupportMatrix={operationFieldSupportMatrix}
|
||||
selectedColumnOperationType={selectedColumn && selectedColumn.operationType}
|
||||
selectedColumnSourceField={
|
||||
selectedColumn && hasField(selectedColumn) ? selectedColumn.sourceField : undefined
|
||||
{!selectedColumn ||
|
||||
selectedOperationDefinition?.input === 'field' ||
|
||||
(incompatibleSelectedOperationType &&
|
||||
operationDefinitionMap[incompatibleSelectedOperationType].input === 'field') ? (
|
||||
<EuiFormRow
|
||||
data-test-subj="indexPattern-field-selection-row"
|
||||
label={i18n.translate('xpack.lens.indexPattern.chooseField', {
|
||||
defaultMessage: 'Choose a field',
|
||||
})}
|
||||
fullWidth
|
||||
isInvalid={Boolean(incompatibleSelectedOperationType)}
|
||||
error={
|
||||
selectedColumn && incompatibleSelectedOperationType
|
||||
? selectedOperationDefinition?.input === 'field'
|
||||
? i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
|
||||
defaultMessage: 'To use this function, select a different field.',
|
||||
})
|
||||
: i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
|
||||
defaultMessage: 'To use this function, select a field.',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
incompatibleSelectedOperationType={incompatibleSelectedOperationType}
|
||||
onDeleteColumn={() => {
|
||||
setState(
|
||||
deleteColumn({
|
||||
state,
|
||||
layerId,
|
||||
columnId,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChoose={(choice) => {
|
||||
let column: IndexPatternColumn;
|
||||
if (
|
||||
!incompatibleSelectedOperationType &&
|
||||
selectedColumn &&
|
||||
'field' in choice &&
|
||||
choice.operationType === selectedColumn.operationType
|
||||
) {
|
||||
// If we just changed the field are not in an error state and the operation didn't change,
|
||||
// we use the operations onFieldChange method to calculate the new column.
|
||||
column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]);
|
||||
} else {
|
||||
// Otherwise we'll use the buildColumn method to calculate a new column
|
||||
const compatibleOperations =
|
||||
('field' in choice &&
|
||||
operationFieldSupportMatrix.operationByField[choice.field]) ||
|
||||
[];
|
||||
let operation;
|
||||
if (compatibleOperations.length > 0) {
|
||||
operation =
|
||||
incompatibleSelectedOperationType &&
|
||||
compatibleOperations.includes(incompatibleSelectedOperationType)
|
||||
? incompatibleSelectedOperationType
|
||||
: compatibleOperations[0];
|
||||
} else if ('field' in choice) {
|
||||
operation = choice.operationType;
|
||||
}
|
||||
column = buildColumn({
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
field: fieldMap[choice.field],
|
||||
indexPattern: currentIndexPattern,
|
||||
layerId: props.layerId,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
op: operation as OperationType,
|
||||
previousColumn: selectedColumn,
|
||||
});
|
||||
>
|
||||
<FieldSelect
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
fieldMap={fieldMap}
|
||||
operationSupportMatrix={operationSupportMatrix}
|
||||
selectedColumnOperationType={selectedColumn && selectedColumn.operationType}
|
||||
selectedColumnSourceField={
|
||||
selectedColumn && hasField(selectedColumn) ? selectedColumn.sourceField : undefined
|
||||
}
|
||||
incompatibleSelectedOperationType={incompatibleSelectedOperationType}
|
||||
onDeleteColumn={() => {
|
||||
setState(
|
||||
deleteColumn({
|
||||
state,
|
||||
layerId,
|
||||
columnId,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChoose={(choice) => {
|
||||
let column: IndexPatternColumn;
|
||||
if (
|
||||
!incompatibleSelectedOperationType &&
|
||||
selectedColumn &&
|
||||
'field' in choice &&
|
||||
choice.operationType === selectedColumn.operationType
|
||||
) {
|
||||
// If we just changed the field are not in an error state and the operation didn't change,
|
||||
// we use the operations onFieldChange method to calculate the new column.
|
||||
column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]);
|
||||
} else {
|
||||
// Otherwise we'll use the buildColumn method to calculate a new column
|
||||
const compatibleOperations =
|
||||
('field' in choice && operationSupportMatrix.operationByField[choice.field]) ||
|
||||
[];
|
||||
let operation;
|
||||
if (compatibleOperations.length > 0) {
|
||||
operation =
|
||||
incompatibleSelectedOperationType &&
|
||||
compatibleOperations.includes(incompatibleSelectedOperationType)
|
||||
? incompatibleSelectedOperationType
|
||||
: compatibleOperations[0];
|
||||
} else if ('field' in choice) {
|
||||
operation = choice.operationType;
|
||||
}
|
||||
column = buildColumn({
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
field: fieldMap[choice.field],
|
||||
indexPattern: currentIndexPattern,
|
||||
layerId: props.layerId,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
op: operation as OperationType,
|
||||
previousColumn: selectedColumn,
|
||||
});
|
||||
}
|
||||
|
||||
setState(
|
||||
changeColumn({
|
||||
state,
|
||||
layerId,
|
||||
columnId,
|
||||
newColumn: column,
|
||||
keepParams: false,
|
||||
})
|
||||
);
|
||||
setInvalidOperationType(null);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
setState(
|
||||
changeColumn({
|
||||
state,
|
||||
layerId,
|
||||
columnId,
|
||||
newColumn: column,
|
||||
keepParams: false,
|
||||
})
|
||||
);
|
||||
setInvalidOperationType(null);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
|
||||
{!incompatibleSelectedOperationType && ParamEditor && (
|
||||
{!incompatibleSelectedOperationType && selectedColumn && ParamEditor && (
|
||||
<>
|
||||
<ParamEditor
|
||||
state={state}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/e
|
|||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
import { IndexPatternColumn } from '../operations';
|
||||
import { documentField } from '../document_field';
|
||||
import { OperationMetadata } from '../../types';
|
||||
|
||||
|
@ -81,12 +82,39 @@ const expectedIndexPatterns = {
|
|||
},
|
||||
};
|
||||
|
||||
const bytesColumn: IndexPatternColumn = {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
params: { format: { id: 'bytes' } },
|
||||
};
|
||||
|
||||
/**
|
||||
* The datasource exposes four main pieces of code which are tested at
|
||||
* an integration test level. The main reason for this fairly high level
|
||||
* of testing is that there is a lot of UI logic that isn't easily
|
||||
* unit tested, such as the transient invalid state.
|
||||
*
|
||||
* - Dimension trigger: Not tested here
|
||||
* - Dimension editor component: First half of the tests
|
||||
*
|
||||
* - canHandleDrop: Tests for dropping of fields or other dimensions
|
||||
* - onDrop: Correct application of drop logic
|
||||
*/
|
||||
describe('IndexPatternDimensionEditorPanel', () => {
|
||||
let state: IndexPatternPrivateState;
|
||||
let setState: jest.Mock;
|
||||
let defaultProps: IndexPatternDimensionEditorProps;
|
||||
let dragDropContext: DragContextState;
|
||||
|
||||
function getStateWithColumns(columns: Record<string, IndexPatternColumn>) {
|
||||
return { ...state, layers: { first: { ...state.layers.first, columns } } };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
indexPatternRefs: [],
|
||||
|
@ -179,7 +207,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
expect(filterOperations).toBeCalled();
|
||||
});
|
||||
|
||||
it('should show field select combo box on click', () => {
|
||||
it('should show field select', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
|
@ -187,6 +215,29 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not show field select on fieldless operation', () => {
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={getStateWithColumns({
|
||||
col1: {
|
||||
label: 'Filters',
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'filters',
|
||||
params: { filters: [] },
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not show any choices if the filter returns false', () => {
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
|
@ -250,26 +301,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={{
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
state={getStateWithColumns({ col1: bytesColumn })}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -292,26 +324,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={{
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
state={getStateWithColumns({ col1: bytesColumn })}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -324,30 +337,15 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain(
|
||||
'incompatible'
|
||||
);
|
||||
|
||||
// Fieldless operation is compatible with field
|
||||
expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain(
|
||||
'compatible'
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep the operation when switching to another field compatible with this operation', () => {
|
||||
const initialState: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
params: { format: { id: 'bytes' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const initialState: IndexPatternPrivateState = getStateWithColumns({ col1: bytesColumn });
|
||||
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent {...defaultProps} state={initialState} />
|
||||
|
@ -415,27 +413,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={{
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
params: { format: { id: 'bytes' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
state={getStateWithColumns({ col1: bytesColumn })}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -505,27 +483,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={{
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
params: { format: { id: 'bytes' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
state={getStateWithColumns({ col1: bytesColumn })}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -553,28 +511,13 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={{
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Custom label',
|
||||
customLabel: true,
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
params: { format: { id: 'bytes' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
state={getStateWithColumns({
|
||||
col1: {
|
||||
...bytesColumn,
|
||||
label: 'Custom label',
|
||||
customLabel: true,
|
||||
},
|
||||
}}
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -640,6 +583,62 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should leave error state if the original operation is re-selected', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} />);
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]')
|
||||
.simulate('click');
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]')
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should leave error state when switching from incomplete state to fieldless operation', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} />);
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]')
|
||||
.simulate('click');
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]')
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should leave error state when re-selecting the original fieldless function', () => {
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={getStateWithColumns({
|
||||
col1: {
|
||||
label: 'Filter',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
// Private
|
||||
operationType: 'filters',
|
||||
params: { filters: [] },
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]')
|
||||
.simulate('click');
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="lns-indexPatternDimension-filters"]')
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should indicate fields compatible with selected operation', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} />);
|
||||
|
||||
|
@ -701,28 +700,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('should select the Records field when count is selected', () => {
|
||||
const initialState: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'avg',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={initialState}
|
||||
state={getStateWithColumns({
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'avg',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
})}
|
||||
columnId="col2"
|
||||
/>
|
||||
);
|
||||
|
@ -737,28 +726,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('should indicate document and field compatibility with selected document operation', () => {
|
||||
const initialState: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={initialState}
|
||||
state={getStateWithColumns({
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
})}
|
||||
columnId="col2"
|
||||
/>
|
||||
);
|
||||
|
@ -942,28 +921,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('should indicate document compatibility when document operation is selected', () => {
|
||||
const initialState: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent
|
||||
{...defaultProps}
|
||||
state={initialState}
|
||||
state={getStateWithColumns({
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
})}
|
||||
columnId={'col2'}
|
||||
/>
|
||||
);
|
||||
|
@ -1031,26 +1000,9 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('should use helper function when changing the function', () => {
|
||||
const initialState: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
col1: {
|
||||
label: 'Max of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'max',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const initialState: IndexPatternPrivateState = getStateWithColumns({
|
||||
col1: bytesColumn,
|
||||
});
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent {...defaultProps} state={initialState} />
|
||||
);
|
||||
|
@ -1095,25 +1047,16 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('allows custom format', () => {
|
||||
const stateWithNumberCol: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Average of memory',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'memory',
|
||||
},
|
||||
},
|
||||
},
|
||||
const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({
|
||||
col1: {
|
||||
label: 'Average of memory',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'memory',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent {...defaultProps} state={stateWithNumberCol} />
|
||||
|
@ -1145,29 +1088,19 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('keeps decimal places while switching', () => {
|
||||
const stateWithNumberCol: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Average of memory',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'memory',
|
||||
params: {
|
||||
format: { id: 'bytes', params: { decimals: 0 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({
|
||||
col1: {
|
||||
label: 'Average of memory',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'memory',
|
||||
params: {
|
||||
format: { id: 'bytes', params: { decimals: 0 } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent {...defaultProps} state={stateWithNumberCol} />
|
||||
);
|
||||
|
@ -1195,28 +1128,19 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('allows custom format with number of decimal places', () => {
|
||||
const stateWithNumberCol: IndexPatternPrivateState = {
|
||||
...state,
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Average of memory',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'memory',
|
||||
params: {
|
||||
format: { id: 'bytes', params: { decimals: 2 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
const stateWithNumberCol: IndexPatternPrivateState = getStateWithColumns({
|
||||
col1: {
|
||||
label: 'Average of memory',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'memory',
|
||||
params: {
|
||||
format: { id: 'bytes', params: { decimals: 2 } },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionEditorComponent {...defaultProps} state={stateWithNumberCol} />
|
||||
|
|
|
@ -46,8 +46,9 @@ export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps<
|
|||
dateRange: DateRange;
|
||||
};
|
||||
|
||||
export interface OperationFieldSupportMatrix {
|
||||
export interface OperationSupportMatrix {
|
||||
operationByField: Partial<Record<string, OperationType[]>>;
|
||||
operationWithoutField: OperationType[];
|
||||
fieldByOperation: Partial<Record<OperationType, string[]>>;
|
||||
}
|
||||
|
||||
|
@ -58,7 +59,7 @@ type Props = Pick<
|
|||
|
||||
// TODO: This code has historically been memoized, as a potentially performance
|
||||
// sensitive task. If we can add memoization without breaking the behavior, we should.
|
||||
const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => {
|
||||
const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => {
|
||||
const layerId = props.layerId;
|
||||
const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId];
|
||||
|
||||
|
@ -67,37 +68,43 @@ const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatr
|
|||
).filter((operation) => props.filterOperations(operation.operationMetaData));
|
||||
|
||||
const supportedOperationsByField: Partial<Record<string, OperationType[]>> = {};
|
||||
const supportedOperationsWithoutField: OperationType[] = [];
|
||||
const supportedFieldsByOperation: Partial<Record<OperationType, string[]>> = {};
|
||||
|
||||
filteredOperationsByMetadata.forEach(({ operations }) => {
|
||||
operations.forEach((operation) => {
|
||||
if (supportedOperationsByField[operation.field]) {
|
||||
supportedOperationsByField[operation.field]!.push(operation.operationType);
|
||||
} else {
|
||||
supportedOperationsByField[operation.field] = [operation.operationType];
|
||||
}
|
||||
if (operation.type === 'field') {
|
||||
if (supportedOperationsByField[operation.field]) {
|
||||
supportedOperationsByField[operation.field]!.push(operation.operationType);
|
||||
} else {
|
||||
supportedOperationsByField[operation.field] = [operation.operationType];
|
||||
}
|
||||
|
||||
if (supportedFieldsByOperation[operation.operationType]) {
|
||||
supportedFieldsByOperation[operation.operationType]!.push(operation.field);
|
||||
} else {
|
||||
supportedFieldsByOperation[operation.operationType] = [operation.field];
|
||||
if (supportedFieldsByOperation[operation.operationType]) {
|
||||
supportedFieldsByOperation[operation.operationType]!.push(operation.field);
|
||||
} else {
|
||||
supportedFieldsByOperation[operation.operationType] = [operation.field];
|
||||
}
|
||||
} else if (operation.type === 'none') {
|
||||
supportedOperationsWithoutField.push(operation.operationType);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
operationByField: _.mapValues(supportedOperationsByField, _.uniq),
|
||||
operationWithoutField: _.uniq(supportedOperationsWithoutField),
|
||||
fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq),
|
||||
};
|
||||
};
|
||||
|
||||
export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPrivateState>) {
|
||||
const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props);
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
||||
const { dragging } = props.dragDropContext;
|
||||
const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationFieldSupportMatrix.operationByField[field.name]);
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedField(dragging)) {
|
||||
|
@ -119,11 +126,11 @@ export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPr
|
|||
}
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props);
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const droppedItem = props.droppedItem;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationFieldSupportMatrix.operationByField[field.name]);
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) {
|
||||
|
@ -167,8 +174,7 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPr
|
|||
return false;
|
||||
}
|
||||
|
||||
const operationsForNewField =
|
||||
operationFieldSupportMatrix.operationByField[droppedItem.field.name];
|
||||
const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name];
|
||||
|
||||
const layerId = props.layerId;
|
||||
const selectedColumn: IndexPatternColumn | null =
|
||||
|
@ -259,7 +265,7 @@ export const IndexPatternDimensionEditorComponent = function IndexPatternDimensi
|
|||
const layerId = props.layerId;
|
||||
const currentIndexPattern =
|
||||
props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId];
|
||||
const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props);
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
||||
const selectedColumn: IndexPatternColumn | null =
|
||||
props.state.layers[layerId].columns[props.columnId] || null;
|
||||
|
@ -269,7 +275,7 @@ export const IndexPatternDimensionEditorComponent = function IndexPatternDimensi
|
|||
{...props}
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
selectedColumn={selectedColumn}
|
||||
operationFieldSupportMatrix={operationFieldSupportMatrix}
|
||||
operationSupportMatrix={operationSupportMatrix}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ import { EuiHighlight } from '@elastic/eui';
|
|||
import { OperationType } from '../indexpattern';
|
||||
import { LensFieldIcon } from '../lens_field_icon';
|
||||
import { DataType } from '../../types';
|
||||
import { OperationFieldSupportMatrix } from './dimension_panel';
|
||||
import { OperationSupportMatrix } from './dimension_panel';
|
||||
import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { fieldExists } from '../pure_helpers';
|
||||
|
@ -37,7 +37,7 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> {
|
|||
incompatibleSelectedOperationType: OperationType | null;
|
||||
selectedColumnOperationType?: OperationType;
|
||||
selectedColumnSourceField?: string;
|
||||
operationFieldSupportMatrix: OperationFieldSupportMatrix;
|
||||
operationSupportMatrix: OperationSupportMatrix;
|
||||
onChoose: (choice: FieldChoice) => void;
|
||||
onDeleteColumn: () => void;
|
||||
existingFields: IndexPatternPrivateState['existingFields'];
|
||||
|
@ -49,13 +49,13 @@ export function FieldSelect({
|
|||
incompatibleSelectedOperationType,
|
||||
selectedColumnOperationType,
|
||||
selectedColumnSourceField,
|
||||
operationFieldSupportMatrix,
|
||||
operationSupportMatrix,
|
||||
onChoose,
|
||||
onDeleteColumn,
|
||||
existingFields,
|
||||
...rest
|
||||
}: FieldSelectProps) {
|
||||
const { operationByField } = operationFieldSupportMatrix;
|
||||
const { operationByField } = operationSupportMatrix;
|
||||
const memoizedFieldOptions = useMemo(() => {
|
||||
const fields = Object.keys(operationByField).sort();
|
||||
|
||||
|
@ -173,15 +173,13 @@ export function FieldSelect({
|
|||
options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]}
|
||||
isInvalid={Boolean(incompatibleSelectedOperationType)}
|
||||
selectedOptions={
|
||||
((selectedColumnOperationType
|
||||
? selectedColumnSourceField
|
||||
? [
|
||||
{
|
||||
label: fieldMap[selectedColumnSourceField].displayName,
|
||||
value: { type: 'field', field: selectedColumnSourceField },
|
||||
},
|
||||
]
|
||||
: [memoizedFieldOptions[0]]
|
||||
((selectedColumnOperationType && selectedColumnSourceField
|
||||
? [
|
||||
{
|
||||
label: fieldMap[selectedColumnSourceField].displayName,
|
||||
value: { type: 'field', field: selectedColumnSourceField },
|
||||
},
|
||||
]
|
||||
: []) as unknown) as EuiComboBoxOptionOption[]
|
||||
}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
|
|
|
@ -483,11 +483,15 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId
|
|||
const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] };
|
||||
const currentFields = state.indexPatterns[state.currentIndexPatternId].fields;
|
||||
const firstBucketLabel =
|
||||
currentFields.find((field) => field.name === layer.columns[firstBucket].sourceField)
|
||||
?.displayName || '';
|
||||
currentFields.find((field) => {
|
||||
const column = layer.columns[firstBucket];
|
||||
return hasField(column) && column.sourceField === field.name;
|
||||
})?.displayName || '';
|
||||
const secondBucketLabel =
|
||||
currentFields.find((field) => field.name === layer.columns[secondBucket].sourceField)
|
||||
?.displayName || '';
|
||||
currentFields.find((field) => {
|
||||
const column = layer.columns[secondBucket];
|
||||
return hasField(column) && column.sourceField === field.name;
|
||||
})?.displayName || '';
|
||||
|
||||
return buildSuggestion({
|
||||
state,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OperationDefinition } from './index';
|
||||
import { FormattedIndexPatternColumn } from './column_types';
|
||||
import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types';
|
||||
|
||||
const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']);
|
||||
|
||||
|
@ -21,15 +21,18 @@ function ofName(name: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export interface CardinalityIndexPatternColumn extends FormattedIndexPatternColumn {
|
||||
export interface CardinalityIndexPatternColumn
|
||||
extends FormattedIndexPatternColumn,
|
||||
FieldBasedIndexPatternColumn {
|
||||
operationType: 'cardinality';
|
||||
}
|
||||
|
||||
export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternColumn> = {
|
||||
export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternColumn, 'field'> = {
|
||||
type: OPERATION_TYPE,
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.cardinality', {
|
||||
defaultMessage: 'Unique count',
|
||||
}),
|
||||
input: 'field',
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
if (
|
||||
supportedTypes.has(type) &&
|
||||
|
|
|
@ -14,7 +14,6 @@ import { Operation, DimensionPriority } from '../../../types';
|
|||
export interface BaseIndexPatternColumn extends Operation {
|
||||
// Private
|
||||
operationType: string;
|
||||
sourceField: string;
|
||||
suggestedPriority?: DimensionPriority;
|
||||
customLabel?: boolean;
|
||||
}
|
||||
|
@ -31,23 +30,6 @@ export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base type for a column that doesn't have additional parameter.
|
||||
*
|
||||
* * `TOperationType` should be a string type containing just the type
|
||||
* of the operation (e.g. `"sum"`).
|
||||
* * `TBase` is the base column interface the operation type is set for -
|
||||
* by default this is `FieldBasedIndexPatternColumn`, so
|
||||
* `ParameterlessIndexPatternColumn<'foo'>` will give you a column type
|
||||
* for an operation named foo that operates on a field.
|
||||
* By passing in another `TBase` (e.g. just `BaseIndexPatternColumn`),
|
||||
* you can also create other column types.
|
||||
*/
|
||||
export type ParameterlessIndexPatternColumn<
|
||||
TOperationType extends string,
|
||||
TBase extends BaseIndexPatternColumn = FieldBasedIndexPatternColumn
|
||||
> = TBase & { operationType: TOperationType };
|
||||
|
||||
export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn {
|
||||
suggestedPriority?: DimensionPriority;
|
||||
sourceField: string;
|
||||
}
|
||||
|
|
|
@ -6,23 +6,25 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OperationDefinition } from './index';
|
||||
import { FormattedIndexPatternColumn } from './column_types';
|
||||
import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types';
|
||||
import { IndexPatternField } from '../../types';
|
||||
|
||||
const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', {
|
||||
defaultMessage: 'Count of records',
|
||||
});
|
||||
|
||||
export type CountIndexPatternColumn = FormattedIndexPatternColumn & {
|
||||
operationType: 'count';
|
||||
};
|
||||
export type CountIndexPatternColumn = FormattedIndexPatternColumn &
|
||||
FieldBasedIndexPatternColumn & {
|
||||
operationType: 'count';
|
||||
};
|
||||
|
||||
export const countOperation: OperationDefinition<CountIndexPatternColumn> = {
|
||||
export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field'> = {
|
||||
type: 'count',
|
||||
priority: 2,
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.count', {
|
||||
defaultMessage: 'Count',
|
||||
}),
|
||||
input: 'field',
|
||||
onFieldChange: (oldColumn, indexPattern, field) => {
|
||||
return {
|
||||
...oldColumn,
|
||||
|
|
|
@ -36,11 +36,15 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC
|
|||
};
|
||||
}
|
||||
|
||||
export const dateHistogramOperation: OperationDefinition<DateHistogramIndexPatternColumn> = {
|
||||
export const dateHistogramOperation: OperationDefinition<
|
||||
DateHistogramIndexPatternColumn,
|
||||
'field'
|
||||
> = {
|
||||
type: 'date_histogram',
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', {
|
||||
defaultMessage: 'Date histogram',
|
||||
}),
|
||||
input: 'field',
|
||||
priority: 5, // Highest priority level used
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
if (
|
||||
|
@ -136,7 +140,7 @@ export const dateHistogramOperation: OperationDefinition<DateHistogramIndexPatte
|
|||
},
|
||||
};
|
||||
},
|
||||
paramEditor: ({ state, setState, currentColumn: currentColumn, layerId, dateRange, data }) => {
|
||||
paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => {
|
||||
const field =
|
||||
currentColumn &&
|
||||
state.indexPatterns[state.layers[layerId].indexPatternId].fields.find(
|
||||
|
|
|
@ -59,7 +59,6 @@ describe('filters', () => {
|
|||
operationType: 'filters',
|
||||
scale: 'ordinal',
|
||||
isBucketed: true,
|
||||
sourceField: 'Records',
|
||||
params: {
|
||||
filters: [
|
||||
{
|
||||
|
@ -112,34 +111,14 @@ describe('filters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getPossibleOperationForField', () => {
|
||||
describe('getPossibleOperation', () => {
|
||||
it('should return operation with the right type for document', () => {
|
||||
expect(
|
||||
filtersOperation.getPossibleOperationForField({
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
name: 'test',
|
||||
displayName: 'test',
|
||||
type: 'document',
|
||||
})
|
||||
).toEqual({
|
||||
expect(filtersOperation.getPossibleOperation()).toEqual({
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
scale: 'ordinal',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return operation if field type is not document', () => {
|
||||
expect(
|
||||
filtersOperation.getPossibleOperationForField({
|
||||
aggregatable: false,
|
||||
searchable: true,
|
||||
name: 'test',
|
||||
displayName: 'test',
|
||||
type: 'string',
|
||||
})
|
||||
).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popover param editor', () => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui';
|
||||
import { updateColumnParam } from '../../../state_helpers';
|
||||
import { OperationDefinition } from '../index';
|
||||
import { FieldBasedIndexPatternColumn } from '../column_types';
|
||||
import { BaseIndexPatternColumn } from '../column_types';
|
||||
import { FilterPopover } from './filter_popover';
|
||||
import { IndexPattern } from '../../../types';
|
||||
import { Query, esKuery, esQuery } from '../../../../../../../../src/plugins/data/public';
|
||||
|
@ -61,31 +61,22 @@ export const isQueryValid = (input: Query, indexPattern: IndexPattern) => {
|
|||
}
|
||||
};
|
||||
|
||||
export interface FiltersIndexPatternColumn extends FieldBasedIndexPatternColumn {
|
||||
export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn {
|
||||
operationType: 'filters';
|
||||
params: {
|
||||
filters: Filter[];
|
||||
};
|
||||
}
|
||||
|
||||
export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn> = {
|
||||
export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'none'> = {
|
||||
type: 'filters',
|
||||
displayName: filtersLabel,
|
||||
priority: 3, // Higher than any metric
|
||||
getPossibleOperationForField: ({ type }) => {
|
||||
if (type === 'document') {
|
||||
return {
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
scale: 'ordinal',
|
||||
};
|
||||
}
|
||||
},
|
||||
isTransferable: () => false,
|
||||
|
||||
onFieldChange: (oldColumn, indexPattern, field) => oldColumn,
|
||||
input: 'none',
|
||||
isTransferable: () => true,
|
||||
|
||||
buildColumn({ suggestedPriority, field, previousColumn }) {
|
||||
buildColumn({ suggestedPriority, previousColumn }) {
|
||||
let params = { filters: [defaultFilter] };
|
||||
if (previousColumn?.operationType === 'terms') {
|
||||
params = {
|
||||
|
@ -108,11 +99,18 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn> =
|
|||
scale: 'ordinal',
|
||||
suggestedPriority,
|
||||
isBucketed: true,
|
||||
sourceField: field.name,
|
||||
params,
|
||||
};
|
||||
},
|
||||
|
||||
getPossibleOperation() {
|
||||
return {
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
scale: 'ordinal',
|
||||
};
|
||||
},
|
||||
|
||||
toEsAggsConfig: (column, columnId, indexPattern) => {
|
||||
const validFilters = column.params.filters?.filter((f: Filter) =>
|
||||
isQueryValid(f.input, indexPattern)
|
||||
|
|
|
@ -28,22 +28,6 @@ import { DateRange } from '../../../../common';
|
|||
import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public';
|
||||
import { RangeIndexPatternColumn, rangeOperation } from './ranges';
|
||||
|
||||
// List of all operation definitions registered to this data source.
|
||||
// If you want to implement a new operation, add the definition to this array and
|
||||
// the column type to the `IndexPatternColumn` union type below.
|
||||
const internalOperationDefinitions = [
|
||||
filtersOperation,
|
||||
termsOperation,
|
||||
dateHistogramOperation,
|
||||
minOperation,
|
||||
maxOperation,
|
||||
averageOperation,
|
||||
cardinalityOperation,
|
||||
sumOperation,
|
||||
countOperation,
|
||||
rangeOperation,
|
||||
];
|
||||
|
||||
/**
|
||||
* A union type of all available column types. If a column is of an unknown type somewhere
|
||||
* withing the indexpattern data source it should be typed as `IndexPatternColumn` to make
|
||||
|
@ -61,6 +45,24 @@ export type IndexPatternColumn =
|
|||
| SumIndexPatternColumn
|
||||
| CountIndexPatternColumn;
|
||||
|
||||
export type FieldBasedIndexPatternColumn = Extract<IndexPatternColumn, { sourceField: string }>;
|
||||
|
||||
// List of all operation definitions registered to this data source.
|
||||
// If you want to implement a new operation, add the definition to this array and
|
||||
// the column type to the `IndexPatternColumn` union type below.
|
||||
const internalOperationDefinitions = [
|
||||
filtersOperation,
|
||||
termsOperation,
|
||||
dateHistogramOperation,
|
||||
minOperation,
|
||||
maxOperation,
|
||||
averageOperation,
|
||||
cardinalityOperation,
|
||||
sumOperation,
|
||||
countOperation,
|
||||
rangeOperation,
|
||||
];
|
||||
|
||||
export { termsOperation } from './terms';
|
||||
export { rangeOperation } from './ranges';
|
||||
export { filtersOperation } from './filters';
|
||||
|
@ -71,7 +73,7 @@ export { countOperation } from './count';
|
|||
/**
|
||||
* Properties passed to the operation-specific part of the popover editor
|
||||
*/
|
||||
export interface ParamEditorProps<C extends BaseIndexPatternColumn> {
|
||||
export interface ParamEditorProps<C> {
|
||||
currentColumn: C;
|
||||
state: IndexPatternPrivateState;
|
||||
setState: StateSetter<IndexPatternPrivateState>;
|
||||
|
@ -138,13 +140,25 @@ interface BaseBuildColumnArgs {
|
|||
indexPattern: IndexPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of an operation definition. If the type parameter of the definition
|
||||
* indicates a field based column, `getPossibleOperationForField` has to be
|
||||
* specified, otherwise `getPossibleOperationForDocument` has to be defined.
|
||||
*/
|
||||
export interface OperationDefinition<C extends BaseIndexPatternColumn>
|
||||
extends BaseOperationDefinitionProps<C> {
|
||||
interface FieldlessOperationDefinition<C extends BaseIndexPatternColumn> {
|
||||
input: 'none';
|
||||
/**
|
||||
* Builds the column object for the given parameters. Should include default p
|
||||
*/
|
||||
buildColumn: (
|
||||
arg: BaseBuildColumnArgs & {
|
||||
previousColumn?: IndexPatternColumn;
|
||||
}
|
||||
) => C;
|
||||
/**
|
||||
* Returns the meta data of the operation if applied. Undefined
|
||||
* if the field is not applicable.
|
||||
*/
|
||||
getPossibleOperation: () => OperationMetadata | undefined;
|
||||
}
|
||||
|
||||
interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
|
||||
input: 'field';
|
||||
/**
|
||||
* Returns the meta data of the operation if applied to the given field. Undefined
|
||||
* if the field is not applicable to the operation.
|
||||
|
@ -156,7 +170,8 @@ export interface OperationDefinition<C extends BaseIndexPatternColumn>
|
|||
buildColumn: (
|
||||
arg: BaseBuildColumnArgs & {
|
||||
field: IndexPatternField;
|
||||
previousColumn?: IndexPatternColumn;
|
||||
// previousColumn?: IndexPatternColumn;
|
||||
previousColumn?: C;
|
||||
}
|
||||
) => C;
|
||||
/**
|
||||
|
@ -175,9 +190,29 @@ export interface OperationDefinition<C extends BaseIndexPatternColumn>
|
|||
* @param indexPattern The index pattern that field is on.
|
||||
* @param field The field that the user changed to.
|
||||
*/
|
||||
onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C;
|
||||
onFieldChange: (
|
||||
// oldColumn: FieldBasedIndexPatternColumn,
|
||||
oldColumn: C,
|
||||
indexPattern: IndexPattern,
|
||||
field: IndexPatternField
|
||||
) => C;
|
||||
}
|
||||
|
||||
interface OperationDefinitionMap<C extends BaseIndexPatternColumn> {
|
||||
field: FieldBasedOperationDefinition<C>;
|
||||
none: FieldlessOperationDefinition<C>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of an operation definition. If the type parameter of the definition
|
||||
* indicates a field based column, `getPossibleOperationForField` has to be
|
||||
* specified, otherwise `getPossibleOperation` has to be defined.
|
||||
*/
|
||||
export type OperationDefinition<
|
||||
C extends BaseIndexPatternColumn,
|
||||
Input extends keyof OperationDefinitionMap<C>
|
||||
> = BaseOperationDefinitionProps<C> & OperationDefinitionMap<C>[Input];
|
||||
|
||||
/**
|
||||
* A union type of all available operation types. The operation type is a unique id of an operation.
|
||||
* Each column is assigned to exactly one operation type.
|
||||
|
@ -188,7 +223,9 @@ export type OperationType = typeof internalOperationDefinitions[number]['type'];
|
|||
* This is an operation definition of an unspecified column out of all possible
|
||||
* column types.
|
||||
*/
|
||||
export type GenericOperationDefinition = OperationDefinition<IndexPatternColumn>;
|
||||
export type GenericOperationDefinition =
|
||||
| OperationDefinition<IndexPatternColumn, 'field'>
|
||||
| OperationDefinition<IndexPatternColumn, 'none'>;
|
||||
|
||||
/**
|
||||
* List of all available operation definitions
|
||||
|
@ -206,7 +243,10 @@ export const operationDefinitions = internalOperationDefinitions as GenericOpera
|
|||
* (e.g. `import { termsOperation } from './operations/definitions'`). This map is
|
||||
* intended to be used in situations where the operation type is not known during compile time.
|
||||
*/
|
||||
export const operationDefinitionMap = internalOperationDefinitions.reduce(
|
||||
export const operationDefinitionMap: Record<
|
||||
string,
|
||||
GenericOperationDefinition
|
||||
> = internalOperationDefinitions.reduce(
|
||||
(definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }),
|
||||
{}
|
||||
) as Record<OperationType, GenericOperationDefinition>;
|
||||
);
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OperationDefinition } from './index';
|
||||
import { FormattedIndexPatternColumn } from './column_types';
|
||||
import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types';
|
||||
|
||||
type MetricColumn<T> = FormattedIndexPatternColumn & {
|
||||
operationType: T;
|
||||
};
|
||||
type MetricColumn<T> = FormattedIndexPatternColumn &
|
||||
FieldBasedIndexPatternColumn & {
|
||||
operationType: T;
|
||||
};
|
||||
|
||||
function buildMetricOperation<T extends MetricColumn<string>>({
|
||||
type,
|
||||
|
@ -27,6 +28,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
type,
|
||||
priority,
|
||||
displayName,
|
||||
input: 'field',
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => {
|
||||
if (
|
||||
fieldType === 'number' &&
|
||||
|
@ -78,7 +80,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
missing: 0,
|
||||
},
|
||||
}),
|
||||
} as OperationDefinition<T>;
|
||||
} as OperationDefinition<T, 'field'>;
|
||||
}
|
||||
|
||||
export type SumIndexPatternColumn = MetricColumn<'sum'>;
|
||||
|
|
|
@ -76,12 +76,13 @@ function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) {
|
|||
};
|
||||
}
|
||||
|
||||
export const rangeOperation: OperationDefinition<RangeIndexPatternColumn> = {
|
||||
export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field'> = {
|
||||
type: 'range',
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.ranges', {
|
||||
defaultMessage: 'Ranges',
|
||||
}),
|
||||
priority: 4, // Higher than terms, so numbers get histogram
|
||||
input: 'field',
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
if (
|
||||
type === 'number' &&
|
||||
|
|
|
@ -48,12 +48,13 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn {
|
|||
};
|
||||
}
|
||||
|
||||
export const termsOperation: OperationDefinition<TermsIndexPatternColumn> = {
|
||||
export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field'> = {
|
||||
type: 'terms',
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.terms', {
|
||||
defaultMessage: 'Top values',
|
||||
}),
|
||||
priority: 3, // Higher than any metric
|
||||
input: 'field',
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
if (
|
||||
supportedTypes.has(type) &&
|
||||
|
@ -95,23 +96,25 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn> = {
|
|||
},
|
||||
};
|
||||
},
|
||||
toEsAggsConfig: (column, columnId, _indexPattern) => ({
|
||||
id: columnId,
|
||||
enabled: true,
|
||||
type: 'terms',
|
||||
schema: 'segment',
|
||||
params: {
|
||||
field: column.sourceField,
|
||||
orderBy:
|
||||
column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId,
|
||||
order: column.params.orderDirection,
|
||||
size: column.params.size,
|
||||
otherBucket: false,
|
||||
otherBucketLabel: 'Other',
|
||||
missingBucket: false,
|
||||
missingBucketLabel: 'Missing',
|
||||
},
|
||||
}),
|
||||
toEsAggsConfig: (column, columnId, _indexPattern) => {
|
||||
return {
|
||||
id: columnId,
|
||||
enabled: true,
|
||||
type: 'terms',
|
||||
schema: 'segment',
|
||||
params: {
|
||||
field: column.sourceField,
|
||||
orderBy:
|
||||
column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId,
|
||||
order: column.params.orderDirection,
|
||||
size: column.params.size,
|
||||
otherBucket: false,
|
||||
otherBucketLabel: 'Other',
|
||||
missingBucket: false,
|
||||
missingBucketLabel: 'Missing',
|
||||
},
|
||||
};
|
||||
},
|
||||
onFieldChange: (oldColumn, indexPattern, field) => {
|
||||
return {
|
||||
...oldColumn,
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
*/
|
||||
|
||||
export * from './operations';
|
||||
export { OperationType, IndexPatternColumn } from './definitions';
|
||||
export { OperationType, IndexPatternColumn, FieldBasedIndexPatternColumn } from './definitions';
|
||||
|
|
|
@ -182,7 +182,7 @@ describe('getOperationTypesForField', () => {
|
|||
},
|
||||
};
|
||||
|
||||
it('should build a column for the given operation type if it is passed in', () => {
|
||||
it('should build a column for the given field-based operation type if it is passed in', () => {
|
||||
const column = buildColumn({
|
||||
layerId: 'first',
|
||||
indexPattern: expectedIndexPatterns[1],
|
||||
|
@ -194,6 +194,17 @@ describe('getOperationTypesForField', () => {
|
|||
expect(column.operationType).toEqual('count');
|
||||
});
|
||||
|
||||
it('should build a column for the given no-input operation type if it is passed in', () => {
|
||||
const column = buildColumn({
|
||||
layerId: 'first',
|
||||
indexPattern: expectedIndexPatterns[1],
|
||||
columns: state.layers.first.columns,
|
||||
suggestedPriority: 0,
|
||||
op: 'filters',
|
||||
});
|
||||
expect(column.operationType).toEqual('filters');
|
||||
});
|
||||
|
||||
it('should build a column for the given operation type and field if it is passed in', () => {
|
||||
const field = expectedIndexPatterns[1].fields[1];
|
||||
const column = buildColumn({
|
||||
|
@ -222,7 +233,7 @@ describe('getOperationTypesForField', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should list out all field-operation tuples for different operation meta data', () => {
|
||||
it('should list out all operation tuples', () => {
|
||||
expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
@ -255,13 +266,17 @@ describe('getOperationTypesForField', () => {
|
|||
},
|
||||
Object {
|
||||
"operationMetaData": Object {
|
||||
"dataType": "number",
|
||||
"dataType": "string",
|
||||
"isBucketed": true,
|
||||
"scale": "ordinal",
|
||||
},
|
||||
"operations": Array [
|
||||
Object {
|
||||
"field": "bytes",
|
||||
"operationType": "filters",
|
||||
"type": "none",
|
||||
},
|
||||
Object {
|
||||
"field": "source",
|
||||
"operationType": "terms",
|
||||
"type": "field",
|
||||
},
|
||||
|
@ -269,13 +284,13 @@ describe('getOperationTypesForField', () => {
|
|||
},
|
||||
Object {
|
||||
"operationMetaData": Object {
|
||||
"dataType": "string",
|
||||
"dataType": "number",
|
||||
"isBucketed": true,
|
||||
"scale": "ordinal",
|
||||
},
|
||||
"operations": Array [
|
||||
Object {
|
||||
"field": "source",
|
||||
"field": "bytes",
|
||||
"operationType": "terms",
|
||||
"type": "field",
|
||||
},
|
||||
|
|
|
@ -63,7 +63,7 @@ export function getOperationTypesForField(field: IndexPatternField): OperationTy
|
|||
return operationDefinitions
|
||||
.filter(
|
||||
(operationDefinition) =>
|
||||
'getPossibleOperationForField' in operationDefinition &&
|
||||
operationDefinition.input === 'field' &&
|
||||
operationDefinition.getPossibleOperationForField(field)
|
||||
)
|
||||
.sort(getSortScoreByPriority)
|
||||
|
@ -80,11 +80,16 @@ export function isDocumentOperation(type: string) {
|
|||
return documentOperations.has(type);
|
||||
}
|
||||
|
||||
interface OperationFieldTuple {
|
||||
type: 'field';
|
||||
operationType: OperationType;
|
||||
field: string;
|
||||
}
|
||||
type OperationFieldTuple =
|
||||
| {
|
||||
type: 'field';
|
||||
operationType: OperationType;
|
||||
field: string;
|
||||
}
|
||||
| {
|
||||
type: 'none';
|
||||
operationType: OperationType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all possible operations (matches between operations and fields of the index
|
||||
|
@ -100,11 +105,18 @@ interface OperationFieldTuple {
|
|||
* [
|
||||
* {
|
||||
* operationMetaData: { dataType: 'string', isBucketed: true },
|
||||
* operations: ['terms']
|
||||
* operations: [{
|
||||
* type: 'field',
|
||||
* operationType: ['terms'],
|
||||
* field: 'keyword'
|
||||
* }]
|
||||
* },
|
||||
* {
|
||||
* operationMetaData: { dataType: 'number', isBucketed: false },
|
||||
* operations: ['avg', 'min', 'max']
|
||||
* operationMetaData: { dataType: 'string', isBucketed: true },
|
||||
* operations: [{
|
||||
* type: 'none',
|
||||
* operationType: ['filters'],
|
||||
* }]
|
||||
* },
|
||||
* ]
|
||||
* ```
|
||||
|
@ -133,30 +145,31 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) {
|
|||
};
|
||||
|
||||
operationDefinitions.sort(getSortScoreByPriority).forEach((operationDefinition) => {
|
||||
indexPattern.fields.forEach((field) => {
|
||||
if (operationDefinition.input === 'field') {
|
||||
indexPattern.fields.forEach((field) => {
|
||||
addToMap(
|
||||
{
|
||||
type: 'field',
|
||||
operationType: operationDefinition.type,
|
||||
field: field.name,
|
||||
},
|
||||
operationDefinition.getPossibleOperationForField(field)
|
||||
);
|
||||
});
|
||||
} else if (operationDefinition.input === 'none') {
|
||||
addToMap(
|
||||
{
|
||||
type: 'field',
|
||||
type: 'none',
|
||||
operationType: operationDefinition.type,
|
||||
field: field.name,
|
||||
},
|
||||
getPossibleOperationForField(operationDefinition, field)
|
||||
operationDefinition.getPossibleOperation()
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(operationByMetadata);
|
||||
}
|
||||
|
||||
function getPossibleOperationForField(
|
||||
operationDefinition: GenericOperationDefinition,
|
||||
field: IndexPatternField
|
||||
): OperationMetadata | undefined {
|
||||
return 'getPossibleOperationForField' in operationDefinition
|
||||
? operationDefinition.getPossibleOperationForField(field)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of
|
||||
* the operation definition of the column. Returns a new column object with the field changed.
|
||||
|
@ -171,13 +184,13 @@ export function changeField(
|
|||
) {
|
||||
const operationDefinition = operationDefinitionMap[column.operationType];
|
||||
|
||||
if (!('onFieldChange' in operationDefinition)) {
|
||||
if (operationDefinition.input === 'field' && 'sourceField' in column) {
|
||||
return operationDefinition.onFieldChange(column, indexPattern, newField);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invariant error: Cannot change field if operation isn't a field based operaiton"
|
||||
);
|
||||
}
|
||||
|
||||
return operationDefinition.onFieldChange(column, indexPattern, newField);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,7 +216,7 @@ export function buildColumn({
|
|||
suggestedPriority: DimensionPriority | undefined;
|
||||
layerId: string;
|
||||
indexPattern: IndexPattern;
|
||||
field: IndexPatternField;
|
||||
field?: IndexPatternField;
|
||||
previousColumn?: IndexPatternColumn;
|
||||
}): IndexPatternColumn {
|
||||
const operationDefinition = operationDefinitionMap[op];
|
||||
|
@ -220,16 +233,18 @@ export function buildColumn({
|
|||
previousColumn,
|
||||
};
|
||||
|
||||
if (operationDefinition.input === 'none') {
|
||||
return operationDefinition.buildColumn(baseOptions);
|
||||
}
|
||||
|
||||
if (!field) {
|
||||
throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`);
|
||||
}
|
||||
|
||||
const newColumn = operationDefinition.buildColumn({
|
||||
return operationDefinition.buildColumn({
|
||||
...baseOptions,
|
||||
field,
|
||||
});
|
||||
|
||||
return newColumn;
|
||||
}
|
||||
|
||||
export { operationDefinitionMap } from './definitions';
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { isColumnTransferable } from './operations';
|
||||
import { operationDefinitionMap, IndexPatternColumn } from './operations';
|
||||
import { isColumnTransferable, operationDefinitionMap, IndexPatternColumn } from './operations';
|
||||
import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types';
|
||||
|
||||
export function updateColumnParam<C extends IndexPatternColumn, K extends keyof C['params']>({
|
||||
|
|
|
@ -67,11 +67,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
// legend item(s), so we're using a class selector here.
|
||||
expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3);
|
||||
});
|
||||
|
||||
it('should create an xy visualization with filters aggregation', async () => {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await listingTable.searchForItemWithName('lnsXYvis');
|
||||
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
// Change the IP field to filters
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger',
|
||||
operation: 'filters',
|
||||
|
@ -79,6 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
await PageObjects.lens.addFilterToAgg(`geo.src : CN`);
|
||||
|
||||
// Verify that the field was persisted from the transition
|
||||
expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]);
|
||||
expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue