[Lens] Rank top values by custom metric (#134811)

* [Lens] order by custom agg

* type check to only allow allowed columns to be included

* remove unused code

* adjusting to the design, correcting full Width everywhere

* change that will make a lot of tests to break

* fix updating only ref column, not full column

* fix cyclic dependency

* remove outdated comment

* added custom labels

* inline modules, ugly code

* fix tests

* clone the aggConfigParams to avoid the Cannot assign to read only property schema of object

* feedback

* Revert "clone the aggConfigParams to avoid the Cannot assign to read only property schema of object"

This reverts commit c4931aad06.

* cr feedback

Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Marta Bondyra 2022-07-06 14:37:30 +02:00 committed by GitHub
parent e3c5e1c327
commit f0965a39b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1880 additions and 924 deletions

View file

@ -110,6 +110,7 @@ export const CustomizablePalette = ({
</EuiFormRow>
{showRangeTypeSelector && (
<EuiFormRow
fullWidth
label={
<>
{i18n.translate('coloring.dynamicColoring.rangeType.label', {
@ -131,6 +132,7 @@ export const CustomizablePalette = ({
display="rowCompressed"
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('coloring.dynamicColoring.rangeType.label', {
defaultMessage: 'Value type',
})}
@ -169,7 +171,6 @@ export const CustomizablePalette = ({
payload: { rangeType: newRangeType, dataBounds, palettes },
});
}}
isFullWidth
/>
</EuiFormRow>
)}

View file

@ -98,6 +98,7 @@ export function PalettePicker({
}
return (
<EuiColorPalettePicker
fullWidth
data-test-subj="lns-palettePicker"
compressed
palettes={palettesToShow}

View file

@ -152,7 +152,7 @@ export class AggConfig {
const isDeserialized = isType || isObject;
if (!isDeserialized) {
val = aggParam.deserialize(val, this);
val = aggParam.deserialize(_.cloneDeep(val), this);
}
to[aggParam.name] = val;

View file

@ -190,6 +190,7 @@ export function TableDimensionEditor(
</EuiFormRow>
{!column.isTransposed && (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.lens.table.columnVisibilityLabel', {
defaultMessage: 'Hide column',
})}
@ -266,6 +267,7 @@ export function TableDimensionEditor(
})}
>
<EuiFieldText
fullWidth
compressed
data-test-subj="lnsDatatable_summaryrow_label"
value={summaryLabel ?? fallbackSummaryLabel}

View file

@ -64,7 +64,7 @@ export function BucketNestingEditor({
defaultMessage: 'Group by this field first',
});
return (
<EuiFormRow label={useAsTopLevelAggCopy} display="columnCompressedSwitch">
<EuiFormRow label={useAsTopLevelAggCopy} display="columnCompressedSwitch" fullWidth>
<EuiSwitch
compressed
label={useAsTopLevelAggCopy}

View file

@ -19,7 +19,7 @@ import {
import ReactDOM from 'react-dom';
import type { IndexPatternDimensionEditorProps } from './dimension_panel';
import type { OperationSupportMatrix } from './operation_support';
import type { GenericIndexPatternColumn } from '../indexpattern';
import { deleteColumn, GenericIndexPatternColumn } from '../indexpattern';
import {
operationDefinitionMap,
getOperationDisplay,
@ -31,12 +31,13 @@ import {
FieldBasedIndexPatternColumn,
canTransition,
DEFAULT_TIME_SCALE,
adjustColumnReferencesForChangedColumn,
} from '../operations';
import { mergeLayer } from '../state_helpers';
import { hasField } from '../pure_utils';
import { fieldIsInvalid } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';
import type { IndexPattern, IndexPatternLayer } from '../types';
import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { FormatSelector } from './format_selector';
import { ReferenceEditor } from './reference_editor';
@ -60,8 +61,8 @@ import { FieldInput } from './field_input';
import { NameInput } from '../../shared_components';
import { ParamEditorProps } from '../operations/definitions';
import { WrappingHelpPopover } from '../help_popover';
const operationPanels = getOperationDisplay();
import { FieldChoice } from './field_select';
import { isColumn } from '../operations/definitions/helpers';
export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
selectedColumn?: GenericIndexPatternColumn;
@ -70,6 +71,8 @@ export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
currentIndexPattern: IndexPattern;
}
const operationDisplay = getOperationDisplay();
export function DimensionEditor(props: DimensionEditorProps) {
const {
selectedColumn,
@ -114,15 +117,47 @@ export function DimensionEditor(props: DimensionEditorProps) {
);
const setStateWrapper = (
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer),
setter:
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
| GenericIndexPatternColumn,
options: { forceRender?: boolean } = {}
) => {
const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
const isDimensionComplete = Boolean(hypotheticalLayer.columns[columnId]);
const layer = state.layers[layerId];
let hypotethicalLayer: IndexPatternLayer;
if (isColumn(setter)) {
hypotethicalLayer = {
...layer,
columns: {
...layer.columns,
[columnId]: setter,
},
};
} else {
hypotethicalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
}
const isDimensionComplete = Boolean(hypotethicalLayer.columns[columnId]);
setState(
(prevState) => {
const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
return mergeLayer({ state: prevState, layerId, newLayer: layer });
let outputLayer: IndexPatternLayer;
const prevLayer = prevState.layers[layerId];
if (isColumn(setter)) {
outputLayer = {
...prevLayer,
columns: {
...prevLayer.columns,
[columnId]: setter,
},
};
} else {
outputLayer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
}
return mergeLayer({
state: prevState,
layerId,
newLayer: adjustColumnReferencesForChangedColumn(outputLayer, columnId),
});
},
{
isDimensionComplete,
@ -189,7 +224,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
// Note: it forced a rerender at this point to avoid UI glitches in async updates (another hack upstream)
// TODO: revisit this once we get rid of updateDatasourceAsync upstream
const moveDefinetelyToStaticValueAndUpdate = (
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
setter:
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
| GenericIndexPatternColumn
) => {
if (temporaryStaticValue) {
setTemporaryState('none');
@ -206,6 +244,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
}
);
}
if (isColumn(setter)) {
throw new Error('static value should only be updated by the whole layer');
}
};
const ParamEditor = getParamEditor(
@ -290,23 +331,23 @@ export function DimensionEditor(props: DimensionEditorProps) {
color = 'subdued';
}
let label: EuiListGroupItemProps['label'] = operationPanels[operationType].displayName;
let label: EuiListGroupItemProps['label'] = operationDisplay[operationType].displayName;
if (isActive && disabledStatus) {
label = (
<EuiToolTip content={disabledStatus} display="block" position="left">
<EuiText color="danger" size="s">
<strong>{operationPanels[operationType].displayName}</strong>
<strong>{operationDisplay[operationType].displayName}</strong>
</EuiText>
</EuiToolTip>
);
} else if (disabledStatus) {
label = (
<EuiToolTip content={disabledStatus} display="block" position="left">
<span>{operationPanels[operationType].displayName}</span>
<span>{operationDisplay[operationType].displayName}</span>
</EuiToolTip>
);
} else if (isActive) {
label = <strong>{operationPanels[operationType].displayName}</strong>;
label = <strong>{operationDisplay[operationType].displayName}</strong>;
}
return {
@ -438,6 +479,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
if (temporaryQuickFunction) {
setTemporaryState('none');
}
const newLayer = replaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
@ -475,11 +517,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
const FieldInputComponent = selectedOperationDefinition?.renderFieldInput || FieldInput;
const paramEditorProps: ParamEditorProps<GenericIndexPatternColumn> = {
const paramEditorProps: ParamEditorProps<
GenericIndexPatternColumn,
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
| GenericIndexPatternColumn
> = {
layer: state.layers[layerId],
layerId,
activeData: props.activeData,
updateLayer: (setter) => {
paramEditorUpdater: (setter) => {
if (temporaryQuickFunction) {
setTemporaryState('none');
}
@ -494,6 +541,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
isFullscreen,
setIsCloseable,
paramEditorCustomProps,
ReferenceEditor,
existingFields: state.existingFields,
...services,
};
@ -523,21 +572,75 @@ export function DimensionEditor(props: DimensionEditorProps) {
<>
{selectedColumn.references.map((referenceId, index) => {
const validation = selectedOperationDefinition.requiredReferences[index];
const layer = state.layers[layerId];
return (
<ReferenceEditor
operationDefinitionMap={operationDefinitionMap}
key={index}
layer={state.layers[layerId]}
layer={layer}
layerId={layerId}
activeData={props.activeData}
columnId={referenceId}
updateLayer={(
column={layer.columns[referenceId]}
incompleteColumn={
layer.incompleteColumns ? layer.incompleteColumns[referenceId] : undefined
}
onDeleteColumn={() => {
updateLayer(
deleteColumn({
layer,
columnId: referenceId,
indexPattern: currentIndexPattern,
})
);
}}
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
updateLayer(
insertOrReplaceColumn({
layer,
columnId: referenceId,
op: operationType,
indexPattern: currentIndexPattern,
field,
visualizationGroups: dimensionGroups,
})
);
}}
onChooseField={(choice: FieldChoice) => {
trackUiEvent('indexpattern_dimension_field_changed');
updateLayer(
insertOrReplaceColumn({
layer,
columnId: referenceId,
indexPattern: currentIndexPattern,
op: choice.operationType,
field: currentIndexPattern.getFieldByName(choice.field),
visualizationGroups: dimensionGroups,
})
);
}}
paramEditorUpdater={(
setter:
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
| GenericIndexPatternColumn
) => {
updateLayer(
typeof setter === 'function' ? setter(state.layers[layerId]) : setter
let newLayer: IndexPatternLayer;
if (typeof setter === 'function') {
newLayer = setter(layer);
} else if (isColumn(setter)) {
newLayer = {
...layer,
columns: {
...layer.columns,
[referenceId]: setter,
},
};
} else {
newLayer = setter;
}
return updateLayer(
adjustColumnReferencesForChangedColumn(newLayer, referenceId)
);
}}
validation={validation}
@ -548,9 +651,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
data: props.data,
uiSettings: props.uiSettings,
currentColumn: state.layers[layerId].columns[columnId],
currentColumn: layer.columns[columnId],
})}
dimensionGroups={dimensionGroups}
isFullscreen={isFullscreen}
toggleFullscreen={toggleFullscreen}
setIsCloseable={setIsCloseable}
@ -600,19 +702,23 @@ export function DimensionEditor(props: DimensionEditorProps) {
const customParamEditor = ParamEditor ? (
<>
<ParamEditor
existingFields={state.existingFields}
layer={state.layers[layerId]}
layerId={layerId}
activeData={props.activeData}
updateLayer={temporaryStaticValue ? moveDefinetelyToStaticValueAndUpdate : setStateWrapper}
paramEditorUpdater={
temporaryStaticValue ? moveDefinetelyToStaticValueAndUpdate : setStateWrapper
}
columnId={columnId}
currentColumn={state.layers[layerId].columns[columnId]}
dateRange={dateRange}
indexPattern={currentIndexPattern}
operationDefinitionMap={operationDefinitionMap}
toggleFullscreen={toggleFullscreen}
isFullscreen={isFullscreen}
setIsCloseable={setIsCloseable}
layerId={layerId}
paramEditorCustomProps={paramEditorCustomProps}
dateRange={dateRange}
isFullscreen={isFullscreen}
indexPattern={currentIndexPattern}
toggleFullscreen={toggleFullscreen}
setIsCloseable={setIsCloseable}
ReferenceEditor={ReferenceEditor}
{...services}
/>
</>

View file

@ -49,11 +49,16 @@ import { DimensionEditor } from './dimension_editor';
import { AdvancedOptions } from './advanced_options';
import { layerTypes } from '../../../common';
jest.mock('./reference_editor', () => ({
ReferenceEditor: () => null,
}));
jest.mock('../loader');
jest.mock('../query_input', () => ({
QueryInput: () => null,
}));
jest.mock('../operations');
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');

View file

@ -36,7 +36,7 @@ interface GetDropPropsArgs {
type DropProps = { dropTypes: DropType[]; nextLabel?: string } | undefined;
const operationLabels = getOperationDisplay();
const operationDisplay = getOperationDisplay();
export function getNewOperation(
field: IndexPatternField | undefined | false,
@ -133,7 +133,7 @@ function getDropPropsForField({
const newOperation = getNewOperation(source.field, target.filterOperations, targetColumn);
if (isTheSameIndexPattern && newOperation) {
const nextLabel = operationLabels[newOperation].displayName;
const nextLabel = operationDisplay[newOperation].displayName;
if (!targetColumn) {
return { dropTypes: ['field_add'], nextLabel };
@ -227,7 +227,7 @@ function getDropPropsFromIncompatibleGroup(
return {
dropTypes,
nextLabel: operationLabels[newOperationForSource].displayName,
nextLabel: operationDisplay[newOperationForSource].displayName,
};
}
}

View file

@ -77,7 +77,6 @@ export function FieldSelect({
fields,
(field) => currentIndexPattern.getFieldByName(field)?.type === 'document'
);
const containsData = (field: string) =>
currentIndexPattern.getFieldByName(field)?.type === 'document' ||
fieldExists(existingFields, currentIndexPattern.title, field);
@ -207,10 +206,11 @@ export function FieldSelect({
(selectedOperationType && selectedField
? [
{
label: fieldIsInvalid
? selectedField
: currentIndexPattern.getFieldByName(selectedField)?.displayName ??
selectedField,
label:
(selectedOperationType &&
selectedField &&
currentIndexPattern.getFieldByName(selectedField)?.displayName) ??
selectedField,
value: { type: 'field', field: selectedField },
},
]

View file

@ -21,25 +21,41 @@ import { ReferenceEditor, ReferenceEditorProps } from './reference_editor';
import {
insertOrReplaceColumn,
LastValueIndexPatternColumn,
operationDefinitionMap,
TermsIndexPatternColumn,
} from '../operations';
import { FieldSelect } from './field_select';
import { IndexPatternLayer } from '../types';
jest.mock('../operations');
describe('reference editor', () => {
let wrapper: ReactWrapper | ShallowWrapper;
let updateLayer: jest.Mock<ReferenceEditorProps['updateLayer']>;
let paramEditorUpdater: jest.Mock<ReferenceEditorProps['paramEditorUpdater']>;
const layer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Top values of dest',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'dest',
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'desc' },
} as TermsIndexPatternColumn,
},
};
function getDefaultArgs() {
return {
layer: {
indexPatternId: '1',
columns: {},
columnOrder: [],
},
layer,
column: layer.columns.ref,
onChooseField: jest.fn(),
onChooseFunction: jest.fn(),
onDeleteColumn: jest.fn(),
columnId: 'ref',
updateLayer,
paramEditorUpdater,
selectionStyle: 'full' as const,
currentIndexPattern: createMockedIndexPattern(),
existingFields: {
@ -63,11 +79,12 @@ describe('reference editor', () => {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
operationDefinitionMap,
};
}
beforeEach(() => {
updateLayer = jest.fn().mockImplementation((newLayer) => {
paramEditorUpdater = jest.fn().mockImplementation((newLayer) => {
if (wrapper instanceof ReactWrapper) {
wrapper.setProps({ layer: newLayer });
}
@ -90,6 +107,7 @@ describe('reference editor', () => {
input: ['field'],
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
}}
column={undefined}
/>
);
@ -115,27 +133,67 @@ describe('reference editor', () => {
);
});
it('should indicate functions and fields that are incompatible with the current', () => {
it('should indicate fields that are incompatible with the current', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Top values of dest',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'dest',
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'desc' },
} as TermsIndexPatternColumn,
},
}}
layer={newLayer}
column={newLayer.columns.ref}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => meta.isBucketed,
validateMetadata: (meta: OperationMetadata) => !meta.isBucketed,
}}
/>
);
const fields = wrapper
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-dimension-field"]')
.prop('options');
const findFieldDataTestSubj = (l: string) => {
return fields![0].options!.find(({ label }) => label === l)!['data-test-subj'];
};
expect(findFieldDataTestSubj('timestampLabel')).toContain('Incompatible');
expect(findFieldDataTestSubj('source')).toContain('Incompatible');
expect(findFieldDataTestSubj('memory')).toContain('lns-fieldOption-memory');
});
it('should indicate functions that are incompatible with the current', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Unique count of dest',
dataType: 'string',
isBucketed: false,
operationType: 'unique_count',
sourceField: 'dest',
},
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={newLayer}
column={newLayer.columns.ref}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => !meta.isBucketed,
}}
/>
);
@ -144,36 +202,31 @@ describe('reference editor', () => {
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-reference-function"]')
.prop('options');
expect(functions.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain(
expect(functions.find(({ label }) => label === 'Average')!['data-test-subj']).toContain(
'incompatible'
);
const fields = wrapper
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-dimension-field"]')
.prop('options');
expect(
fields![0].options!.find(({ label }) => label === 'timestampLabel')!['data-test-subj']
).toContain('Incompatible');
});
it('should not update when selecting the same operation', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
}}
layer={newLayer}
column={newLayer.columns.ref}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
@ -193,26 +246,30 @@ describe('reference editor', () => {
});
it('should keep the field when replacing an existing reference with a compatible function', () => {
const onChooseFunction = jest.fn();
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
}}
layer={newLayer}
column={newLayer.columns.ref}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
}}
onChooseFunction={onChooseFunction}
/>
);
@ -225,31 +282,35 @@ describe('reference editor', () => {
comboBox.prop('onChange')!([option]);
});
expect(insertOrReplaceColumn).toHaveBeenCalledWith(
expect(onChooseFunction).toHaveBeenCalledWith(
'max',
expect.objectContaining({
op: 'max',
field: expect.objectContaining({ name: 'bytes' }),
name: 'bytes',
})
);
});
it('should transition to another function with incompatible field', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Unique count of dest',
dataType: 'string',
isBucketed: false,
operationType: 'unique_count',
sourceField: 'dest',
},
},
} as IndexPatternLayer;
const onChooseFunction = jest.fn();
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
}}
onChooseFunction={onChooseFunction}
column={newLayer.columns.ref}
layer={newLayer}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
@ -260,39 +321,36 @@ describe('reference editor', () => {
const comboBox = wrapper
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-reference-function"]');
const option = comboBox.prop('options')!.find(({ label }) => label === 'Date histogram')!;
const option = comboBox.prop('options')!.find(({ label }) => label === 'Average')!;
act(() => {
comboBox.prop('onChange')!([option]);
});
expect(insertOrReplaceColumn).toHaveBeenCalledWith(
expect.objectContaining({
op: 'date_histogram',
field: undefined,
})
);
expect(onChooseFunction).toHaveBeenCalledWith('average', undefined);
});
it("should show the sub-function as invalid if there's no field compatible with it", () => {
// This may happen for saved objects after changing the type of a field
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
currentIndexPattern={createMockedIndexPatternWithoutType('number')}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
}}
column={newLayer.columns.ref}
layer={newLayer}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
@ -321,6 +379,8 @@ describe('reference editor', () => {
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
column={undefined}
currentIndexPattern={createMockedIndexPatternWithoutType('number')}
validation={{
input: ['field', 'fullReference', 'managedReference'],
validateMetadata: (meta: OperationMetadata) => true,
@ -331,8 +391,8 @@ describe('reference editor', () => {
const subFunctionSelect = wrapper
.find('[data-test-subj="indexPattern-reference-function"]')
.first();
expect(subFunctionSelect.prop('isInvalid')).toEqual(true);
expect(subFunctionSelect.prop('selectedOptions')).not.toEqual(
expect.arrayContaining([expect.objectContaining({ value: 'math' })])
);
@ -345,6 +405,7 @@ describe('reference editor', () => {
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
column={undefined}
selectionStyle={'field' as const}
validation={{
input: ['field'],
@ -360,25 +421,28 @@ describe('reference editor', () => {
});
it('should pass the incomplete operation info to FieldSelect', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
incompleteColumns: {
ref: { operationType: 'max' },
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
incompleteColumns: {
ref: { operationType: 'max' },
},
}}
incompleteColumn={newLayer.incompleteColumns?.ref}
column={newLayer.columns.ref}
layer={newLayer}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
@ -395,25 +459,28 @@ describe('reference editor', () => {
});
it('should pass the incomplete field info to FieldSelect', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
incompleteColumns: {
ref: { sourceField: 'timestamp' },
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'bytes',
},
},
incompleteColumns: {
ref: { sourceField: 'timestamp' },
},
}}
layer={newLayer}
incompleteColumn={newLayer.incompleteColumns?.ref}
column={newLayer.columns.ref}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
@ -432,6 +499,7 @@ describe('reference editor', () => {
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
column={undefined}
selectionStyle="field"
validation={{
input: ['field'],
@ -449,22 +517,24 @@ describe('reference editor', () => {
});
it('should show the FieldSelect as invalid if the selected field is missing', () => {
const newLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of missing',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'missing',
},
},
} as IndexPatternLayer;
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Average of missing',
dataType: 'number',
isBucketed: false,
operationType: 'average',
sourceField: 'missing',
},
},
}}
layer={newLayer}
column={newLayer.columns.ref}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
@ -481,25 +551,27 @@ describe('reference editor', () => {
});
it('should show the ParamEditor for functions that offer one', () => {
const lastValueLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Last value of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'last_value',
sourceField: 'bytes',
params: {
sortField: 'timestamp',
},
} as LastValueIndexPatternColumn,
},
};
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Last value of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'last_value',
sourceField: 'bytes',
params: {
sortField: 'timestamp',
},
} as LastValueIndexPatternColumn,
},
}}
column={lastValueLayer.columns.ref}
layer={lastValueLayer}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,
@ -513,28 +585,31 @@ describe('reference editor', () => {
});
it('should hide the ParamEditor for incomplete functions', () => {
const lastValueLayer = {
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Last value of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'last_value',
sourceField: 'bytes',
params: {
sortField: 'timestamp',
},
} as LastValueIndexPatternColumn,
},
incompleteColumns: {
ref: { operationType: 'max' },
},
};
wrapper = mount(
<ReferenceEditor
{...getDefaultArgs()}
layer={{
indexPatternId: '1',
columnOrder: ['ref'],
columns: {
ref: {
label: 'Last value of bytes',
dataType: 'number',
isBucketed: false,
operationType: 'last_value',
sourceField: 'bytes',
params: {
sortField: 'timestamp',
},
} as LastValueIndexPatternColumn,
},
incompleteColumns: {
ref: { operationType: 'max' },
},
}}
incompleteColumn={lastValueLayer.incompleteColumns.ref}
column={lastValueLayer.columns.ref}
layer={lastValueLayer}
validation={{
input: ['field'],
validateMetadata: (meta: OperationMetadata) => true,

View file

@ -8,13 +8,7 @@
import './dimension_editor.scss';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFormRow,
EuiFormRowProps,
EuiSpacer,
EuiComboBox,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import { EuiFormRowProps, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
@ -22,24 +16,58 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DateRange } from '../../../common';
import type { OperationSupportMatrix } from './operation_support';
import type { OperationType } from '../indexpattern';
import type { GenericIndexPatternColumn, OperationType } from '../indexpattern';
import {
operationDefinitionMap,
getOperationDisplay,
insertOrReplaceColumn,
deleteColumn,
isOperationAllowedAsReference,
FieldBasedIndexPatternColumn,
RequiredReference,
IncompleteColumn,
GenericOperationDefinition,
} from '../operations';
import { FieldSelect } from './field_select';
import { FieldChoice, FieldSelect } from './field_select';
import { hasField } from '../pure_utils';
import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types';
import type {
IndexPattern,
IndexPatternField,
IndexPatternLayer,
IndexPatternPrivateState,
} from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import type { ParamEditorCustomProps, VisualizationDimensionGroupConfig } from '../../types';
import type { ParamEditorCustomProps } from '../../types';
import type { IndexPatternDimensionEditorProps } from './dimension_panel';
import { FormRow } from '../operations/definitions/shared_components';
const operationPanels = getOperationDisplay();
const operationDisplay = getOperationDisplay();
const getFunctionOptions = (
operationSupportMatrix: OperationSupportMatrix & {
operationTypes: Set<OperationType>;
},
operationDefinitionMap: Record<string, GenericOperationDefinition>,
column?: GenericIndexPatternColumn
): Array<EuiComboBoxOptionOption<OperationType>> => {
return Array.from(operationSupportMatrix.operationTypes).map((operationType) => {
const def = operationDefinitionMap[operationType];
const label = operationDisplay[operationType].displayName;
const isCompatible =
!column ||
(column &&
hasField(column) &&
def.input === 'field' &&
operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) ||
(column && !hasField(column) && def.input !== 'field');
return {
label,
value: operationType,
className: 'lnsIndexPatternDimensionEditor__operation',
'data-test-subj': `lns-indexPatternDimension-${operationType}${
isCompatible ? '' : ' incompatible'
}`,
};
});
};
export interface ReferenceEditorProps {
layer: IndexPatternLayer;
@ -48,18 +76,29 @@ export interface ReferenceEditorProps {
selectionStyle: 'full' | 'field' | 'hidden';
validation: RequiredReference;
columnId: string;
updateLayer: (
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
) => void;
column?: GenericIndexPatternColumn;
incompleteColumn?: IncompleteColumn;
currentIndexPattern: IndexPattern;
functionLabel?: string;
fieldLabel?: string;
operationDefinitionMap: Record<string, GenericOperationDefinition>;
isInline?: boolean;
existingFields: IndexPatternPrivateState['existingFields'];
dateRange: DateRange;
labelAppend?: EuiFormRowProps['labelAppend'];
dimensionGroups: VisualizationDimensionGroupConfig[];
isFullscreen: boolean;
toggleFullscreen: () => void;
setIsCloseable: (isCloseable: boolean) => void;
paramEditorCustomProps?: ParamEditorCustomProps;
paramEditorUpdater: (
setter:
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
| GenericIndexPatternColumn
) => void;
onChooseField: (choice: FieldChoice) => void;
onDeleteColumn: () => void;
onChooseFunction: (operationType: string, field?: IndexPatternField) => void;
// Services
uiSettings: IUiSettingsClient;
@ -69,39 +108,28 @@ export interface ReferenceEditorProps {
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
paramEditorCustomProps?: ParamEditorCustomProps;
}
export function ReferenceEditor(props: ReferenceEditorProps) {
export const ReferenceEditor = (props: ReferenceEditorProps) => {
const {
layer,
layerId,
activeData,
columnId,
updateLayer,
currentIndexPattern,
existingFields,
validation,
selectionStyle,
dateRange,
labelAppend,
dimensionGroups,
isFullscreen,
toggleFullscreen,
setIsCloseable,
paramEditorCustomProps,
...services
column,
incompleteColumn,
functionLabel,
onChooseField,
onDeleteColumn,
onChooseFunction,
fieldLabel,
operationDefinitionMap,
isInline,
} = props;
const column = layer.columns[columnId];
const selectedOperationDefinition = column && operationDefinitionMap[column.operationType];
const ParamEditor = selectedOperationDefinition?.paramEditor;
const incompleteInfo = layer.incompleteColumns ? layer.incompleteColumns[columnId] : undefined;
const incompleteOperation = incompleteInfo?.operationType;
const incompleteField = incompleteInfo?.sourceField ?? null;
// Basically the operation support matrix, but different validation
const operationSupportMatrix: OperationSupportMatrix & {
operationTypes: Set<OperationType>;
@ -111,7 +139,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
const operationByField: Partial<Record<string, Set<OperationType>>> = {};
const fieldByOperation: Partial<Record<OperationType, Set<string>>> = {};
Object.values(operationDefinitionMap)
.filter(({ hidden }) => !hidden)
.filter(({ hidden, allowAsReference }) => !hidden && allowAsReference)
.sort((op1, op2) => {
return op1.displayName.localeCompare(op2.displayName);
})
@ -152,81 +180,44 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
operationByField,
fieldByOperation,
};
}, [currentIndexPattern, validation]);
const functionOptions: Array<EuiComboBoxOptionOption<OperationType>> = Array.from(
operationSupportMatrix.operationTypes
).map((operationType) => {
const def = operationDefinitionMap[operationType];
const label = operationPanels[operationType].displayName;
const isCompatible =
!column ||
(column &&
hasField(column) &&
def.input === 'field' &&
operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) ||
(column && !hasField(column) && def.input !== 'field');
return {
label,
value: operationType,
className: 'lnsIndexPatternDimensionEditor__operation',
'data-test-subj': `lns-indexPatternDimension-${operationType}${
isCompatible ? '' : ' incompatible'
}`,
};
});
function onChooseFunction(operationType: OperationType) {
if (column?.operationType === operationType) {
return;
}
const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
if (column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)) {
// Reuse the current field if possible
updateLayer(
insertOrReplaceColumn({
layer,
columnId,
op: operationType,
indexPattern: currentIndexPattern,
field: currentIndexPattern.getFieldByName(column.sourceField),
visualizationGroups: dimensionGroups,
})
);
} else {
// If reusing the field is impossible, we generally can't choose for the user.
// The one exception is if the field is the only possible field, like Count of Records.
const possibleField =
possibleFieldNames?.size === 1
? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
: undefined;
updateLayer(
insertOrReplaceColumn({
layer,
columnId,
op: operationType,
indexPattern: currentIndexPattern,
field: possibleField,
visualizationGroups: dimensionGroups,
})
);
}
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
return;
}
}, [currentIndexPattern, validation, operationDefinitionMap]);
if (selectionStyle === 'hidden') {
return null;
}
const incompleteOperation = incompleteColumn?.operationType;
const incompleteField = incompleteColumn?.sourceField ?? null;
const functionOptions = getFunctionOptions(
operationSupportMatrix,
operationDefinitionMap,
column
);
const selectedOption = incompleteOperation
? [functionOptions.find(({ value }) => value === incompleteOperation)!]
? [functionOptions?.find(({ value }) => value === incompleteOperation)!]
: column
? [functionOptions.find(({ value }) => value === column.operationType)!]
? [functionOptions?.find(({ value }) => value === column.operationType)!]
: [];
// what about a field changing type and becoming invalid?
// Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation.
// At this point the ComboBox will crash.
// Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option
const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null);
if (showSelectionFunctionInvalid) {
const selectedOperationType = incompleteOperation || column?.operationType;
const brokenFunctionOption = {
label: selectedOperationType && operationDisplay[selectedOperationType].displayName,
value: selectedOperationType,
className: 'lnsIndexPatternDimensionEditor__operation',
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
} as EuiComboBoxOptionOption<string>;
functionOptions?.push(brokenFunctionOption);
selectedOption[0] = brokenFunctionOption;
}
// If the operationType is incomplete, the user needs to select a field- so
// the function is marked as valid.
const showOperationInvalid = !column && !Boolean(incompleteOperation);
@ -238,144 +229,114 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
);
// what about a field changing type and becoming invalid?
// Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation.
// At this point the ComboBox will crash.
// Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option
const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null);
if (showSelectionFunctionInvalid) {
const selectedOperationType = incompleteOperation || column.operationType;
const brokenFunctionOption = {
label: operationPanels[selectedOperationType].displayName,
value: selectedOperationType,
className: 'lnsIndexPatternDimensionEditor__operation',
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
};
functionOptions.push(brokenFunctionOption);
selectedOption[0] = brokenFunctionOption;
}
const ParamEditor = selectedOperationDefinition?.paramEditor;
return (
<div id={columnId}>
<div>
{selectionStyle !== 'field' ? (
<>
<EuiFormRow
data-test-subj="indexPattern-subFunction-selection-row"
label={i18n.translate('xpack.lens.indexPattern.chooseSubFunction', {
<div>
{selectionStyle !== 'field' ? (
<>
<FormRow
isInline={isInline}
data-test-subj="indexPattern-subFunction-selection-row"
label={
functionLabel ||
i18n.translate('xpack.lens.indexPattern.chooseSubFunction', {
defaultMessage: 'Choose a sub-function',
})}
fullWidth
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
>
<EuiComboBox
fullWidth
compressed
isClearable={false}
data-test-subj="indexPattern-reference-function"
placeholder={i18n.translate(
'xpack.lens.indexPattern.referenceFunctionPlaceholder',
{
defaultMessage: 'Sub-function',
}
)}
options={functionOptions}
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
selectedOptions={selectedOption}
singleSelection={{ asPlainText: true }}
onChange={(choices) => {
if (choices.length === 0) {
updateLayer(
deleteColumn({
layer,
columnId,
indexPattern: currentIndexPattern,
})
);
return;
}
trackUiEvent('indexpattern_dimension_field_changed');
onChooseFunction(choices[0].value!);
}}
/>
</EuiFormRow>
<EuiSpacer size="s" />
</>
) : null}
{!column || selectedOperationDefinition.input === 'field' ? (
<EuiFormRow
data-test-subj="indexPattern-reference-field-selection-row"
label={i18n.translate('xpack.lens.indexPattern.chooseField', {
defaultMessage: 'Field',
})}
})
}
fullWidth
isInvalid={showFieldInvalid || showFieldMissingInvalid}
labelAppend={labelAppend}
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
>
<FieldSelect
fieldIsInvalid={showFieldInvalid || showFieldMissingInvalid}
currentIndexPattern={currentIndexPattern}
existingFields={existingFields}
operationByField={operationSupportMatrix.operationByField}
selectedOperationType={
// Allows operation to be selected before creating a valid column
column ? column.operationType : incompleteOperation
<EuiComboBox
fullWidth
compressed
isClearable={false}
data-test-subj="indexPattern-reference-function"
placeholder={
functionLabel ||
i18n.translate('xpack.lens.indexPattern.referenceFunctionPlaceholder', {
defaultMessage: 'Sub-function',
})
}
selectedField={
// Allows field to be selected
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
}
incompleteOperation={incompleteOperation}
markAllFieldsCompatible={selectionStyle === 'field'}
onDeleteColumn={() => {
updateLayer(
deleteColumn({
layer,
columnId,
indexPattern: currentIndexPattern,
})
);
}}
onChoose={(choice) => {
updateLayer(
insertOrReplaceColumn({
layer,
columnId,
indexPattern: currentIndexPattern,
op: choice.operationType,
field: currentIndexPattern.getFieldByName(choice.field),
visualizationGroups: dimensionGroups,
})
);
}}
/>
</EuiFormRow>
) : null}
options={functionOptions}
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
selectedOptions={selectedOption}
singleSelection={{ asPlainText: true }}
onChange={(choices: Array<EuiComboBoxOptionOption<string>>) => {
if (choices.length === 0) {
return onDeleteColumn();
}
{column && !incompleteInfo && ParamEditor && (
<>
<ParamEditor
updateLayer={updateLayer}
currentColumn={column}
layer={layer}
layerId={layerId}
activeData={activeData}
columnId={columnId}
indexPattern={currentIndexPattern}
dateRange={dateRange}
operationDefinitionMap={operationDefinitionMap}
isFullscreen={isFullscreen}
toggleFullscreen={toggleFullscreen}
setIsCloseable={setIsCloseable}
paramEditorCustomProps={paramEditorCustomProps}
{...services}
const operationType = choices[0].value!;
if (column?.operationType === operationType) {
return;
}
const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
const field =
column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)
? currentIndexPattern.getFieldByName(column.sourceField)
: possibleFieldNames?.size === 1
? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
: undefined;
onChooseFunction(operationType, field);
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
return;
}}
/>
</>
)}
</div>
</FormRow>
<EuiSpacer size="s" />
</>
) : null}
{!column || selectedOperationDefinition?.input === 'field' ? (
<FormRow
isInline={isInline}
data-test-subj="indexPattern-reference-field-selection-row"
label={
fieldLabel ||
i18n.translate('xpack.lens.indexPattern.chooseField', {
defaultMessage: 'Field',
})
}
fullWidth
isInvalid={showFieldInvalid || showFieldMissingInvalid}
labelAppend={labelAppend}
>
<FieldSelect
fieldIsInvalid={showFieldInvalid || showFieldMissingInvalid}
currentIndexPattern={currentIndexPattern}
existingFields={existingFields}
operationByField={operationSupportMatrix.operationByField}
selectedOperationType={
// Allows operation to be selected before creating a valid column
column ? column.operationType : incompleteOperation
}
selectedField={
// Allows field to be selected
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
}
incompleteOperation={incompleteOperation}
markAllFieldsCompatible={selectionStyle === 'field'}
onDeleteColumn={onDeleteColumn}
onChoose={onChooseField}
/>
</FormRow>
) : null}
{column && !incompleteColumn && ParamEditor && (
<>
<EuiSpacer size="s" />
<ParamEditor
{...props}
isReferenced={true}
operationDefinitionMap={operationDefinitionMap}
currentColumn={column}
indexPattern={props.currentIndexPattern}
/>
</>
)}
</div>
);
}
};

View file

@ -101,6 +101,7 @@ export function TimeScaling({
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiSelect
fullWidth
compressed
options={Object.entries(unitSuffixesLong).map(([unit, text]) => ({
value: unit,

View file

@ -163,7 +163,7 @@ export function TimeShift({
}
isInvalid={Boolean(isLocalValueInvalid || localValueTooSmall || localValueNotMultiple)}
>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiComboBox
fullWidth

View file

@ -42,6 +42,9 @@ import { DatatableColumn } from '@kbn/expressions-plugin';
jest.mock('./loader');
jest.mock('../id_generator');
jest.mock('./operations');
jest.mock('./dimension_panel/reference_editor', () => ({
ReferenceEditor: () => null,
}));
const fieldsOne = [
{

View file

@ -16,6 +16,7 @@ jest.spyOn(actualHelpers, 'copyColumn');
jest.spyOn(actualHelpers, 'insertOrReplaceColumn');
jest.spyOn(actualHelpers, 'insertNewColumn');
jest.spyOn(actualHelpers, 'replaceColumn');
jest.spyOn(actualHelpers, 'adjustColumnReferencesForChangedColumn');
jest.spyOn(actualHelpers, 'getErrorMessages');
jest.spyOn(actualHelpers, 'getColumnOrder');
@ -50,6 +51,7 @@ export const {
isOperationAllowedAsReference,
canTransition,
isColumnValidAsReference,
adjustColumnReferencesForChangedColumn,
getManagedColumnsFrom,
} = actualHelpers;

View file

@ -173,7 +173,7 @@ Example: Smooth a line of measurements:
function MovingAverageParamEditor({
layer,
updateLayer,
paramEditorUpdater,
currentColumn,
columnId,
}: ParamEditorProps<MovingAverageIndexPatternColumn>) {
@ -183,7 +183,7 @@ function MovingAverageParamEditor({
() => {
if (!isValidNumber(inputValue, true, undefined, 1)) return;
const inputNumber = parseInt(inputValue, 10);
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -207,6 +207,7 @@ function MovingAverageParamEditor({
isInvalid={!isValidNumber(inputValue)}
>
<EuiFieldNumber
fullWidth
compressed
value={inputValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}

View file

@ -65,11 +65,17 @@ export interface CardinalityIndexPatternColumn extends FieldBasedIndexPatternCol
};
}
export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternColumn, 'field'> = {
export const cardinalityOperation: OperationDefinition<
CardinalityIndexPatternColumn,
'field',
{},
true
> = {
type: OPERATION_TYPE,
displayName: i18n.translate('xpack.lens.indexPattern.cardinality', {
defaultMessage: 'Unique count',
}),
allowAsReference: true,
input: 'field',
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
if (
@ -123,7 +129,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
layer,
columnId,
currentColumn,
updateLayer,
paramEditorUpdater,
}: ParamEditorProps<CardinalityIndexPatternColumn>) => {
return [
{
@ -141,7 +147,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
}}
checked={Boolean(currentColumn.params?.emptyAsNull)}
onChange={() => {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,

View file

@ -40,7 +40,7 @@ export type CountIndexPatternColumn = FieldBasedIndexPatternColumn & {
};
};
export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field'> = {
export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field', {}, true> = {
type: 'count',
priority: 2,
displayName: i18n.translate('xpack.lens.indexPattern.count', {
@ -52,6 +52,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
getDisallowedPreviousShiftMessage(layer, columnId),
]),
allowAsReference: true,
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
@ -112,7 +113,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
layer,
columnId,
currentColumn,
updateLayer,
paramEditorUpdater,
}: ParamEditorProps<CountIndexPatternColumn>) => {
return [
{
@ -130,7 +131,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
}}
checked={Boolean(currentColumn.params?.emptyAsNull)}
onChange={() => {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,

View file

@ -106,6 +106,14 @@ const defaultOptions = {
isFullscreen: false,
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('date_histogram', () => {
@ -310,7 +318,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -346,7 +354,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={secondLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={secondLayer.columns.col2 as DateHistogramIndexPatternColumn}
indexPattern={indexPattern2}
@ -382,7 +390,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={thirdLayer}
updateLayer={jest.fn()}
paramEditorUpdater={jest.fn()}
columnId="col1"
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
indexPattern={indexPattern1}
@ -418,7 +426,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={thirdLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -459,7 +467,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={thirdLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
indexPattern={{ ...indexPattern1, timeFieldName: undefined }}
@ -502,7 +510,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={thirdLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
indexPattern={{ ...indexPattern1, timeFieldName: undefined }}
@ -544,7 +552,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={thirdLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
indexPattern={{ ...indexPattern1, timeFieldName: 'other_timestamp' }}
@ -559,7 +567,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -581,7 +589,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={testLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -598,7 +606,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={testLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -615,7 +623,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={testLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -631,7 +639,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -655,7 +663,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={testLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -707,7 +715,7 @@ describe('date_histogram', () => {
{...defaultOptions}
layer={layer}
indexPattern={indexPattern}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@ -741,7 +749,7 @@ describe('date_histogram', () => {
<InlineOptions
{...defaultOptions}
layer={thirdLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
/>

View file

@ -171,7 +171,7 @@ export const dateHistogramOperation: OperationDefinition<
layer,
columnId,
currentColumn,
updateLayer,
paramEditorUpdater,
dateRange,
data,
indexPattern,
@ -200,7 +200,7 @@ export const dateHistogramOperation: OperationDefinition<
// updateColumnParam will be called async
// store the checked value before the event pooling clears it
const value = ev.target.checked;
updateLayer((newLayer) =>
paramEditorUpdater((newLayer) =>
updateColumnParam({
layer: newLayer,
columnId,
@ -209,7 +209,7 @@ export const dateHistogramOperation: OperationDefinition<
})
);
},
[columnId, updateLayer]
[columnId, paramEditorUpdater]
);
const setInterval = useCallback(
@ -221,11 +221,11 @@ export const dateHistogramOperation: OperationDefinition<
? autoInterval
: `${isCalendarInterval ? '1' : newInterval.value}${newInterval.unit || 'd'}`;
updateLayer((newLayer) =>
paramEditorUpdater((newLayer) =>
updateColumnParam({ layer: newLayer, columnId, paramName: 'interval', value })
);
},
[columnId, updateLayer]
[columnId, paramEditorUpdater]
);
const options = (intervalOptions || [])
@ -323,7 +323,7 @@ export const dateHistogramOperation: OperationDefinition<
const newValue = opts.length ? opts[0].key! : '';
setIntervalInput(newValue);
if (newValue === autoInterval && currentColumn.params.ignoreTimeRange) {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -397,7 +397,7 @@ export const dateHistogramOperation: OperationDefinition<
});
setIntervalInput(newFixedInterval);
}
updateLayer(newLayer);
paramEditorUpdater(newLayer);
}}
compressed
/>
@ -410,7 +410,7 @@ export const dateHistogramOperation: OperationDefinition<
checked={Boolean(currentColumn.params.includeEmptyRows)}
data-test-subj="indexPattern-include-empty-rows"
onChange={() => {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,

View file

@ -36,6 +36,14 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
// mocking random id generator function
@ -304,7 +312,7 @@ describe('filters', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as FiltersIndexPatternColumn}
/>
@ -357,7 +365,7 @@ describe('filters', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as FiltersIndexPatternColumn}
/>
@ -382,7 +390,7 @@ describe('filters', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as FiltersIndexPatternColumn}
/>

View file

@ -145,11 +145,11 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n
}).toAst();
},
paramEditor: ({ layer, columnId, currentColumn, indexPattern, updateLayer, data }) => {
paramEditor: ({ layer, columnId, currentColumn, indexPattern, paramEditorUpdater }) => {
const filters = currentColumn.params.filters;
const setFilters = (newFilters: Filter[]) =>
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -159,7 +159,7 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n
);
return (
<EuiFormRow>
<EuiFormRow fullWidth>
<FilterList
filters={filters}
setFilters={setFilters}

View file

@ -85,7 +85,7 @@ const MemoizedFormulaEditor = React.memo(FormulaEditor);
export function FormulaEditor({
layer,
updateLayer,
paramEditorUpdater,
currentColumn,
columnId,
indexPattern,
@ -153,7 +153,7 @@ export function FormulaEditor({
setIsCloseable(true);
// If the text is not synced, update the column.
if (text !== currentColumn.params.formula) {
updateLayer(
paramEditorUpdater(
(prevLayer) =>
insertOrReplaceFormulaColumn(
columnId,
@ -183,7 +183,7 @@ export function FormulaEditor({
monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
if (currentColumn.params.formula) {
// Only submit if valid
updateLayer(
paramEditorUpdater(
insertOrReplaceFormulaColumn(
columnId,
{
@ -232,7 +232,7 @@ export function FormulaEditor({
if (previousFormulaWasBroken || previousFormulaWasOkButNoData) {
// If the formula is already broken, show the latest error message in the workspace
if (currentColumn.params.formula !== text) {
updateLayer(
paramEditorUpdater(
insertOrReplaceFormulaColumn(
columnId,
{
@ -314,7 +314,7 @@ export function FormulaEditor({
}
);
updateLayer(newLayer);
paramEditorUpdater(newLayer);
const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns);
const markers: monaco.editor.IMarkerData[] = managedColumns

View file

@ -12,7 +12,7 @@ import {
FormattedIndexPatternColumn,
ReferenceBasedIndexPatternColumn,
} from './column_types';
import { IndexPattern, IndexPatternField } from '../../types';
import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types';
import { hasField } from '../../pure_utils';
export function getInvalidFieldMessage(
@ -128,6 +128,15 @@ export function isColumnOfType<C extends GenericIndexPatternColumn>(
return column.operationType === type;
}
export const isColumn = (
setter:
| GenericIndexPatternColumn
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
): setter is GenericIndexPatternColumn => {
return 'operationType' in setter;
};
export function isColumnFormatted(
column: GenericIndexPatternColumn
): column is FormattedIndexPatternColumn {

View file

@ -64,6 +64,7 @@ import { DateRange, LayerType } from '../../../../common';
import { rangeOperation } from './ranges';
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel';
import type { OriginalColumn } from '../../to_expression';
import { ReferenceEditorProps } from '../../dimension_panel/reference_editor';
export type {
IncompleteColumn,
@ -160,12 +161,14 @@ export { staticValueOperation } from './static_value';
/**
* Properties passed to the operation-specific part of the popover editor
*/
export interface ParamEditorProps<C> {
export interface ParamEditorProps<
C,
U = IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
> {
currentColumn: C;
layer: IndexPatternLayer;
updateLayer: (
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
) => void;
paramEditorUpdater: (setter: U) => void;
ReferenceEditor?: (props: ReferenceEditorProps) => JSX.Element | null;
toggleFullscreen: () => void;
setIsCloseable: (isCloseable: boolean) => void;
isFullscreen: boolean;
@ -183,6 +186,8 @@ export interface ParamEditorProps<C> {
activeData?: IndexPatternDimensionEditorProps['activeData'];
operationDefinitionMap: Record<string, GenericOperationDefinition>;
paramEditorCustomProps?: ParamEditorCustomProps;
existingFields: Record<string, Record<string, boolean>>;
isReferenced?: boolean;
}
export interface FieldInputProps<C> {
@ -227,7 +232,11 @@ export interface AdvancedOption {
helpPopup?: string | null;
}
interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn, P = {}> {
interface BaseOperationDefinitionProps<
C extends BaseIndexPatternColumn,
AR extends boolean,
P = {}
> {
type: C['operationType'];
/**
* The priority of the operation. If multiple operations are possible in
@ -258,7 +267,10 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn, P = {}>
/**
* React component for operation specific settings shown in the flyout editor
*/
paramEditor?: React.ComponentType<ParamEditorProps<C>>;
allowAsReference?: AR;
paramEditor?: React.ComponentType<
AR extends true ? ParamEditorProps<C, GenericIndexPatternColumn> : ParamEditorProps<C>
>;
getAdvancedOptions?: (params: ParamEditorProps<C>) => AdvancedOption[] | undefined;
/**
* Returns true if the `column` can also be used on `newIndexPattern`.
@ -498,7 +510,8 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn, P = {}
indexPattern: IndexPattern,
layer: IndexPatternLayer,
uiSettings: IUiSettingsClient,
orderedColumnIds: string[]
orderedColumnIds: string[],
operationDefinitionMap?: Record<string, GenericOperationDefinition>
) => ExpressionAstFunction;
/**
* Validate that the operation has the right preconditions in the state. For example:
@ -646,8 +659,9 @@ interface OperationDefinitionMap<C extends BaseIndexPatternColumn, P = {}> {
export type OperationDefinition<
C extends BaseIndexPatternColumn,
Input extends keyof OperationDefinitionMap<C>,
P = {}
> = BaseOperationDefinitionProps<C> & OperationDefinitionMap<C, P>[Input];
P = {},
AR extends boolean = false
> = BaseOperationDefinitionProps<C, AR> & OperationDefinitionMap<C, P>[Input];
/**
* A union type of all available operation types. The operation type is a unique id of an operation.

View file

@ -40,6 +40,14 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('last_value', () => {
@ -642,6 +650,13 @@ describe('last_value', () => {
return this.showArrayValuesSwitch.prop('disabled');
}
public get arrayValuesSwitchNotExisiting() {
return (
this._instance.find('[data-test-subj="lns-indexPattern-lastValue-showArrayValues"]')
.length === 0
);
}
changeSortFieldOptions(options: Array<{ label: string; value: string }>) {
this.sortField.find(EuiComboBox).prop('onChange')!([
{ label: 'datefield2', value: 'datefield2' },
@ -659,7 +674,7 @@ describe('last_value', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
/>
@ -676,7 +691,7 @@ describe('last_value', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
/>
@ -685,16 +700,10 @@ describe('last_value', () => {
new Harness(instance).changeSortFieldOptions([{ label: 'datefield2', value: 'datefield2' }]);
expect(updateLayerSpy).toHaveBeenCalledWith({
...layer,
columns: {
...layer.columns,
col2: {
...layer.columns.col2,
params: {
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
sortField: 'datefield2',
},
},
...layer.columns.col2,
params: {
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
sortField: 'datefield2',
},
});
});
@ -707,7 +716,7 @@ describe('last_value', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
/>
@ -718,27 +727,29 @@ describe('last_value', () => {
harness.toggleShowArrayValues();
expect(updateLayerSpy).toHaveBeenCalledWith({
...layer,
columns: {
...layer.columns,
col2: {
...layer.columns.col2,
params: {
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
showArrayValues: true,
},
},
...layer.columns.col2,
params: {
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
showArrayValues: true,
},
});
// have to do this manually, but it happens automatically in the app
const newLayer = updateLayerSpy.mock.calls[0][0];
const newColumn = updateLayerSpy.mock.calls[0][0];
const newLayer = {
...layer,
columns: {
...layer.columns,
col2: newColumn,
},
};
instance.setProps({ layer: newLayer, currentColumn: newLayer.columns.col2 });
expect(harness.showingTopValuesWarning).toBeTruthy();
});
it('should not warn user when top-values not in use', () => {
// todo: move to dimension editor
const updateLayerSpy = jest.fn();
const localLayer = {
...layer,
@ -754,7 +765,7 @@ describe('last_value', () => {
<InlineOptions
{...defaultProps}
layer={localLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
/>
@ -764,7 +775,15 @@ describe('last_value', () => {
harness.toggleShowArrayValues();
// have to do this manually, but it happens automatically in the app
const newLayer = updateLayerSpy.mock.calls[0][0];
const newColumn = updateLayerSpy.mock.calls[0][0];
const newLayer = {
...localLayer,
columns: {
...localLayer.columns,
col2: newColumn,
},
};
instance.setProps({ layer: newLayer, currentColumn: newLayer.columns.col2 });
expect(harness.showingTopValuesWarning).toBeFalsy();
@ -778,7 +797,7 @@ describe('last_value', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
/>
@ -786,6 +805,21 @@ describe('last_value', () => {
expect(new Harness(instance).showArrayValuesSwitchDisabled).toBeTruthy();
});
it('should not display an array for the last value if the column is referenced', () => {
const updateLayerSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
isReferenced={true}
layer={layer}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
/>
);
expect(new Harness(instance).arrayValuesSwitchNotExisiting).toBeTruthy();
});
});
});
@ -829,6 +863,7 @@ describe('last_value', () => {
'Field notExisting was not found',
]);
});
it('shows error message if the sortField does not exist in index pattern', () => {
errorLayer = {
...errorLayer,

View file

@ -20,7 +20,6 @@ import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
import { OperationDefinition } from '.';
import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types';
import { IndexPatternField, IndexPattern } from '../../types';
import { adjustColumnReferencesForChangedColumn, updateColumnParam } from '../layer_helpers';
import { DataType } from '../../../types';
import {
getFormatFromPreviousColumn,
@ -31,6 +30,7 @@ import {
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
import { isScriptedField } from './terms/helpers';
import { FormRow } from './shared_components/form_row';
function ofName(name: string, timeShift: string | undefined) {
return adjustTimeScaleLabelSuffix(
@ -122,7 +122,8 @@ function getExistsFilter(field: string) {
export const lastValueOperation: OperationDefinition<
LastValueIndexPatternColumn,
'field',
Partial<LastValueIndexPatternColumn['params']>
Partial<LastValueIndexPatternColumn['params']>,
true
> = {
type: 'last_value',
displayName: i18n.translate('xpack.lens.indexPattern.lastValue', {
@ -257,8 +258,22 @@ export const lastValueOperation: OperationDefinition<
supportedTypes.has(newField.type)
);
},
allowAsReference: true,
paramEditor: ({
layer,
paramEditorUpdater,
currentColumn,
indexPattern,
isReferenced,
paramEditorCustomProps,
}) => {
const { labels, isInline } = paramEditorCustomProps || {};
const sortByFieldLabel =
labels?.[0] ||
i18n.translate('xpack.lens.indexPattern.lastValue.sortField', {
defaultMessage: 'Sort by date field',
});
paramEditor: ({ layer, updateLayer, columnId, currentColumn, indexPattern }) => {
const dateFields = getDateFields(indexPattern);
const isSortFieldInvalid = !!getInvalidSortFieldMessage(
currentColumn.params.sortField,
@ -270,27 +285,20 @@ export const lastValueOperation: OperationDefinition<
);
const setShowArrayValues = (use: boolean) => {
let updatedLayer = updateColumnParam({
layer,
columnId,
paramName: 'showArrayValues',
value: use,
});
updatedLayer = {
...updatedLayer,
columns: adjustColumnReferencesForChangedColumn(updatedLayer, columnId),
};
updateLayer(updatedLayer);
return paramEditorUpdater({
...currentColumn,
params: {
...currentColumn.params,
showArrayValues: use,
},
} as LastValueIndexPatternColumn);
};
return (
<>
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.lastValue.sortField', {
defaultMessage: 'Sort by date field',
})}
<FormRow
isInline={isInline}
label={sortByFieldLabel}
display="rowCompressed"
fullWidth
error={i18n.translate('xpack.lens.indexPattern.sortField.invalid', {
@ -302,14 +310,13 @@ export const lastValueOperation: OperationDefinition<
placeholder={i18n.translate('xpack.lens.indexPattern.lastValue.sortFieldPlaceholder', {
defaultMessage: 'Sort field',
})}
fullWidth
compressed
isClearable={false}
data-test-subj="lns-indexPattern-lastValue-sortField"
isInvalid={isSortFieldInvalid}
singleSelection={{ asPlainText: true }}
aria-label={i18n.translate('xpack.lens.indexPattern.lastValue.sortField', {
defaultMessage: 'Sort by date field',
})}
aria-label={sortByFieldLabel}
options={dateFields?.map((field: IndexPatternField) => {
return {
value: field.name,
@ -320,14 +327,13 @@ export const lastValueOperation: OperationDefinition<
if (choices.length === 0) {
return;
}
updateLayer(
updateColumnParam({
layer,
columnId,
paramName: 'sortField',
value: choices[0].value,
})
);
return paramEditorUpdater({
...currentColumn,
params: {
...currentColumn.params,
sortField: choices[0].value,
},
} as LastValueIndexPatternColumn);
}}
selectedOptions={
(currentColumn.params?.sortField
@ -342,41 +348,43 @@ export const lastValueOperation: OperationDefinition<
: []) as unknown as EuiComboBoxOptionOption[]
}
/>
</EuiFormRow>
<EuiFormRow
error={i18n.translate(
'xpack.lens.indexPattern.lastValue.showArrayValuesWithTopValuesWarning',
{
defaultMessage:
'When you show array values, you are unable to use this field to rank Top values.',
}
)}
isInvalid={currentColumn.params.showArrayValues && usingTopValues}
display="rowCompressed"
fullWidth
data-test-subj="lns-indexPattern-lastValue-showArrayValues"
>
<EuiToolTip
content={i18n.translate(
'xpack.lens.indexPattern.lastValue.showArrayValuesExplanation',
</FormRow>
{!isReferenced && (
<EuiFormRow
error={i18n.translate(
'xpack.lens.indexPattern.lastValue.showArrayValuesWithTopValuesWarning',
{
defaultMessage:
'Displays all values associated with this field in each last document.',
'When you show array values, you are unable to use this field to rank top values.',
}
)}
position="left"
isInvalid={currentColumn.params.showArrayValues && usingTopValues}
display="rowCompressed"
fullWidth
data-test-subj="lns-indexPattern-lastValue-showArrayValues"
>
<EuiSwitch
label={i18n.translate('xpack.lens.indexPattern.lastValue.showArrayValues', {
defaultMessage: 'Show array values',
})}
compressed={true}
checked={Boolean(currentColumn.params.showArrayValues)}
disabled={isScriptedField(currentColumn.sourceField, indexPattern)}
onChange={() => setShowArrayValues(!currentColumn.params.showArrayValues)}
/>
</EuiToolTip>
</EuiFormRow>
<EuiToolTip
content={i18n.translate(
'xpack.lens.indexPattern.lastValue.showArrayValuesExplanation',
{
defaultMessage:
'Displays all values associated with this field in each last document.',
}
)}
position="left"
>
<EuiSwitch
label={i18n.translate('xpack.lens.indexPattern.lastValue.showArrayValues', {
defaultMessage: 'Show array values',
})}
compressed={true}
checked={Boolean(currentColumn.params.showArrayValues)}
disabled={isScriptedField(currentColumn.sourceField, indexPattern)}
onChange={() => setShowArrayValues(!currentColumn.params.showArrayValues)}
/>
</EuiToolTip>
</EuiFormRow>
)}
</>
);
},

View file

@ -81,6 +81,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
return {
type,
allowAsReference: true,
priority,
displayName,
description,
@ -142,7 +143,12 @@ function buildMetricOperation<T extends MetricColumn<string>>({
sourceField: field.name,
};
},
getAdvancedOptions: ({ layer, columnId, currentColumn, updateLayer }: ParamEditorProps<T>) => {
getAdvancedOptions: ({
layer,
columnId,
currentColumn,
paramEditorUpdater,
}: ParamEditorProps<T>) => {
if (!hideZeroOption) return [];
return [
{
@ -160,7 +166,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
}}
checked={Boolean(currentColumn.params?.emptyAsNull)}
onChange={() => {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -221,7 +227,7 @@ Example: Get the {metric} of price for orders from the UK:
}),
},
shiftable: true,
} as OperationDefinition<T, 'field'>;
} as OperationDefinition<T, 'field', {}, true>;
}
export type SumIndexPatternColumn = MetricColumn<'sum'>;

View file

@ -56,6 +56,14 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('percentile', () => {
@ -715,7 +723,7 @@ describe('percentile', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
/>
@ -732,7 +740,7 @@ describe('percentile', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
/>
@ -752,17 +760,11 @@ describe('percentile', () => {
instance.update();
expect(updateLayerSpy).toHaveBeenCalledWith({
...layer,
columns: {
...layer.columns,
col2: {
...layer.columns.col2,
params: {
percentile: 27,
},
label: '27th percentile of a',
},
...layer.columns.col2,
params: {
percentile: 27,
},
label: '27th percentile of a',
});
});
@ -772,7 +774,7 @@ describe('percentile', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
/>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiFormRow, EuiRange, EuiRangeProps } from '@elastic/eui';
import { EuiFieldNumber, EuiRange } from '@elastic/eui';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { AggFunctionsMapping, METRIC_TYPES } from '@kbn/data-plugin/public';
@ -29,6 +29,7 @@ import { FieldBasedIndexPatternColumn } from './column_types';
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
import { useDebouncedValue } from '../../../shared_components';
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
import { FormRow } from './shared_components';
export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn {
operationType: 'percentile';
@ -64,9 +65,11 @@ const supportedFieldTypes = ['number', 'histogram'];
export const percentileOperation: OperationDefinition<
PercentileIndexPatternColumn,
'field',
{ percentile: number }
{ percentile: number },
true
> = {
type: 'percentile',
allowAsReference: true,
displayName: i18n.translate('xpack.lens.indexPattern.percentile', {
defaultMessage: 'Percentile',
}),
@ -268,12 +271,17 @@ export const percentileOperation: OperationDefinition<
getDisallowedPreviousShiftMessage(layer, columnId),
]),
paramEditor: function PercentileParamEditor({
layer,
updateLayer,
paramEditorUpdater,
currentColumn,
columnId,
indexPattern,
paramEditorCustomProps,
}) {
const { labels, isInline } = paramEditorCustomProps || {};
const percentileLabel =
labels?.[0] ||
i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
defaultMessage: 'Percentile',
});
const onChange = useCallback(
(value) => {
if (
@ -282,29 +290,23 @@ export const percentileOperation: OperationDefinition<
) {
return;
}
updateLayer({
...layer,
columns: {
...layer.columns,
[columnId]: {
...currentColumn,
label: currentColumn.customLabel
? currentColumn.label
: ofName(
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
currentColumn.sourceField,
Number(value),
currentColumn.timeShift
),
params: {
...currentColumn.params,
percentile: Number(value),
},
} as PercentileIndexPatternColumn,
paramEditorUpdater({
...currentColumn,
label: currentColumn.customLabel
? currentColumn.label
: ofName(
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
currentColumn.sourceField,
Number(value),
currentColumn.timeShift
),
params: {
...currentColumn.params,
percentile: Number(value),
},
});
} as PercentileIndexPatternColumn);
},
[updateLayer, layer, columnId, currentColumn, indexPattern]
[paramEditorUpdater, currentColumn, indexPattern]
);
const { inputValue, handleInputChange: handleInputChangeWithoutValidation } = useDebouncedValue<
string | undefined
@ -314,16 +316,15 @@ export const percentileOperation: OperationDefinition<
});
const inputValueIsValid = isValidNumber(inputValue, true, 99, 1);
const handleInputChange: EuiRangeProps['onChange'] = useCallback(
const handleInputChange = useCallback(
(e) => handleInputChangeWithoutValidation(String(e.currentTarget.value)),
[handleInputChangeWithoutValidation]
);
return (
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
defaultMessage: 'Percentile',
})}
<FormRow
isInline={isInline}
label={percentileLabel}
data-test-subj="lns-indexPattern-percentile-form"
display="rowCompressed"
fullWidth
@ -335,20 +336,33 @@ export const percentileOperation: OperationDefinition<
})
}
>
<EuiRange
data-test-subj="lns-indexPattern-percentile-input"
compressed
value={inputValue ?? ''}
min={1}
max={99}
step={1}
onChange={handleInputChange}
showInput
aria-label={i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
defaultMessage: 'Percentile',
})}
/>
</EuiFormRow>
{isInline ? (
<EuiFieldNumber
fullWidth
data-test-subj="lns-indexPattern-percentile-input"
compressed
value={inputValue ?? ''}
min={1}
max={99}
step={1}
onChange={handleInputChange}
aria-label={percentileLabel}
/>
) : (
<EuiRange
fullWidth
data-test-subj="lns-indexPattern-percentile-input"
compressed
value={inputValue ?? ''}
min={1}
max={99}
step={1}
onChange={handleInputChange}
showInput
aria-label={percentileLabel}
/>
)}
</FormRow>
);
},
documentation: {

View file

@ -50,6 +50,14 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('percentile ranks', () => {
@ -274,7 +282,7 @@ describe('percentile ranks', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileRanksIndexPatternColumn}
/>
@ -291,7 +299,7 @@ describe('percentile ranks', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileRanksIndexPatternColumn}
/>
@ -310,17 +318,11 @@ describe('percentile ranks', () => {
instance.update();
expect(updateLayerSpy).toHaveBeenCalledWith({
...layer,
columns: {
...layer.columns,
col2: {
...layer.columns.col2,
params: {
value: 103,
},
label: 'Percentile rank (103) of a',
},
...layer.columns.col2,
params: {
value: 103,
},
label: 'Percentile rank (103) of a',
});
});
@ -330,7 +332,7 @@ describe('percentile ranks', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileRanksIndexPatternColumn}
/>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiFormRow, EuiFieldNumberProps, EuiFieldNumber } from '@elastic/eui';
import { EuiFieldNumberProps, EuiFieldNumber } from '@elastic/eui';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
@ -24,6 +24,7 @@ import { FieldBasedIndexPatternColumn } from './column_types';
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
import { useDebouncedValue } from '../../../shared_components';
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
import { FormRow } from './shared_components';
export interface PercentileRanksIndexPatternColumn extends FieldBasedIndexPatternColumn {
operationType: 'percentile_rank';
@ -52,9 +53,11 @@ const supportedFieldTypes = ['number', 'histogram'];
export const percentileRanksOperation: OperationDefinition<
PercentileRanksIndexPatternColumn,
'field',
{ value: number }
{ value: number },
true
> = {
type: 'percentile_rank',
allowAsReference: true,
displayName: i18n.translate('xpack.lens.indexPattern.percentileRank', {
defaultMessage: 'Percentile rank',
}),
@ -143,40 +146,39 @@ export const percentileRanksOperation: OperationDefinition<
getDisallowedPreviousShiftMessage(layer, columnId),
]),
paramEditor: function PercentileParamEditor({
layer,
updateLayer,
paramEditorUpdater,
currentColumn,
columnId,
indexPattern,
paramEditorCustomProps,
}) {
const { labels, isInline } = paramEditorCustomProps || {};
const percentileRanksLabel =
labels?.[0] ||
i18n.translate('xpack.lens.indexPattern.percentile.percentileRanksValue', {
defaultMessage: 'Percentile ranks value',
});
const onChange = useCallback(
(value) => {
if (!isValidNumber(value) || Number(value) === currentColumn.params.value) {
return;
}
updateLayer({
...layer,
columns: {
...layer.columns,
[columnId]: {
...currentColumn,
label: currentColumn.customLabel
? currentColumn.label
: ofName(
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
currentColumn.sourceField,
Number(value),
currentColumn.timeShift
),
params: {
...currentColumn.params,
value: Number(value),
},
} as PercentileRanksIndexPatternColumn,
paramEditorUpdater({
...currentColumn,
label: currentColumn.customLabel
? currentColumn.label
: ofName(
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
currentColumn.sourceField,
Number(value),
currentColumn.timeShift
),
params: {
...currentColumn.params,
value: Number(value),
},
});
} as PercentileRanksIndexPatternColumn);
},
[updateLayer, layer, columnId, currentColumn, indexPattern]
[paramEditorUpdater, currentColumn, indexPattern]
);
const { inputValue, handleInputChange: handleInputChangeWithoutValidation } = useDebouncedValue<
string | undefined
@ -197,10 +199,9 @@ export const percentileRanksOperation: OperationDefinition<
);
return (
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.percentile.percentileRanksValue', {
defaultMessage: 'Percentile ranks value',
})}
<FormRow
isInline={isInline}
label={percentileRanksLabel}
data-test-subj="lns-indexPattern-percentile_ranks-form"
display="rowCompressed"
fullWidth
@ -213,16 +214,15 @@ export const percentileRanksOperation: OperationDefinition<
}
>
<EuiFieldNumber
fullWidth
data-test-subj="lns-indexPattern-percentile_ranks-input"
compressed
value={inputValue ?? ''}
onChange={handleInputChange}
step="any"
aria-label={i18n.translate('xpack.lens.indexPattern.percentile.percentileRanksValue', {
defaultMessage: 'Percentile ranks value',
})}
aria-label={percentileRanksLabel}
/>
</EuiFormRow>
</FormRow>
);
},
documentation: {

View file

@ -246,6 +246,7 @@ export const AdvancedRangeEditor = ({
return (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.lens.indexPattern.ranges.customRanges', {
defaultMessage: 'Ranges',
})}

View file

@ -83,6 +83,14 @@ const defaultOptions = {
storage: {} as IStorageWrapper,
uiSettings: uiSettingsMock,
savedObjectsClient: {} as SavedObjectsClientContract,
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
dateRange: {
fromDate: 'now-1y',
toDate: 'now',
@ -374,7 +382,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -390,7 +398,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -433,7 +441,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -503,7 +511,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -519,7 +527,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -539,7 +547,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={
{
@ -565,7 +573,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -620,7 +628,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -675,7 +683,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -722,7 +730,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -772,7 +780,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -810,7 +818,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -842,7 +850,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
indexPattern={{
@ -872,7 +880,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
indexPattern={{
@ -896,7 +904,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>
@ -916,7 +924,7 @@ describe('ranges', () => {
<InlineOptions
{...defaultOptions}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
/>

View file

@ -180,7 +180,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
layer,
columnId,
currentColumn,
updateLayer,
paramEditorUpdater,
indexPattern,
uiSettings,
data,
@ -208,7 +208,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
// Used to change one param at the time
const setParam: UpdateParamsFnType = (paramName, value) => {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -226,7 +226,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
newMode === MODES.Range
? { id: 'range', params: { template: 'arrow_right', replaceInfinity: true } }
: undefined;
updateLayer({
paramEditorUpdater({
...layer,
columns: {
...layer.columns,

View file

@ -0,0 +1,3 @@
.lnsIndexPatternDimensionEditor__labelCustomRank {
min-width: 96px;
}

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFormLabel, EuiFormRow, EuiFormRowProps } from '@elastic/eui';
import './form_row.scss';
type FormRowProps = EuiFormRowProps & { isInline?: boolean };
export const FormRow = ({ children, label, isInline, ...props }: FormRowProps) => {
return !isInline ? (
<EuiFormRow {...props} label={label}>
{children}
</EuiFormRow>
) : (
<div data-test-subj={props['data-test-subj']}>
{React.cloneElement(children, {
prepend: (
<EuiFormLabel className="lnsIndexPatternDimensionEditor__labelCustomRank">
{label}
</EuiFormLabel>
),
})}
</div>
);
};

View file

@ -7,3 +7,4 @@
export * from './label_input';
export * from './buckets';
export * from './form_row';

View file

@ -49,6 +49,14 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
existingFields: {
my_index_pattern: {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('static_value', () => {
@ -340,7 +348,7 @@ describe('static_value', () => {
<ParamEditor
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as StaticValueIndexPatternColumn}
/>
@ -371,7 +379,7 @@ describe('static_value', () => {
<ParamEditor
{...defaultProps}
layer={zeroLayer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={zeroLayer.columns.col2 as StaticValueIndexPatternColumn}
/>
@ -387,7 +395,7 @@ describe('static_value', () => {
<ParamEditor
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as StaticValueIndexPatternColumn}
/>
@ -428,7 +436,7 @@ describe('static_value', () => {
<ParamEditor
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as StaticValueIndexPatternColumn}
/>

View file

@ -153,7 +153,7 @@ export const staticValueOperation: OperationDefinition<
},
paramEditor: function StaticValueEditor({
updateLayer,
paramEditorUpdater,
currentColumn,
columnId,
activeData,
@ -168,7 +168,7 @@ export const staticValueOperation: OperationDefinition<
}
// Because of upstream specific UX flows, we need fresh layer state here
// so need to use the updater pattern
updateLayer((newLayer) => {
paramEditorUpdater((newLayer) => {
const newColumn = newLayer.columns[columnId] as StaticValueIndexPatternColumn;
return {
...newLayer,
@ -186,7 +186,7 @@ export const staticValueOperation: OperationDefinition<
};
});
},
[columnId, updateLayer, currentColumn?.params?.value]
[columnId, paramEditorUpdater, currentColumn?.params?.value]
);
// Pick the data from the current activeData (to be used when the current operation is not static_value)
@ -216,9 +216,10 @@ export const staticValueOperation: OperationDefinition<
return (
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--shaded">
<EuiFormLabel>{paramEditorCustomProps?.label || defaultLabel}</EuiFormLabel>
<EuiFormLabel>{paramEditorCustomProps?.labels?.[0] || defaultLabel}</EuiFormLabel>
<EuiSpacer size="s" />
<EuiFieldNumber
fullWidth
data-test-subj="lns-indexPattern-static_value-input"
compressed
value={inputValue ?? ''}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFormRow,
@ -21,12 +21,17 @@ import {
import { uniq } from 'lodash';
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
import { DOCUMENT_FIELD_NAME } from '../../../../../common';
import { insertOrReplaceColumn, updateColumnParam, updateDefaultLabels } from '../../layer_helpers';
import type { DataType } from '../../../../types';
import type { DataType, OperationMetadata } from '../../../../types';
import { OperationDefinition } from '..';
import { FieldBasedIndexPatternColumn } from '../column_types';
import {
FieldBasedIndexPatternColumn,
GenericIndexPatternColumn,
IncompleteColumn,
} from '../column_types';
import { ValuesInput } from './values_input';
import { getInvalidFieldMessage } from '../helpers';
import { getInvalidFieldMessage, isColumn } from '../helpers';
import { FieldInputs, getInputFieldErrorMessage, MAX_MULTI_FIELDS_SIZE } from './field_inputs';
import {
FieldInput as FieldInputBase,
@ -226,7 +231,15 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
},
};
},
toEsAggsFn: (column, columnId, _indexPattern, layer, uiSettings, orderedColumnIds) => {
toEsAggsFn: (
column,
columnId,
_indexPattern,
layer,
uiSettings,
orderedColumnIds,
operationDefinitionMap
) => {
if (column.params?.orderBy.type === 'rare') {
return buildExpressionFunction<AggFunctionsMapping['aggRareTerms']>('aggRareTerms', {
id: columnId,
@ -236,7 +249,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
max_doc_count: column.params.orderBy.maxDocCount,
}).toAst();
}
let orderBy = '_key';
let orderBy: string = '_key';
if (column.params?.orderBy.type === 'column') {
const orderColumn = layer.columns[column.params.orderBy.columnId];
@ -254,6 +267,29 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
? Math.max(1000, column.params.size * 1.5 + 10)
: undefined;
const orderAggColumn = column.params.orderAgg;
let orderAgg;
if (orderAggColumn) {
orderBy = 'custom';
const def = operationDefinitionMap?.[orderAggColumn?.operationType];
if (def && 'toEsAggsFn' in def) {
orderAgg = [
{
type: 'expression' as const,
chain: [
def.toEsAggsFn(
orderAggColumn,
`${columnId}-orderAgg`,
_indexPattern,
layer,
uiSettings,
orderedColumnIds
),
],
},
];
}
}
if (column.params?.secondaryFields?.length) {
return buildExpressionFunction<AggFunctionsMapping['aggMultiTerms']>('aggMultiTerms', {
id: columnId,
@ -262,6 +298,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
fields: [column.sourceField, ...column.params.secondaryFields],
orderBy,
order: column.params.orderDirection,
orderAgg,
size: column.params.size,
shardSize,
otherBucket: Boolean(column.params.otherBucket),
@ -270,6 +307,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}),
}).toAst();
}
return buildExpressionFunction<AggFunctionsMapping['aggTerms']>('aggTerms', {
id: columnId,
enabled: true,
@ -277,6 +315,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
field: column.sourceField,
orderBy,
order: column.params.orderDirection,
orderAgg,
size: column.params.size,
shardSize,
otherBucket: Boolean(column.params.otherBucket),
@ -498,7 +537,22 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
</EuiFormRow>
);
},
paramEditor: function ParamEditor({ layer, updateLayer, currentColumn, columnId, indexPattern }) {
paramEditor: function ParamEditor({
layer,
paramEditorUpdater,
currentColumn,
columnId,
indexPattern,
existingFields,
operationDefinitionMap,
ReferenceEditor,
paramEditorCustomProps,
...rest
}) {
const [incompleteColumn, setIncompleteColumn] = useState<IncompleteColumn | undefined>(
undefined
);
const hasRestrictions = indexPattern.hasRestrictions;
const SEPARATOR = '$$$';
@ -516,6 +570,9 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
if (value === 'rare') {
return { type: 'rare', maxDocCount: DEFAULT_MAX_DOC_COUNT };
}
if (value === 'custom') {
return { type: 'custom' };
}
const parts = value.split(SEPARATOR);
return {
type: 'column',
@ -548,6 +605,12 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}),
});
}
orderOptions.push({
value: toValue({ type: 'custom' }),
text: i18n.translate('xpack.lens.indexPattern.terms.orderCustomMetric', {
defaultMessage: 'Custom',
}),
});
const secondaryFieldsCount = currentColumn.params.secondaryFields
? currentColumn.params.secondaryFields.length
@ -559,7 +622,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
value={currentColumn.params.size}
disabled={currentColumn.params.orderBy.type === 'rare'}
onChange={(value) => {
updateLayer({
paramEditorUpdater({
...layer,
columns: {
...layer.columns,
@ -590,7 +653,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
})}
maxValue={MAXIMUM_MAX_DOC_COUNT}
onChange={(value) => {
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -626,12 +689,13 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
>
<EuiSelect
compressed
fullWidth
data-test-subj="indexPattern-terms-orderBy"
options={orderOptions}
value={toValue(currentColumn.params.orderBy)}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newOrderByValue = fromValue(e.target.value);
const updatedLayer = updateDefaultLabels(
let updatedLayer = updateDefaultLabels(
updateColumnParam({
layer,
columnId,
@ -640,8 +704,33 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}),
indexPattern
);
updateLayer(
if (newOrderByValue.type === 'custom') {
const initialOperation = (
operationDefinitionMap.count as OperationDefinition<
GenericIndexPatternColumn,
'field'
>
).buildColumn({
layer,
indexPattern,
field: indexPattern.getFieldByName(DOCUMENT_FIELD_NAME)!,
});
updatedLayer = updateColumnParam({
layer: updatedLayer,
columnId,
paramName: 'orderAgg',
value: initialOperation,
});
} else {
updatedLayer = updateColumnParam({
layer: updatedLayer,
columnId,
paramName: 'orderAgg',
value: undefined,
});
}
setIncompleteColumn(undefined);
paramEditorUpdater(
updateColumnParam({
layer: updatedLayer,
columnId,
@ -655,6 +744,113 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
})}
/>
</EuiFormRow>
{currentColumn.params.orderAgg && ReferenceEditor && (
<>
<EuiSpacer size="s" />
<ReferenceEditor
operationDefinitionMap={operationDefinitionMap}
functionLabel={i18n.translate('xpack.lens.indexPattern.terms.orderAgg.rankFunction', {
defaultMessage: 'Rank function',
})}
fieldLabel={i18n.translate('xpack.lens.indexPattern.terms.orderAgg.rankField', {
defaultMessage: 'Rank field',
})}
isInline={true}
paramEditorCustomProps={{
...paramEditorCustomProps,
isInline: true,
labels: getLabelForRankFunctions(currentColumn.params.orderAgg.operationType),
}}
layer={layer}
selectionStyle="full"
columnId={`${columnId}-orderAgg`}
currentIndexPattern={indexPattern}
paramEditorUpdater={(setter) => {
if (!isColumn(setter)) {
throw new Error('Setter should always be a column when ran here.');
}
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
paramName: 'orderAgg',
value: setter,
})
);
}}
column={currentColumn.params.orderAgg}
incompleteColumn={incompleteColumn}
existingFields={existingFields}
onDeleteColumn={() => {
throw new Error('Should not be called');
}}
onChooseField={(choice) => {
const field = choice.field && indexPattern.getFieldByName(choice.field);
if (field) {
const hypotethicalColumn = (
operationDefinitionMap[choice.operationType] as OperationDefinition<
GenericIndexPatternColumn,
'field'
>
).buildColumn({
previousColumn: currentColumn.params.orderAgg,
layer,
indexPattern,
field,
});
setIncompleteColumn(undefined);
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
paramName: 'orderAgg',
value: hypotethicalColumn,
})
);
} else {
setIncompleteColumn({
sourceField: choice.field,
operationType: choice.operationType,
});
}
}}
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
if (field) {
const hypotethicalColumn = (
operationDefinitionMap[operationType] as OperationDefinition<
GenericIndexPatternColumn,
'field'
>
).buildColumn({
previousColumn: currentColumn.params.orderAgg,
layer,
indexPattern,
field,
});
setIncompleteColumn(undefined);
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
paramName: 'orderAgg',
value: hypotethicalColumn,
})
);
} else {
setIncompleteColumn({ operationType });
}
}}
validation={{
input: ['field', 'managedReference'],
validateMetadata: (meta: OperationMetadata) =>
meta.dataType === 'number' && !meta.isBucketed,
}}
{...rest}
/>
<EuiSpacer size="m" />
</>
)}
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
defaultMessage: 'Rank direction',
@ -698,7 +894,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
idPrefix,
''
) as TermsIndexPatternColumn['params']['orderDirection'];
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -729,7 +925,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
checked={Boolean(currentColumn.params.otherBucket)}
disabled={currentColumn.params.orderBy.type === 'rare'}
onChange={(e: EuiSwitchEvent) =>
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -753,7 +949,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
data-test-subj="indexPattern-terms-missing-bucket"
checked={Boolean(currentColumn.params.missingBucket)}
onChange={(e: EuiSwitchEvent) =>
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -791,7 +987,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
currentColumn.params.accuracyMode && currentColumn.params.orderBy.type !== 'rare'
)}
onChange={(e: EuiSwitchEvent) =>
updateLayer(
paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@ -808,3 +1004,21 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
);
},
};
function getLabelForRankFunctions(operationType: string) {
switch (operationType) {
case 'last_value':
return [
i18n.translate('xpack.lens.indexPattern.terms.lastValue.sortRankBy', {
defaultMessage: 'Sort rank by',
}),
];
case 'percentile_rank':
return [
i18n.translate('xpack.lens.indexPattern.terms.percentile.', {
defaultMessage: 'Percentile ranks',
}),
];
default:
return;
}
}

View file

@ -22,12 +22,18 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { createMockedIndexPattern } from '../../../mocks';
import { ValuesInput } from './values_input';
import type { TermsIndexPatternColumn } from '.';
import { GenericOperationDefinition, termsOperation, LastValueIndexPatternColumn } from '..';
import {
GenericOperationDefinition,
termsOperation,
LastValueIndexPatternColumn,
operationDefinitionMap,
} from '..';
import { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../../../types';
import { FrameDatasourceAPI } from '../../../../types';
import { DateHistogramIndexPatternColumn } from '../date_histogram';
import { getOperationSupportMatrix } from '../../../dimension_panel/operation_support';
import { FieldSelect } from '../../../dimension_panel/field_select';
import { ReferenceEditor } from '../../../dimension_panel/reference_editor';
// mocking random id generator function
jest.mock('@elastic/eui', () => {
@ -65,11 +71,20 @@ const defaultProps = {
http: {} as HttpSetup,
indexPattern: createMockedIndexPattern(),
// need to provide the terms operation as some helpers use operation specific features
operationDefinitionMap: { terms: termsOperation as unknown as GenericOperationDefinition },
operationDefinitionMap,
isFullscreen: false,
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
ReferenceEditor,
existingFields: {
'my-fake-index-pattern': {
timestamp: true,
bytes: true,
memory: true,
source: true,
},
},
};
describe('terms', () => {
@ -136,7 +151,8 @@ describe('terms', () => {
{} as IndexPattern,
layer,
uiSettingsMock,
[]
[],
operationDefinitionMap
);
expect(esAggsFn).toEqual(
expect.objectContaining({
@ -229,6 +245,59 @@ describe('terms', () => {
);
});
it('should pass orderAgg correctly', () => {
const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
const esAggsFn = termsOperation.toEsAggsFn(
{
...termsColumn,
params: {
...termsColumn.params,
orderAgg: {
label: 'Maximum of price',
dataType: 'number',
operationType: 'max',
sourceField: 'price',
isBucketed: false,
scale: 'ratio',
},
orderBy: {
type: 'custom',
},
},
},
'col1',
{} as IndexPattern,
layer,
uiSettingsMock,
[],
operationDefinitionMap
);
expect(esAggsFn).toEqual(
expect.objectContaining({
arguments: expect.objectContaining({
orderAgg: [
{
chain: [
{
arguments: {
enabled: [true],
field: ['price'],
id: ['col1-orderAgg'],
schema: ['metric'],
},
function: 'aggMax',
type: 'function',
},
],
type: 'expression',
},
],
orderBy: ['custom'],
}),
})
);
});
it('should default percentile rank with non integer value to alphabetical sort', () => {
const newLayer = {
...layer,
@ -1801,7 +1870,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1824,7 +1893,7 @@ describe('terms', () => {
...createMockedIndexPattern(),
hasRestrictions: true,
}}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1839,7 +1908,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -1858,7 +1927,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={
{
@ -1885,7 +1954,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={{
...(layer.columns.col1 as TermsIndexPatternColumn),
@ -1916,7 +1985,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={() => {}}
paramEditorUpdater={() => {}}
columnId="col1"
currentColumn={
{
@ -1965,7 +2034,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={{
...(layer.columns.col1 as TermsIndexPatternColumn),
@ -1992,7 +2061,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={
{
@ -2020,7 +2089,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -2056,7 +2125,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -2070,6 +2139,7 @@ describe('terms', () => {
'column$$$col2',
'alphabetical',
'rare',
'custom',
]);
});
@ -2079,7 +2149,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={
{ ...layer.columns.col1, sourceField: 'memory' } as TermsIndexPatternColumn
@ -2094,6 +2164,7 @@ describe('terms', () => {
expect(select.prop('options')!.map(({ value }) => value)).toEqual([
'column$$$col2',
'alphabetical',
'custom',
]);
});
@ -2103,7 +2174,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -2143,7 +2214,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -2160,7 +2231,7 @@ describe('terms', () => {
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -2183,13 +2254,210 @@ describe('terms', () => {
});
});
it('should render reference editor when order is set to custom metric', () => {
const updateLayerSpy = jest.fn();
const currentLayer = {
...layer,
columns: {
...layer.columns,
col1: {
...layer.columns.col1,
params: {
...(layer.columns.col1 as TermsIndexPatternColumn).params,
type: 'custom',
orderDirection: 'desc',
orderAgg: {
label: 'Median of bytes',
dataType: 'number',
operationType: 'median',
isBucketed: false,
scale: 'ratio',
sourceField: 'bytes',
},
},
},
},
};
const instance = shallow(
<InlineOptions
{...defaultProps}
layer={currentLayer}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
/>
);
expect(instance.find(`ReferenceEditor`)).toHaveLength(1);
instance
.find(EuiSelect)
.find('[data-test-subj="indexPattern-terms-orderBy"]')
.simulate('change', {
target: {
value: 'column$$$col2',
},
});
expect(updateLayerSpy).toHaveBeenCalledWith({
...currentLayer,
columns: {
...currentLayer.columns,
col1: {
...currentLayer.columns.col1,
params: {
...(currentLayer.columns.col1 as TermsIndexPatternColumn).params,
orderAgg: undefined,
orderBy: {
columnId: 'col2',
type: 'column',
},
},
},
},
});
});
it('should update column when changing the operation for orderAgg', () => {
const updateLayerSpy = jest.fn();
const currentLayer = {
...layer,
columns: {
...layer.columns,
col1: {
...layer.columns.col1,
params: {
...(layer.columns.col1 as TermsIndexPatternColumn).params,
type: 'custom',
orderDirection: 'desc',
orderAgg: {
label: 'Median of bytes',
dataType: 'number',
operationType: 'median',
isBucketed: false,
scale: 'ratio',
sourceField: 'bytes',
},
},
},
},
};
const instance = mount(
<InlineOptions
{...defaultProps}
layer={currentLayer}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
/>
);
const refEditor = instance.find(`ReferenceEditor`);
expect(refEditor).toHaveLength(1);
const functionComboBox = refEditor
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-reference-function"]');
const option = functionComboBox.prop('options')!.find(({ label }) => label === 'Average')!;
act(() => {
functionComboBox.prop('onChange')!([option]);
});
expect(updateLayerSpy).toHaveBeenCalledWith({
...currentLayer,
columns: {
...currentLayer.columns,
col1: {
...currentLayer.columns.col1,
params: {
...(currentLayer.columns.col1 as TermsIndexPatternColumn).params,
orderAgg: expect.objectContaining({
dataType: 'number',
isBucketed: false,
label: 'Average of bytes',
operationType: 'average',
sourceField: 'bytes',
}),
},
},
},
});
});
it('should update column when changing the field for orderAgg', () => {
const updateLayerSpy = jest.fn();
const currentLayer = {
...layer,
columns: {
...layer.columns,
col1: {
...layer.columns.col1,
params: {
...(layer.columns.col1 as TermsIndexPatternColumn).params,
type: 'custom',
orderDirection: 'desc',
orderAgg: {
label: 'Median of bytes',
dataType: 'number',
operationType: 'median',
isBucketed: false,
scale: 'ratio',
sourceField: 'bytes',
},
},
},
},
};
const instance = mount(
<InlineOptions
{...defaultProps}
layer={currentLayer}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
/>
);
const refEditor = instance.find(`ReferenceEditor`);
expect(refEditor).toHaveLength(1);
const comboBoxes = refEditor.find(EuiComboBox);
const fieldComboBox = comboBoxes.filter('[data-test-subj="indexPattern-dimension-field"]');
const option = fieldComboBox
.prop('options')[0]
.options!.find(({ label }) => label === 'memory')!;
act(() => {
fieldComboBox.prop('onChange')!([option]);
});
expect(updateLayerSpy).toHaveBeenCalledWith({
...currentLayer,
columns: {
...currentLayer.columns,
col1: {
...currentLayer.columns.col1,
params: {
...(currentLayer.columns.col1 as TermsIndexPatternColumn).params,
orderAgg: expect.objectContaining({
dataType: 'number',
isBucketed: false,
label: 'Median of memory',
operationType: 'median',
sourceField: 'memory',
}),
},
},
},
});
});
it('should render current size value', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>
@ -2198,13 +2466,64 @@ describe('terms', () => {
expect(instance.find(EuiFieldNumber).prop('value')).toEqual('3');
});
it('should not update the column when the change creates incomplete column', () => {
const updateLayerSpy = jest.fn();
const currentLayer = {
...layer,
columns: {
...layer.columns,
col1: {
...layer.columns.col1,
params: {
...(layer.columns.col1 as TermsIndexPatternColumn).params,
type: 'custom',
orderDirection: 'desc',
orderAgg: {
label: 'Count of records',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
},
},
},
},
};
const instance = mount(
<InlineOptions
{...defaultProps}
layer={currentLayer}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
/>
);
const refEditor = instance.find(`ReferenceEditor`);
expect(refEditor).toHaveLength(1);
const comboBoxes = refEditor.find(EuiComboBox);
const functionComboBox = comboBoxes.filter(
'[data-test-subj="indexPattern-reference-function"]'
);
const fieldComboBox = comboBoxes.filter('[data-test-subj="indexPattern-dimension-field"]');
const option = functionComboBox.prop('options')!.find(({ label }) => label === 'Average')!;
act(() => {
functionComboBox.prop('onChange')!([option]);
});
expect(fieldComboBox.prop('isInvalid')).toBeTruthy();
expect(updateLayerSpy).not.toHaveBeenCalled();
});
it('should update state with the size value', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
/>

View file

@ -18,7 +18,9 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn {
orderBy:
| { type: 'alphabetical'; fallback?: boolean }
| { type: 'rare'; maxDocCount: number }
| { type: 'column'; columnId: string };
| { type: 'column'; columnId: string }
| { type: 'custom' };
orderAgg?: FieldBasedIndexPatternColumn;
orderDirection: 'asc' | 'desc';
otherBucket?: boolean;
missingBucket?: boolean;

View file

@ -77,6 +77,7 @@ export const ValuesInput = ({
}
>
<EuiFieldNumber
fullWidth
min={minValue}
max={maxValue}
step={1}

View file

@ -41,6 +41,9 @@ import { CoreStart } from '@kbn/core/public';
jest.mock('.');
jest.mock('../../id_generator');
jest.mock('../dimension_panel/reference_editor', () => ({
ReferenceEditor: () => null,
}));
const indexPatternFields = [
{

View file

@ -503,17 +503,14 @@ export function replaceColumn({
tempLayer = {
...tempLayer,
columnOrder: getColumnOrder(tempLayer),
columns: {
...tempLayer.columns,
[columnId]: column,
},
};
return updateDefaultLabels(
{
...tempLayer,
columnOrder: getColumnOrder(tempLayer),
columns: adjustColumnReferencesForChangedColumn(tempLayer, columnId),
},
adjustColumnReferencesForChangedColumn(tempLayer, columnId),
indexPattern
);
} else if (
@ -573,11 +570,14 @@ export function replaceColumn({
}
return updateDefaultLabels(
{
...tempLayer,
columnOrder: getColumnOrder(newLayer),
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
},
adjustColumnReferencesForChangedColumn(
{
...tempLayer,
columnOrder: getColumnOrder(newLayer),
columns: newLayer.columns,
},
columnId
),
indexPattern
);
}
@ -592,13 +592,18 @@ export function replaceColumn({
indexPattern
);
const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } };
const newLayer = {
...tempLayer,
columns: { ...tempLayer.columns, [columnId]: newColumn },
};
return updateDefaultLabels(
{
...tempLayer,
columnOrder: getColumnOrder(newLayer),
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
},
adjustColumnReferencesForChangedColumn(
{
...newLayer,
columnOrder: getColumnOrder(newLayer),
},
columnId
),
indexPattern
);
}
@ -650,11 +655,13 @@ export function replaceColumn({
}
const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } };
return updateDefaultLabels(
{
...tempLayer,
columnOrder: getColumnOrder(newLayer),
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
},
adjustColumnReferencesForChangedColumn(
{
...newLayer,
columnOrder: getColumnOrder(newLayer),
},
columnId
),
indexPattern
);
} else if (
@ -677,11 +684,13 @@ export function replaceColumn({
{ ...layer, columns: { ...layer.columns, [columnId]: newColumn } },
columnId
);
return {
...newLayer,
columnOrder: getColumnOrder(newLayer),
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
};
return adjustColumnReferencesForChangedColumn(
{
...newLayer,
columnOrder: getColumnOrder(newLayer),
},
columnId
);
} else {
throw new Error('nothing changed');
}
@ -836,11 +845,13 @@ function applyReferenceTransition({
},
},
};
layer = {
...layer,
columnOrder: getColumnOrder(newLayer),
columns: adjustColumnReferencesForChangedColumn(newLayer, newId),
};
layer = adjustColumnReferencesForChangedColumn(
{
...newLayer,
columnOrder: getColumnOrder(newLayer),
},
newId
);
return newId;
}
@ -977,11 +988,13 @@ function applyReferenceTransition({
},
};
return updateDefaultLabels(
{
...layer,
columnOrder: getColumnOrder(layer),
columns: adjustColumnReferencesForChangedColumn(layer, columnId),
},
adjustColumnReferencesForChangedColumn(
{
...layer,
columnOrder: getColumnOrder(layer),
},
columnId
),
indexPattern
);
}
@ -1044,11 +1057,13 @@ function addBucket(
columns: { ...layer.columns, [addedColumnId]: column },
columnOrder: updatedColumnOrder,
};
return {
...tempLayer,
columns: adjustColumnReferencesForChangedColumn(tempLayer, addedColumnId),
columnOrder: getColumnOrder(tempLayer),
};
return adjustColumnReferencesForChangedColumn(
{
...tempLayer,
columnOrder: getColumnOrder(tempLayer),
},
addedColumnId
);
}
export function reorderByGroups(
@ -1108,11 +1123,13 @@ function addMetric(
[addedColumnId]: column,
},
};
return {
...tempLayer,
columnOrder: getColumnOrder(tempLayer),
columns: adjustColumnReferencesForChangedColumn(tempLayer, addedColumnId),
};
return adjustColumnReferencesForChangedColumn(
{
...tempLayer,
columnOrder: getColumnOrder(tempLayer),
},
addedColumnId
);
}
export function getMetricOperationTypes(field: IndexPatternField) {
@ -1146,7 +1163,7 @@ export function updateColumnLabel<C extends GenericIndexPatternColumn>({
};
}
export function updateColumnParam<C extends GenericIndexPatternColumn>({
export function updateColumnParam({
layer,
columnId,
paramName,
@ -1157,15 +1174,15 @@ export function updateColumnParam<C extends GenericIndexPatternColumn>({
paramName: string;
value: unknown;
}): IndexPatternLayer {
const oldColumn = layer.columns[columnId];
const currentColumn = layer.columns[columnId];
return {
...layer,
columns: {
...layer.columns,
[columnId]: {
...oldColumn,
...currentColumn,
params: {
...('params' in oldColumn ? oldColumn.params : {}),
...('params' in currentColumn ? currentColumn.params : {}),
[paramName]: value,
},
},
@ -1210,7 +1227,10 @@ export function adjustColumnReferencesForChangedColumn(
: currentColumn;
}
});
return newColumns;
return {
...layer,
columns: newColumns,
};
}
export function deleteColumn({
@ -1238,13 +1258,13 @@ export function deleteColumn({
const hypotheticalColumns = { ...layer.columns };
delete hypotheticalColumns[columnId];
let newLayer = {
...layer,
columns: adjustColumnReferencesForChangedColumn(
{ ...layer, columns: hypotheticalColumns },
columnId
),
};
let newLayer = adjustColumnReferencesForChangedColumn(
{
...layer,
columns: hypotheticalColumns,
},
columnId
);
extraDeletions.forEach((id) => {
newLayer = deleteColumn({ layer: newLayer, columnId: id, indexPattern });

View file

@ -39,7 +39,7 @@ export function getOperations(): OperationType[] {
/**
* Returns a list of the display names of all operations with any guaranteed order.
*/
export function getOperationDisplay() {
export const getOperationDisplay = memoize(() => {
const display = {} as Record<
OperationType,
{
@ -54,7 +54,7 @@ export function getOperationDisplay() {
};
});
return display;
}
});
export function getSortScoreByPriority(
a: GenericOperationDefinition,

View file

@ -130,7 +130,8 @@ function getExpressionForLayer(
indexPattern,
layer,
uiSettings,
orderedColumnIds
orderedColumnIds,
operationDefinitionMap
);
if (wrapInFilter) {
aggAst = buildExpressionFunction<AggFunctionsMapping['aggFilteredMetric']>(

View file

@ -46,6 +46,7 @@ export function CollapseSetting({
fullWidth
>
<EuiSelect
fullWidth
compressed
data-test-subj="indexPattern-terms-orderBy"
options={options}

View file

@ -44,6 +44,7 @@ export function PalettePicker({
>
<>
<EuiColorPalettePicker
fullWidth
data-test-subj="lns-palettePicker"
compressed
palettes={palettesToShow}

View file

@ -430,7 +430,10 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
invalid?: boolean;
invalidMessage?: string;
};
export type ParamEditorCustomProps = Record<string, unknown> & { label?: string };
export type ParamEditorCustomProps = Record<string, unknown> & {
labels?: string[];
isInline?: boolean;
};
// The only way a visualization has to restrict the query building
export type DatasourceDimensionEditorProps<T = unknown> = DatasourceDimensionProps<T> & {
// Not a StateSetter because we have this unique use case of determining valid columns

View file

@ -458,9 +458,11 @@ export const getReferenceConfiguration = ({
enableDimensionEditor: true,
supportStaticValue: true,
paramEditorCustomProps: {
label: i18n.translate('xpack.lens.indexPattern.staticValue.label', {
defaultMessage: 'Reference line value',
}),
labels: [
i18n.translate('xpack.lens.indexPattern.staticValue.label', {
defaultMessage: 'Reference line value',
}),
],
},
supportFieldFormat: false,
dataTestSubj,

View file

@ -72,7 +72,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
loadTestFile(require.resolve('./add_to_dashboard'));
loadTestFile(require.resolve('./runtime_fields'));
loadTestFile(require.resolve('./dashboard'));
loadTestFile(require.resolve('./multi_terms'));
loadTestFile(require.resolve('./terms'));
loadTestFile(require.resolve('./epoch_millis'));
loadTestFile(require.resolve('./show_underlying_data'));
loadTestFile(require.resolve('./show_underlying_data_dashboard'));

View file

@ -1,90 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
const elasticChart = getService('elasticChart');
describe('lens multi terms suite', () => {
it('should allow creation of lens xy chart with multi terms categories', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await elasticChart.setNewChartUiDebugFlag(true);
await PageObjects.lens.goToTimeRange();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
field: 'bytes',
});
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
keepOpen: true,
});
await PageObjects.lens.addTermToAgg('geo.dest');
await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
'Top values of geo.src + 1 other'
);
await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel');
await PageObjects.lens.addTermToAgg('bytes');
await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
'Top values of geo.src + 2 others'
);
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
expect(data!.bars![0].bars[0].x).to.eql('PE US 19,986');
});
it('should allow creation of lens xy chart with multi terms categories split', async () => {
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
keepOpen: true,
});
await PageObjects.lens.addTermToAgg('geo.dest');
await PageObjects.lens.addTermToAgg('bytes');
await PageObjects.lens.closeDimensionEditor();
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
expect(data?.bars?.[0]?.name).to.eql('PE US 19,986');
});
it('should not show existing defined fields for new term', async () => {
await PageObjects.lens.openDimensionEditor('lnsXY_splitDimensionPanel');
await PageObjects.lens.checkTermsAreNotAvailableToAgg(['bytes', 'geo.src', 'geo.dest']);
await PageObjects.lens.closeDimensionEditor();
});
});
}

View file

@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
const elasticChart = getService('elasticChart');
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const find = getService('find');
const retry = getService('retry');
describe('lens terms', () => {
describe('lens multi terms suite', () => {
it('should allow creation of lens xy chart with multi terms categories', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await elasticChart.setNewChartUiDebugFlag(true);
await PageObjects.lens.goToTimeRange();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
field: 'bytes',
});
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
keepOpen: true,
});
await PageObjects.lens.addTermToAgg('geo.dest');
await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
'Top values of geo.src + 1 other'
);
await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel');
await PageObjects.lens.addTermToAgg('bytes');
await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
'Top values of geo.src + 2 others'
);
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
expect(data!.bars![0].bars[0].x).to.eql('PE US 19,986');
});
it('should allow creation of lens xy chart with multi terms categories split', async () => {
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
keepOpen: true,
});
await PageObjects.lens.addTermToAgg('geo.dest');
await PageObjects.lens.addTermToAgg('bytes');
await PageObjects.lens.closeDimensionEditor();
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
expect(data?.bars?.[0]?.name).to.eql('PE US 19,986');
});
it('should not show existing defined fields for new term', async () => {
await PageObjects.lens.openDimensionEditor('lnsXY_splitDimensionPanel');
await PageObjects.lens.checkTermsAreNotAvailableToAgg(['bytes', 'geo.src', 'geo.dest']);
await PageObjects.lens.closeDimensionEditor();
});
});
describe('sorting by custom metric', () => {
it('should allow sort by custom metric', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await elasticChart.setNewChartUiDebugFlag(true);
await PageObjects.lens.goToTimeRange();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
field: 'bytes',
});
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
keepOpen: true,
});
await find.clickByCssSelector(
'select[data-test-subj="indexPattern-terms-orderBy"] > option[value="custom"]'
);
const fnTarget = await testSubjects.find('indexPattern-reference-function');
await comboBox.openOptionsList(fnTarget);
await comboBox.setElement(fnTarget, 'percentile');
const fieldTarget = await testSubjects.find(
'indexPattern-reference-field-selection-row>indexPattern-dimension-field'
);
await comboBox.openOptionsList(fieldTarget);
await comboBox.setElement(fieldTarget, 'bytes');
await retry.try(async () => {
// Can not use testSubjects because data-test-subj is placed range input and number input
const percentileInput = await find.byCssSelector(
`input[data-test-subj="lns-indexPattern-percentile-input"][type='number']`
);
await percentileInput.click();
await percentileInput.clearValue();
await percentileInput.type('60');
const percentileValue = await percentileInput.getAttribute('value');
if (percentileValue !== '60') {
throw new Error('layerPanelTopHitsSize not set to 60');
}
});
await PageObjects.lens.waitForVisualization('xyVisChart');
await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
'Top 5 values of geo.src'
);
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
expect(data!.bars![0].bars[0].x).to.eql('BN');
expect(data!.bars![0].bars[0].y).to.eql(19265);
});
});
});
}