Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Add bucket nesting editor to indexpattern #42869

Merged
merged 7 commits into from
Aug 14, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { mount } from 'enzyme';
import React from 'react';
import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPatternColumn } from '../indexpattern';

describe('BucketNestingEditor', () => {
function mockCol(col: Partial<IndexPatternColumn> = {}): IndexPatternColumn {
const result = {
dataType: 'string',
isBucketed: true,
label: 'a',
operationType: 'terms',
params: {
size: 5,
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
},
sourceField: 'a',
suggestedPriority: 0,
...col,
};

return result as IndexPatternColumn;
}

it('should display an unchecked switch if there are two buckets and it is the root', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
setColumns={jest.fn()}
/>
);
const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first();

expect(control.prop('checked')).toBeFalsy();
});

it('should display a checked switch if there are two buckets and it is not the root', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['b', 'a', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
setColumns={jest.fn()}
/>
);
const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first();

expect(control.prop('checked')).toBeTruthy();
});

it('should reorder the columns when toggled', () => {
const setColumns = jest.fn();
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['b', 'a', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
setColumns={setColumns}
/>
);
const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first();

(control.prop('onChange') as () => {})();

expect(setColumns).toHaveBeenCalledWith(['a', 'b', 'c']);
});

it('should display nothing if there are no buckets', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this is the same test as the one above, maybe you forgot to implement it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops. You're right. Copy/paste for the win.

const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'avg', isBucketed: false }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: false }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
setColumns={jest.fn()}
/>
);

expect(component.children().length).toBe(0);
});

it('should display nothing if there is one bucket', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: false }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
setColumns={jest.fn()}
/>
);

expect(component.children().length).toBe(0);
});

it('should display a dropdown with the parent column selected if 3+ buckets', () => {
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
setColumns={jest.fn()}
/>
);

const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first();

expect(control.prop('value')).toEqual('c');
});

it('should reorder the columns when a column is selected in the dropdown', () => {
const setColumns = jest.fn();
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
setColumns={setColumns}
/>
);

const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first();
(control.prop('onChange') as (e: unknown) => {})({
target: { value: 'b' },
});

expect(setColumns).toHaveBeenCalledWith(['c', 'b', 'a']);
});

it('should move to root if the first dropdown item is selected', () => {
const setColumns = jest.fn();
const component = mount(
<BucketNestingEditor
columnId="a"
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
setColumns={setColumns}
/>
);

const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first();
(control.prop('onChange') as (e: unknown) => {})({
target: { value: '' },
});

expect(setColumns).toHaveBeenCalledWith(['a', 'c', 'b']);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some test ideas that I don't see, but you can add if you would like:

  • Test that toggling the switch twice ends in the same state as before
  • Test that toggling the switch enables the switch in the other dimension
  • Test the case where it's all buckets- should be able to replace the last column


it('should allow the last bucket to be moved', () => {
const setColumns = jest.fn();
const component = mount(
<BucketNestingEditor
columnId="b"
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
setColumns={setColumns}
/>
);

const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first();
(control.prop('onChange') as (e: unknown) => {})({
target: { value: '' },
});

expect(setColumns).toHaveBeenCalledWith(['b', 'c', 'a']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import _ from 'lodash';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiHorizontalRule, EuiSwitch, EuiSelect, EuiFormLabel } from '@elastic/eui';
import { IndexPatternLayer } from '../indexpattern';

function nestColumn(columnOrder: string[], outer: string, inner: string) {
const result = columnOrder.filter(c => c !== inner);
const outerPosition = result.indexOf(outer);

result.splice(outerPosition + 1, 0, inner);

return result;
}

export function BucketNestingEditor({
columnId,
layer,
setColumns,
}: {
columnId: string;
layer: IndexPatternLayer;
setColumns: (columns: string[]) => void;
}) {
const column = layer.columns[columnId];
const columns = Object.entries(layer.columns);
const aggColumns = columns
.filter(([id, c]) => id !== columnId && c.isBucketed)
.map(([value, c]) => ({ value, text: c.label }));

if (!column || !column.isBucketed || !aggColumns.length) {
return null;
}

const prevColumn = layer.columnOrder[layer.columnOrder.indexOf(columnId) - 1];

if (aggColumns.length === 1) {
const [target] = aggColumns;

return (
<EuiFormRow>
<>
<EuiHorizontalRule margin="m" />
<EuiSwitch
data-test-subj="indexPattern-nesting-switch"
label={i18n.translate('xpack.lens.xyChart.nestUnderTarget', {
defaultMessage: 'Nest under {target}',
values: { target: target.text },
})}
checked={!!prevColumn}
onChange={() => {
if (prevColumn) {
setColumns(nestColumn(layer.columnOrder, columnId, target.value));
} else {
setColumns(nestColumn(layer.columnOrder, target.value, columnId));
}
}}
/>
</>
</EuiFormRow>
);
}

return (
<EuiFormRow>
<>
<EuiHorizontalRule margin="m" />
<EuiFormLabel>
{i18n.translate('xpack.lens.xyChart.nestUnder', {
defaultMessage: 'Nest under',
})}
</EuiFormLabel>
<EuiSelect
data-test-subj="indexPattern-nesting-select"
options={[
{
value: '',
text: i18n.translate('xpack.lens.xyChart.nestUnderRoot', {
defaultMessage: 'Top level',
}),
},
...aggColumns,
]}
value={prevColumn}
onChange={e => setColumns(nestColumn(layer.columnOrder, e.target.value, columnId))}
/>
</>
</EuiFormRow>
);
}
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ import { operationDefinitionMap, getOperationDisplay, buildColumn } from '../ope
import { deleteColumn, changeColumn } from '../state_helpers';
import { FieldSelect } from './field_select';
import { hasField } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';

const operationPanels = getOperationDisplay();

@@ -363,6 +364,22 @@ export function PopoverEditor(props: PopoverEditorProps) {
/>
</EuiFormRow>
)}
<BucketNestingEditor
layer={state.layers[props.layerId]}
columnId={props.columnId}
setColumns={columnOrder => {
setState({
...state,
layers: {
...state.layers,
[props.layerId]: {
...state.layers[props.layerId],
columnOrder,
},
},
});
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>