Skip to content

Commit

Permalink
[Lens] Enable other bucket for top values operation (#82704)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Nov 11, 2020
1 parent 7fc5d1d commit be66841
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import {
EuiFormRow,
EuiSelect,
EuiSwitch,
EuiSwitchEvent,
EuiSpacer,
EuiPopover,
EuiButtonEmpty,
EuiText,
} from '@elastic/eui';
import { IndexPatternColumn } from '../../../indexpattern';
import { updateColumnParam } from '../../layer_helpers';
import { DataType } from '../../../../types';
Expand Down Expand Up @@ -34,6 +43,8 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn {
size: number;
orderBy: { type: 'alphabetical' } | { type: 'column'; columnId: string };
orderDirection: 'asc' | 'desc';
otherBucket?: boolean;
missingBucket?: boolean;
// Terms on numeric fields can be formatted
format?: {
id: string;
Expand Down Expand Up @@ -67,10 +78,11 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
newField &&
supportedTypes.has(newField.type) &&
newField.aggregatable &&
(!newField.aggregationRestrictions || newField.aggregationRestrictions.terms)
(!newField.aggregationRestrictions || newField.aggregationRestrictions.terms) &&
(!column.params.otherBucket || !newIndexPattern.hasRestrictions)
);
},
buildColumn({ columns, field }) {
buildColumn({ columns, field, indexPattern }) {
const existingMetricColumn = Object.entries(columns)
.filter(([_columnId, column]) => column && isSortableByColumn(column))
.map(([id]) => id)[0];
Expand All @@ -91,6 +103,8 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
? { type: 'column', columnId: existingMetricColumn }
: { type: 'alphabetical' },
orderDirection: existingMetricColumn ? 'desc' : 'asc',
otherBucket: !indexPattern.hasRestrictions,
missingBucket: false,
},
};
},
Expand All @@ -106,10 +120,14 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
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',
otherBucket: Boolean(column.params.otherBucket),
otherBucketLabel: i18n.translate('xpack.lens.indexPattern.terms.otherLabel', {
defaultMessage: 'Other',
}),
missingBucket: column.params.otherBucket && column.params.missingBucket,
missingBucketLabel: i18n.translate('xpack.lens.indexPattern.terms.missingLabel', {
defaultMessage: '(missing value)',
}),
},
};
},
Expand Down Expand Up @@ -143,7 +161,12 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}
return currentColumn;
},
paramEditor: ({ state, setState, currentColumn, layerId }) => {
paramEditor: function ParamEditor({ state, setState, currentColumn, layerId }) {
const indexPattern = currentColumn && state.indexPatterns[state.layers[layerId].indexPatternId];
const hasRestrictions = indexPattern.hasRestrictions;

const [popoverOpen, setPopoverOpen] = useState(false);

const SEPARATOR = '$$$';
function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) {
if (orderBy.type === 'alphabetical') {
Expand Down Expand Up @@ -201,6 +224,73 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}}
/>
</EuiFormRow>
{!hasRestrictions && (
<EuiText textAlign="right">
<EuiPopover
ownFocus
button={
<EuiButtonEmpty
size="xs"
iconType="arrowDown"
iconSide="right"
onClick={() => {
setPopoverOpen(true);
}}
>
{i18n.translate('xpack.lens.indexPattern.terms.advancedSettings', {
defaultMessage: 'Advanced',
})}
</EuiButtonEmpty>
}
isOpen={popoverOpen}
closePopover={() => {
setPopoverOpen(false);
}}
>
<EuiSwitch
label={i18n.translate('xpack.lens.indexPattern.terms.otherBucketDescription', {
defaultMessage: 'Group other values as "Other"',
})}
compressed
data-test-subj="indexPattern-terms-other-bucket"
checked={Boolean(currentColumn.params.otherBucket)}
onChange={(e: EuiSwitchEvent) =>
setState(
updateColumnParam({
state,
layerId,
currentColumn,
paramName: 'otherBucket',
value: e.target.checked,
})
)
}
/>
<EuiSpacer size="m" />
<EuiSwitch
label={i18n.translate('xpack.lens.indexPattern.terms.missingBucketDescription', {
defaultMessage: 'Include documents without this field',
})}
compressed
disabled={!currentColumn.params.otherBucket}
data-test-subj="indexPattern-terms-missing-bucket"
checked={Boolean(currentColumn.params.missingBucket)}
onChange={(e: EuiSwitchEvent) =>
setState(
updateColumnParam({
state,
layerId,
currentColumn,
paramName: 'missingBucket',
value: e.target.checked,
})
)
}
/>
</EuiPopover>
<EuiSpacer size="s" />
</EuiText>
)}
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', {
defaultMessage: 'Order by',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';
import { EuiRange, EuiSelect } from '@elastic/eui';
import { EuiRange, EuiSelect, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
Expand All @@ -33,7 +33,11 @@ describe('terms', () => {
beforeEach(() => {
state = {
indexPatternRefs: [],
indexPatterns: {},
indexPatterns: {
'1': {
hasRestrictions: false,
} as IndexPattern,
},
existingFields: {},
currentIndexPatternId: '1',
isFirstExistenceFetch: false,
Expand Down Expand Up @@ -69,8 +73,9 @@ describe('terms', () => {

describe('toEsAggsConfig', () => {
it('should reflect params correctly', () => {
const termsColumn = state.layers.first.columns.col1 as TermsIndexPatternColumn;
const esAggsConfig = termsOperation.toEsAggsConfig(
state.layers.first.columns.col1 as TermsIndexPatternColumn,
{ ...termsColumn, params: { ...termsColumn.params, otherBucket: true } },
'col1',
{} as IndexPattern
);
Expand All @@ -80,6 +85,27 @@ describe('terms', () => {
orderBy: '_key',
field: 'category',
size: 3,
otherBucket: true,
}),
})
);
});

it('should not enable missing bucket if other bucket is not set', () => {
const termsColumn = state.layers.first.columns.col1 as TermsIndexPatternColumn;
const esAggsConfig = termsOperation.toEsAggsConfig(
{
...termsColumn,
params: { ...termsColumn.params, otherBucket: false, missingBucket: true },
},
'col1',
{} as IndexPattern
);
expect(esAggsConfig).toEqual(
expect.objectContaining({
params: expect.objectContaining({
otherBucket: false,
missingBucket: false,
}),
})
);
Expand Down Expand Up @@ -249,6 +275,36 @@ describe('terms', () => {
expect(termsColumn.dataType).toEqual('boolean');
});

it('should set other bucket to true by default', () => {
const termsColumn = termsOperation.buildColumn({
indexPattern: createMockedIndexPattern(),
field: {
aggregatable: true,
searchable: true,
type: 'boolean',
name: 'test',
displayName: 'test',
},
columns: {},
});
expect(termsColumn.params.otherBucket).toEqual(true);
});

it('should set other bucket to false if index pattern has restrictions', () => {
const termsColumn = termsOperation.buildColumn({
indexPattern: { ...createMockedIndexPattern(), hasRestrictions: true },
field: {
aggregatable: true,
searchable: true,
type: 'boolean',
name: 'test',
displayName: 'test',
},
columns: {},
});
expect(termsColumn.params.otherBucket).toEqual(false);
});

it('should use existing metric column as order column', () => {
const termsColumn = termsOperation.buildColumn({
indexPattern: createMockedIndexPattern(),
Expand Down Expand Up @@ -400,6 +456,132 @@ describe('terms', () => {
});

describe('param editor', () => {
it('should render current other bucket value', () => {
const setStateSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
state={state}
setState={setStateSpy}
columnId="col1"
currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn}
layerId="first"
/>
);

const select = instance
.find('[data-test-subj="indexPattern-terms-other-bucket"]')
.find(EuiSwitch);

expect(select.prop('checked')).toEqual(false);
});

it('should hide other bucket setting for rollups', () => {
const setStateSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
state={{ ...state, indexPatterns: { '1': { hasRestrictions: true } as IndexPattern } }}
setState={setStateSpy}
columnId="col1"
currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn}
layerId="first"
/>
);

expect(instance.find('[data-test-subj="indexPattern-terms-other-bucket"]').length).toEqual(0);
});

it('should disable missing bucket setting as long as other bucket is not set', () => {
const setStateSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
state={state}
setState={setStateSpy}
columnId="col1"
currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn}
layerId="first"
/>
);

const select = instance
.find('[data-test-subj="indexPattern-terms-missing-bucket"]')
.find(EuiSwitch);

expect(select.prop('disabled')).toEqual(true);
});

it('should enable missing bucket setting as long as other bucket is set', () => {
const setStateSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
state={state}
setState={setStateSpy}
columnId="col1"
currentColumn={
{
...state.layers.first.columns.col1,
params: {
...state.layers.first.columns.col1.params,
otherBucket: true,
},
} as TermsIndexPatternColumn
}
layerId="first"
/>
);

const select = instance
.find('[data-test-subj="indexPattern-terms-missing-bucket"]')
.find(EuiSwitch);

expect(select.prop('disabled')).toEqual(false);
});

it('should update state when clicking other bucket toggle', () => {
const setStateSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
state={state}
setState={setStateSpy}
columnId="col1"
currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn}
layerId="first"
/>
);

instance
.find('[data-test-subj="indexPattern-terms-other-bucket"]')
.find(EuiSwitch)
.prop('onChange')!({
target: {
checked: true,
},
} as EuiSwitchEvent);

expect(setStateSpy).toHaveBeenCalledWith({
...state,
layers: {
first: {
...state.layers.first,
columns: {
...state.layers.first.columns,
col1: {
...state.layers.first.columns.col1,
params: {
...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params,
otherBucket: true,
},
},
},
},
},
});
});

it('should render current order by value and options', () => {
const setStateSpy = jest.fn();
const instance = shallow(
Expand Down
Loading

0 comments on commit be66841

Please sign in to comment.