mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] allow top metric for last value (#127151)
This commit is contained in:
parent
7b86f00528
commit
a389226138
11 changed files with 652 additions and 125 deletions
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
|
||||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
|
@ -16,6 +16,7 @@ import { LastValueIndexPatternColumn } from './last_value';
|
|||
import { lastValueOperation } from './index';
|
||||
import type { IndexPattern, IndexPatternLayer } from '../../types';
|
||||
import { TermsIndexPatternColumn } from './terms';
|
||||
import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
|
||||
|
||||
const uiSettingsMock = {} as IUiSettingsClient;
|
||||
|
||||
|
@ -85,8 +86,8 @@ describe('last_value', () => {
|
|||
);
|
||||
expect(esAggsFn).toEqual(
|
||||
expect.objectContaining({
|
||||
function: 'aggTopMetrics',
|
||||
arguments: expect.objectContaining({
|
||||
aggregate: ['concat'],
|
||||
field: ['a'],
|
||||
size: [1],
|
||||
sortField: ['datefield'],
|
||||
|
@ -95,6 +96,30 @@ describe('last_value', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use top-hit agg when param is set', () => {
|
||||
const lastValueColumn = layer.columns.col2 as LastValueIndexPatternColumn;
|
||||
const esAggsFn = lastValueOperation.toEsAggsFn(
|
||||
{ ...lastValueColumn, params: { ...lastValueColumn.params, showArrayValues: true } },
|
||||
'col1',
|
||||
{} as IndexPattern,
|
||||
layer,
|
||||
uiSettingsMock,
|
||||
[]
|
||||
);
|
||||
expect(esAggsFn).toEqual(
|
||||
expect.objectContaining({
|
||||
function: 'aggTopHit',
|
||||
arguments: expect.objectContaining({
|
||||
field: ['a'],
|
||||
size: [1],
|
||||
aggregate: ['concat'], // aggregate should only be present when using aggTopHit
|
||||
sortField: ['datefield'],
|
||||
sortOrder: ['desc'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFieldChange', () => {
|
||||
|
@ -107,6 +132,7 @@ describe('last_value', () => {
|
|||
dataType: 'string',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
|
@ -135,6 +161,7 @@ describe('last_value', () => {
|
|||
filter: { language: 'kuery', query: 'source: *' },
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
|
@ -158,6 +185,7 @@ describe('last_value', () => {
|
|||
filter: { language: 'kuery', query: 'something_else: 123' },
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
|
@ -180,6 +208,7 @@ describe('last_value', () => {
|
|||
dataType: 'string',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
|
@ -198,6 +227,7 @@ describe('last_value', () => {
|
|||
dataType: 'number',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
|
@ -208,6 +238,50 @@ describe('last_value', () => {
|
|||
expect(column).toHaveProperty('sourceField', 'source');
|
||||
expect(column.params.format).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set show array values if field is scripted', () => {
|
||||
const oldColumn: LastValueIndexPatternColumn = {
|
||||
operationType: 'last_value',
|
||||
sourceField: 'bytes',
|
||||
label: 'Last value of bytes',
|
||||
isBucketed: false,
|
||||
dataType: 'number',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
const field = indexPattern.fields.find((i) => i.name === 'scripted')!;
|
||||
|
||||
expect(
|
||||
lastValueOperation.onFieldChange(oldColumn, field).params.showArrayValues
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should preserve show array values setting if field is not scripted', () => {
|
||||
const oldColumn: LastValueIndexPatternColumn = {
|
||||
operationType: 'last_value',
|
||||
sourceField: 'bytes',
|
||||
label: 'Last value of bytes',
|
||||
isBucketed: false,
|
||||
dataType: 'number',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
const field = indexPattern.fields.find((i) => i.name === 'source')!;
|
||||
|
||||
expect(lastValueOperation.onFieldChange(oldColumn, field).params.showArrayValues).toBeFalsy();
|
||||
expect(
|
||||
lastValueOperation.onFieldChange(
|
||||
{ ...oldColumn, params: { ...oldColumn.params, showArrayValues: true } },
|
||||
field
|
||||
).params.showArrayValues
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPossibleOperationForField', () => {
|
||||
|
@ -397,6 +471,54 @@ describe('last_value', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set showArrayValues if field is scripted or comes from existing params', () => {
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
|
||||
const scriptedField = indexPattern.fields.find((field) => field.scripted);
|
||||
const nonScriptedField = indexPattern.fields.find((field) => !field.scripted);
|
||||
|
||||
const localLayer = {
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: '___records___',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
columnOrder: [],
|
||||
indexPatternId: '',
|
||||
} as IndexPatternLayer;
|
||||
|
||||
expect(
|
||||
lastValueOperation.buildColumn({
|
||||
indexPattern,
|
||||
layer: localLayer,
|
||||
field: scriptedField!,
|
||||
}).params.showArrayValues
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
lastValueOperation.buildColumn(
|
||||
{
|
||||
indexPattern,
|
||||
layer: localLayer,
|
||||
field: nonScriptedField!,
|
||||
},
|
||||
{ showArrayValues: true }
|
||||
).params.showArrayValues
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
lastValueOperation.buildColumn({
|
||||
indexPattern,
|
||||
layer: localLayer,
|
||||
field: nonScriptedField!,
|
||||
}).params.showArrayValues
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return disabledStatus if indexPattern does contain date field', () => {
|
||||
|
@ -482,6 +604,51 @@ describe('last_value', () => {
|
|||
});
|
||||
|
||||
describe('param editor', () => {
|
||||
class Harness {
|
||||
private _instance: ShallowWrapper;
|
||||
|
||||
constructor(instance: ShallowWrapper) {
|
||||
this._instance = instance;
|
||||
}
|
||||
|
||||
private get sortField() {
|
||||
return this._instance.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]');
|
||||
}
|
||||
|
||||
private get showArrayValuesSwitch() {
|
||||
return this._instance
|
||||
.find('[data-test-subj="lns-indexPattern-lastValue-showArrayValues"]')
|
||||
.find(EuiSwitch);
|
||||
}
|
||||
|
||||
public get showingTopValuesWarning() {
|
||||
return Boolean(
|
||||
this._instance
|
||||
.find('[data-test-subj="lns-indexPattern-lastValue-showArrayValues"]')
|
||||
.find(EuiFormRow)
|
||||
.prop('isInvalid')
|
||||
);
|
||||
}
|
||||
|
||||
toggleShowArrayValues() {
|
||||
this.showArrayValuesSwitch.prop('onChange')({} as EuiSwitchEvent);
|
||||
}
|
||||
|
||||
public get showArrayValuesSwitchDisabled() {
|
||||
return this.showArrayValuesSwitch.prop('disabled');
|
||||
}
|
||||
|
||||
changeSortFieldOptions(options: Array<{ label: string; value: string }>) {
|
||||
this.sortField.find(EuiComboBox).prop('onChange')!([
|
||||
{ label: 'datefield2', value: 'datefield2' },
|
||||
]);
|
||||
}
|
||||
|
||||
public get currentSortFieldOptions() {
|
||||
return this.sortField.prop('selectedOptions');
|
||||
}
|
||||
}
|
||||
|
||||
it('should render current sortField', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = shallow(
|
||||
|
@ -494,9 +661,9 @@ describe('last_value', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const select = instance.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]');
|
||||
const harness = new Harness(instance);
|
||||
|
||||
expect(select.prop('selectedOptions')).toEqual([{ label: 'datefield', value: 'datefield' }]);
|
||||
expect(harness.currentSortFieldOptions).toEqual([{ label: 'datefield', value: 'datefield' }]);
|
||||
});
|
||||
|
||||
it('should update state when changing sortField', () => {
|
||||
|
@ -511,10 +678,7 @@ describe('last_value', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
instance
|
||||
.find('[data-test-subj="lns-indexPattern-lastValue-sortField"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('onChange')!([{ label: 'datefield2', value: 'datefield2' }]);
|
||||
new Harness(instance).changeSortFieldOptions([{ label: 'datefield2', value: 'datefield2' }]);
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...layer,
|
||||
|
@ -530,6 +694,95 @@ describe('last_value', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggling using top-hit agg', () => {
|
||||
it('should toggle param when switch clicked', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
|
||||
const harness = new Harness(instance);
|
||||
|
||||
harness.toggleShowArrayValues();
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col2: {
|
||||
...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];
|
||||
instance.setProps({ layer: newLayer, currentColumn: newLayer.columns.col2 });
|
||||
|
||||
expect(harness.showingTopValuesWarning).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not warn user when top-values not in use', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const localLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col1: {
|
||||
...layer.columns.col1,
|
||||
operationType: 'min', // not terms
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={localLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
|
||||
const harness = new Harness(instance);
|
||||
harness.toggleShowArrayValues();
|
||||
|
||||
// have to do this manually, but it happens automatically in the app
|
||||
const newLayer = updateLayerSpy.mock.calls[0][0];
|
||||
instance.setProps({ layer: newLayer, currentColumn: newLayer.columns.col2 });
|
||||
|
||||
expect(harness.showingTopValuesWarning).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set showArrayValues and disable switch when scripted field', () => {
|
||||
(layer.columns.col2 as LastValueIndexPatternColumn).sourceField = 'scripted';
|
||||
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(new Harness(instance).showArrayValuesSwitchDisabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getErrorMessage', () => {
|
||||
|
|
|
@ -8,13 +8,19 @@
|
|||
import React from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiSwitch,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public';
|
||||
import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public';
|
||||
import { OperationDefinition } from './index';
|
||||
import { FieldBasedIndexPatternColumn } from './column_types';
|
||||
import { IndexPatternField, IndexPattern } from '../../types';
|
||||
import { updateColumnParam } from '../layer_helpers';
|
||||
import { adjustColumnReferencesForChangedColumn, updateColumnParam } from '../layer_helpers';
|
||||
import { DataType } from '../../../types';
|
||||
import {
|
||||
getFormatFromPreviousColumn,
|
||||
|
@ -24,6 +30,7 @@ import {
|
|||
} from './helpers';
|
||||
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
|
||||
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
|
||||
import { isScriptedField } from './terms/helpers';
|
||||
|
||||
function ofName(name: string, timeShift: string | undefined) {
|
||||
return adjustTimeScaleLabelSuffix(
|
||||
|
@ -99,6 +106,7 @@ export interface LastValueIndexPatternColumn extends FieldBasedIndexPatternColum
|
|||
operationType: 'last_value';
|
||||
params: {
|
||||
sortField: string;
|
||||
showArrayValues: boolean;
|
||||
// last value on numeric fields can be formatted
|
||||
format?: {
|
||||
id: string;
|
||||
|
@ -116,7 +124,11 @@ function getExistsFilter(field: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn, 'field'> = {
|
||||
export const lastValueOperation: OperationDefinition<
|
||||
LastValueIndexPatternColumn,
|
||||
'field',
|
||||
Partial<LastValueIndexPatternColumn['params']>
|
||||
> = {
|
||||
type: 'last_value',
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.lastValue', {
|
||||
defaultMessage: 'Last value',
|
||||
|
@ -127,6 +139,8 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
|
|||
onFieldChange: (oldColumn, field) => {
|
||||
const newParams = { ...oldColumn.params };
|
||||
|
||||
newParams.showArrayValues = isScriptedField(field) || oldColumn.params.showArrayValues;
|
||||
|
||||
if ('format' in newParams && field.type !== 'number') {
|
||||
delete newParams.format;
|
||||
}
|
||||
|
@ -191,6 +205,8 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
|
|||
);
|
||||
}
|
||||
|
||||
const showArrayValues = isScriptedField(field) || lastValueParams?.showArrayValues;
|
||||
|
||||
return {
|
||||
label: ofName(field.displayName, previousColumn?.timeShift),
|
||||
dataType: field.type as DataType,
|
||||
|
@ -201,6 +217,7 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
|
|||
filter: getFilter(previousColumn, columnParams) || getExistsFilter(field.name),
|
||||
timeShift: columnParams?.shift || previousColumn?.timeShift,
|
||||
params: {
|
||||
showArrayValues,
|
||||
sortField: lastValueParams?.sortField || sortField,
|
||||
...getFormatFromPreviousColumn(previousColumn),
|
||||
},
|
||||
|
@ -208,19 +225,30 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
|
|||
},
|
||||
filterable: true,
|
||||
shiftable: true,
|
||||
toEsAggsFn: (column, columnId) => {
|
||||
return buildExpressionFunction<AggFunctionsMapping['aggTopHit']>('aggTopHit', {
|
||||
toEsAggsFn: (column, columnId, indexPattern) => {
|
||||
const initialArgs = {
|
||||
id: columnId,
|
||||
enabled: true,
|
||||
schema: 'metric',
|
||||
field: column.sourceField,
|
||||
aggregate: 'concat',
|
||||
size: 1,
|
||||
sortOrder: 'desc',
|
||||
sortField: column.params.sortField,
|
||||
// time shift is added to wrapping aggFilteredMetric if filter is set
|
||||
timeShift: column.filter ? undefined : column.timeShift,
|
||||
}).toAst();
|
||||
} as const;
|
||||
|
||||
return (
|
||||
column.params.showArrayValues
|
||||
? buildExpressionFunction<AggFunctionsMapping['aggTopHit']>('aggTopHit', {
|
||||
...initialArgs,
|
||||
aggregate: 'concat',
|
||||
})
|
||||
: buildExpressionFunction<AggFunctionsMapping['aggTopMetrics']>(
|
||||
'aggTopMetrics',
|
||||
initialArgs
|
||||
)
|
||||
).toAst();
|
||||
},
|
||||
|
||||
isTransferable: (column, newIndexPattern) => {
|
||||
|
@ -241,6 +269,27 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
|
|||
currentColumn.params.sortField,
|
||||
indexPattern
|
||||
);
|
||||
|
||||
const usingTopValues = Object.keys(layer.columns).some(
|
||||
(_columnId) => layer.columns[_columnId].operationType === 'terms'
|
||||
);
|
||||
|
||||
const setShowArrayValues = (use: boolean) => {
|
||||
let updatedLayer = updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'showArrayValues',
|
||||
value: use,
|
||||
});
|
||||
|
||||
updatedLayer = {
|
||||
...updatedLayer,
|
||||
columns: adjustColumnReferencesForChangedColumn(updatedLayer, columnId),
|
||||
};
|
||||
|
||||
updateLayer(updatedLayer);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -299,6 +348,40 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
|
|||
}
|
||||
/>
|
||||
</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',
|
||||
{
|
||||
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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -452,23 +452,47 @@ describe('isSortableByColumn()', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not be sortable by a last_value function', () => {
|
||||
expect(
|
||||
isSortableByColumn(
|
||||
getLayer(getStringBasedOperationColumn(), [
|
||||
{
|
||||
label: 'Last Value',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'bytes',
|
||||
operationType: 'last_value',
|
||||
params: {
|
||||
sortField: 'time',
|
||||
},
|
||||
} as GenericIndexPatternColumn,
|
||||
]),
|
||||
'col2'
|
||||
)
|
||||
).toBeFalsy();
|
||||
describe('last_value operation', () => {
|
||||
it('should NOT be sortable when using top-hit agg', () => {
|
||||
expect(
|
||||
isSortableByColumn(
|
||||
getLayer(getStringBasedOperationColumn(), [
|
||||
{
|
||||
label: 'Last Value',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'bytes',
|
||||
operationType: 'last_value',
|
||||
params: {
|
||||
sortField: 'time',
|
||||
showArrayValues: true,
|
||||
},
|
||||
} as GenericIndexPatternColumn,
|
||||
]),
|
||||
'col2'
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('SHOULD be sortable when NOT using top-hit agg', () => {
|
||||
expect(
|
||||
isSortableByColumn(
|
||||
getLayer(getStringBasedOperationColumn(), [
|
||||
{
|
||||
label: 'Last Value',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'bytes',
|
||||
operationType: 'last_value',
|
||||
params: {
|
||||
sortField: 'time',
|
||||
showArrayValues: false,
|
||||
},
|
||||
} as GenericIndexPatternColumn,
|
||||
]),
|
||||
'col2'
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ import type { FieldStatsResponse } from '../../../../../common';
|
|||
import type { FrameDatasourceAPI } from '../../../../types';
|
||||
import type { FiltersIndexPatternColumn } from '../index';
|
||||
import type { TermsIndexPatternColumn } from './types';
|
||||
import { LastValueIndexPatternColumn } from '../last_value';
|
||||
|
||||
import type { IndexPatternLayer, IndexPattern, IndexPatternField } from '../../../types';
|
||||
import { MULTI_KEY_VISUAL_SEPARATOR, supportedTypes } from './constants';
|
||||
import { isColumnOfType } from '../helpers';
|
||||
|
@ -203,12 +205,19 @@ export function getDisallowedTermsMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function checkLastValue(column: GenericIndexPatternColumn) {
|
||||
return (
|
||||
column.operationType !== 'last_value' ||
|
||||
!(column as LastValueIndexPatternColumn).params.showArrayValues
|
||||
);
|
||||
}
|
||||
|
||||
export function isSortableByColumn(layer: IndexPatternLayer, columnId: string) {
|
||||
const column = layer.columns[columnId];
|
||||
return (
|
||||
column &&
|
||||
!column.isBucketed &&
|
||||
column.operationType !== 'last_value' &&
|
||||
checkLastValue(column) &&
|
||||
!('references' in column) &&
|
||||
!isReferenced(layer, columnId)
|
||||
);
|
||||
|
|
|
@ -515,7 +515,7 @@ describe('terms', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
it('should set alphabetical order type if metric column is of type last value', () => {
|
||||
it('should set alphabetical order type if metric column is of type last value and showing array values', () => {
|
||||
const termsColumn = termsOperation.buildColumn({
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
layer: {
|
||||
|
@ -528,6 +528,7 @@ describe('terms', () => {
|
|||
operationType: 'last_value',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: true,
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
|
@ -546,6 +547,38 @@ describe('terms', () => {
|
|||
expect.objectContaining({ orderBy: { type: 'alphabetical', fallback: true } })
|
||||
);
|
||||
});
|
||||
it('should NOT set alphabetical order type if metric column is of type last value and NOT showing array values', () => {
|
||||
const termsColumn = termsOperation.buildColumn({
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
layer: {
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Last value of a',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'a',
|
||||
operationType: 'last_value',
|
||||
params: {
|
||||
sortField: 'datefield',
|
||||
showArrayValues: false,
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
columnOrder: [],
|
||||
indexPatternId: '',
|
||||
},
|
||||
field: {
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
type: 'boolean',
|
||||
name: 'test',
|
||||
displayName: 'test',
|
||||
},
|
||||
});
|
||||
expect(termsColumn.params).toEqual(
|
||||
expect.objectContaining({ orderBy: { type: 'column', columnId: 'col1' } })
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the default size when there is an existing bucket', () => {
|
||||
const termsColumn = termsOperation.buildColumn({
|
||||
|
@ -633,7 +666,7 @@ describe('terms', () => {
|
|||
expect(updatedColumn).toBe(initialColumn);
|
||||
});
|
||||
|
||||
it('should switch to alphabetical ordering if metric is of type last_value', () => {
|
||||
it('should switch to alphabetical ordering if metric is of type last_value and using top hit agg', () => {
|
||||
const initialColumn: TermsIndexPatternColumn = {
|
||||
label: 'Top value of category',
|
||||
dataType: 'string',
|
||||
|
@ -660,6 +693,7 @@ describe('terms', () => {
|
|||
operationType: 'last_value',
|
||||
params: {
|
||||
sortField: 'time',
|
||||
showArrayValues: true,
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
|
|
|
@ -1183,7 +1183,10 @@ export function updateColumnParam<C extends GenericIndexPatternColumn>({
|
|||
};
|
||||
}
|
||||
|
||||
function adjustColumnReferencesForChangedColumn(layer: IndexPatternLayer, changedColumnId: string) {
|
||||
export function adjustColumnReferencesForChangedColumn(
|
||||
layer: IndexPatternLayer,
|
||||
changedColumnId: string
|
||||
) {
|
||||
const newColumns = { ...layer.columns };
|
||||
Object.keys(newColumns).forEach((currentColumnId) => {
|
||||
if (currentColumnId !== changedColumnId) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
commonRenameFilterReferences,
|
||||
commonRenameOperationsForFormula,
|
||||
commonRenameRecordsField,
|
||||
commonSetLastValueShowArrayValues,
|
||||
commonUpdateVisLayerType,
|
||||
getLensCustomVisualizationMigrations,
|
||||
getLensFilterMigrations,
|
||||
|
@ -93,7 +94,8 @@ export const makeLensEmbeddableFactory =
|
|||
},
|
||||
'8.2.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape810<VisState810> };
|
||||
const migratedLensState = commonEnhanceTableRowHeight(lensState.attributes);
|
||||
let migratedLensState = commonSetLastValueShowArrayValues(lensState.attributes);
|
||||
migratedLensState = commonEnhanceTableRowHeight(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
|
|
|
@ -194,6 +194,23 @@ export const commonRenameFilterReferences = (attributes: LensDocShape715): LensD
|
|||
return newAttributes as LensDocShape810;
|
||||
};
|
||||
|
||||
export const commonSetLastValueShowArrayValues = (
|
||||
attributes: LensDocShape810
|
||||
): LensDocShape810<VisState820> => {
|
||||
const newAttributes = cloneDeep(attributes);
|
||||
for (const layer of Object.values(newAttributes.state.datasourceStates.indexpattern.layers)) {
|
||||
for (const column of Object.values(layer.columns)) {
|
||||
if (
|
||||
column.operationType === 'last_value' &&
|
||||
!(typeof column.params.showArrayValues === 'boolean')
|
||||
) {
|
||||
column.params.showArrayValues = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newAttributes;
|
||||
};
|
||||
|
||||
export const commonEnhanceTableRowHeight = (
|
||||
attributes: LensDocShape810<VisState810>
|
||||
): LensDocShape810<VisState820> => {
|
||||
|
|
|
@ -1782,121 +1782,215 @@ describe('Lens migrations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('8.2.0 rename fitRowToContent to new detailed rowHeight and rowHeightLines', () => {
|
||||
const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext;
|
||||
function getExample(fitToContent: boolean) {
|
||||
return {
|
||||
describe('8.2.0', () => {
|
||||
describe('last_value columns', () => {
|
||||
const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext;
|
||||
const example = {
|
||||
type: 'lens',
|
||||
id: 'mocked-saved-object-id',
|
||||
attributes: {
|
||||
visualizationType: 'lnsDatatable',
|
||||
title: 'Lens visualization',
|
||||
references: [
|
||||
{
|
||||
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
name: 'indexpattern-datasource-layer-cddd8f79-fb20-4191-a3e7-92484780cc62',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
savedObjectId: '1',
|
||||
title: 'MyRenamedOps',
|
||||
description: '',
|
||||
visualizationType: null,
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
currentIndexPatternId: 'logstash-*',
|
||||
layers: {
|
||||
'cddd8f79-fb20-4191-a3e7-92484780cc62': {
|
||||
indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
'2': {
|
||||
columns: {
|
||||
'221f0abf-6e54-4c61-9316-4107ad6fa500': {
|
||||
label: 'Top values of category.keyword',
|
||||
'3': {
|
||||
dataType: 'string',
|
||||
operationType: 'terms',
|
||||
scale: 'ordinal',
|
||||
sourceField: 'category.keyword',
|
||||
isBucketed: true,
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: {
|
||||
type: 'column',
|
||||
columnId: 'c6f07a26-64eb-4871-ad62-c7d937230e33',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
missingBucket: false,
|
||||
parentFormat: {
|
||||
id: 'terms',
|
||||
},
|
||||
},
|
||||
label: 'Top values of geoip.country_iso_code',
|
||||
operationType: 'terms',
|
||||
params: {},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'geoip.country_iso_code',
|
||||
},
|
||||
'c6f07a26-64eb-4871-ad62-c7d937230e33': {
|
||||
label: 'Count of records',
|
||||
'4': {
|
||||
label: 'Anzahl der Aufnahmen',
|
||||
dataType: 'number',
|
||||
operationType: 'count',
|
||||
sourceField: 'Aufnahmen',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
},
|
||||
'5': {
|
||||
label: 'Sum of bytes',
|
||||
dataType: 'numver',
|
||||
operationType: 'last_value',
|
||||
params: {
|
||||
// no showArrayValues
|
||||
},
|
||||
sourceField: 'bytes',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: '___records___',
|
||||
},
|
||||
},
|
||||
columnOrder: [
|
||||
'221f0abf-6e54-4c61-9316-4107ad6fa500',
|
||||
'c6f07a26-64eb-4871-ad62-c7d937230e33',
|
||||
],
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['3', '4', '5'],
|
||||
},
|
||||
'3': {
|
||||
columns: {
|
||||
'5': {
|
||||
label: 'Sum of bytes',
|
||||
dataType: 'numver',
|
||||
operationType: 'last_value',
|
||||
params: {
|
||||
// no showArrayValues
|
||||
},
|
||||
sourceField: 'bytes',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
},
|
||||
},
|
||||
columnOrder: ['3', '4', '5'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
columns: [
|
||||
{
|
||||
isTransposed: false,
|
||||
columnId: '221f0abf-6e54-4c61-9316-4107ad6fa500',
|
||||
},
|
||||
{
|
||||
isTransposed: false,
|
||||
columnId: 'c6f07a26-64eb-4871-ad62-c7d937230e33',
|
||||
},
|
||||
],
|
||||
layerId: 'cddd8f79-fb20-4191-a3e7-92484780cc62',
|
||||
layerType: 'data',
|
||||
fitRowToContent: fitToContent,
|
||||
},
|
||||
visualization: {},
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [],
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
|
||||
}
|
||||
|
||||
it('should migrate enabled fitRowToContent to new rowHeight: "auto"', () => {
|
||||
const result = migrations['8.2.0'](getExample(true), context) as ReturnType<
|
||||
SavedObjectMigrationFn<LensDocShape810<VisState810>, LensDocShape810<VisState820>>
|
||||
>;
|
||||
it('should set showArrayValues for last-value columns', () => {
|
||||
const result = migrations['8.2.0'](example, context) as ReturnType<
|
||||
SavedObjectMigrationFn<LensDocShape, LensDocShape>
|
||||
>;
|
||||
|
||||
expect(result.attributes.state.visualization as VisState820).toEqual(
|
||||
expect.objectContaining({
|
||||
rowHeight: 'auto',
|
||||
})
|
||||
);
|
||||
const layer2Columns =
|
||||
result.attributes.state.datasourceStates.indexpattern.layers['2'].columns;
|
||||
const layer3Columns =
|
||||
result.attributes.state.datasourceStates.indexpattern.layers['3'].columns;
|
||||
expect(layer2Columns['5'].params).toHaveProperty('showArrayValues', true);
|
||||
expect(layer2Columns['3'].params).not.toHaveProperty('showArrayValues');
|
||||
expect(layer3Columns['5'].params).toHaveProperty('showArrayValues', true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should migrate disabled fitRowToContent to new rowHeight: "single"', () => {
|
||||
const result = migrations['8.2.0'](getExample(false), context) as ReturnType<
|
||||
SavedObjectMigrationFn<LensDocShape810<VisState810>, LensDocShape810<VisState820>>
|
||||
>;
|
||||
describe('rename fitRowToContent to new detailed rowHeight and rowHeightLines', () => {
|
||||
const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext;
|
||||
function getExample(fitToContent: boolean) {
|
||||
return {
|
||||
type: 'lens',
|
||||
id: 'mocked-saved-object-id',
|
||||
attributes: {
|
||||
visualizationType: 'lnsDatatable',
|
||||
title: 'Lens visualization',
|
||||
references: [
|
||||
{
|
||||
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
name: 'indexpattern-datasource-layer-cddd8f79-fb20-4191-a3e7-92484780cc62',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
'cddd8f79-fb20-4191-a3e7-92484780cc62': {
|
||||
indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
columns: {
|
||||
'221f0abf-6e54-4c61-9316-4107ad6fa500': {
|
||||
label: 'Top values of category.keyword',
|
||||
dataType: 'string',
|
||||
operationType: 'terms',
|
||||
scale: 'ordinal',
|
||||
sourceField: 'category.keyword',
|
||||
isBucketed: true,
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: {
|
||||
type: 'column',
|
||||
columnId: 'c6f07a26-64eb-4871-ad62-c7d937230e33',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
missingBucket: false,
|
||||
parentFormat: {
|
||||
id: 'terms',
|
||||
},
|
||||
},
|
||||
},
|
||||
'c6f07a26-64eb-4871-ad62-c7d937230e33': {
|
||||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
operationType: 'count',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: '___records___',
|
||||
},
|
||||
},
|
||||
columnOrder: [
|
||||
'221f0abf-6e54-4c61-9316-4107ad6fa500',
|
||||
'c6f07a26-64eb-4871-ad62-c7d937230e33',
|
||||
],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
columns: [
|
||||
{
|
||||
isTransposed: false,
|
||||
columnId: '221f0abf-6e54-4c61-9316-4107ad6fa500',
|
||||
},
|
||||
{
|
||||
isTransposed: false,
|
||||
columnId: 'c6f07a26-64eb-4871-ad62-c7d937230e33',
|
||||
},
|
||||
],
|
||||
layerId: 'cddd8f79-fb20-4191-a3e7-92484780cc62',
|
||||
layerType: 'data',
|
||||
fitRowToContent: fitToContent,
|
||||
},
|
||||
filters: [],
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape810>;
|
||||
}
|
||||
|
||||
expect(result.attributes.state.visualization as VisState820).toEqual(
|
||||
expect.objectContaining({
|
||||
rowHeight: 'single',
|
||||
rowHeightLines: 1,
|
||||
})
|
||||
);
|
||||
it('should migrate enabled fitRowToContent to new rowHeight: "auto"', () => {
|
||||
const result = migrations['8.2.0'](getExample(true), context) as ReturnType<
|
||||
SavedObjectMigrationFn<LensDocShape810<VisState810>, LensDocShape810<VisState820>>
|
||||
>;
|
||||
|
||||
expect(result.attributes.state.visualization as VisState820).toEqual(
|
||||
expect.objectContaining({
|
||||
rowHeight: 'auto',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should migrate disabled fitRowToContent to new rowHeight: "single"', () => {
|
||||
const result = migrations['8.2.0'](getExample(false), context) as ReturnType<
|
||||
SavedObjectMigrationFn<LensDocShape810<VisState810>, LensDocShape810<VisState820>>
|
||||
>;
|
||||
|
||||
expect(result.attributes.state.visualization as VisState820).toEqual(
|
||||
expect.objectContaining({
|
||||
rowHeight: 'single',
|
||||
rowHeightLines: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
getLensCustomVisualizationMigrations,
|
||||
commonRenameRecordsField,
|
||||
fixLensTopValuesCustomFormatting,
|
||||
commonSetLastValueShowArrayValues,
|
||||
commonEnhanceTableRowHeight,
|
||||
} from './common_migrations';
|
||||
|
||||
|
@ -465,6 +466,12 @@ const addParentFormatter: SavedObjectMigrationFn<LensDocShape810, LensDocShape81
|
|||
return { ...newDoc, attributes: fixLensTopValuesCustomFormatting(newDoc.attributes) };
|
||||
};
|
||||
|
||||
const setLastValueShowArrayValues: SavedObjectMigrationFn<LensDocShape810, LensDocShape810> = (
|
||||
doc
|
||||
) => {
|
||||
return { ...doc, attributes: commonSetLastValueShowArrayValues(doc.attributes) };
|
||||
};
|
||||
|
||||
const enhanceTableRowHeight: SavedObjectMigrationFn<LensDocShape810, LensDocShape810> = (doc) => {
|
||||
const newDoc = cloneDeep(doc);
|
||||
return { ...newDoc, attributes: commonEnhanceTableRowHeight(newDoc.attributes) };
|
||||
|
@ -484,7 +491,7 @@ const lensMigrations: SavedObjectMigrationMap = {
|
|||
'7.15.0': addLayerTypeToVisualization,
|
||||
'7.16.0': moveDefaultReversedPaletteToCustom,
|
||||
'8.1.0': flow(renameFilterReferences, renameRecordsField, addParentFormatter),
|
||||
'8.2.0': enhanceTableRowHeight,
|
||||
'8.2.0': flow(setLastValueShowArrayValues, enhanceTableRowHeight),
|
||||
};
|
||||
|
||||
export const getAllMigrations = (
|
||||
|
|
|
@ -280,6 +280,7 @@ export class LensAttributes {
|
|||
filter: columnFilter,
|
||||
params: {
|
||||
sortField: '@timestamp',
|
||||
showArrayValues: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue