[Lens] allow top metric for last value (#127151)

This commit is contained in:
Andrew Tate 2022-03-15 10:02:31 -05:00 committed by GitHub
parent 7b86f00528
commit a389226138
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 652 additions and 125 deletions

View file

@ -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', () => {

View file

@ -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>
</>
);
},

View file

@ -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();
});
});
});

View file

@ -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)
);

View file

@ -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,
},

View file

@ -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) {

View file

@ -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,

View file

@ -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> => {

View file

@ -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,
})
);
});
});
});
});

View file

@ -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 = (

View file

@ -280,6 +280,7 @@ export class LensAttributes {
filter: columnFilter,
params: {
sortField: '@timestamp',
showArrayValues: false,
},
};
}