[Lens][Agg based XY] Convert to Lens Agg based XY. (#142936)

* Add conversion config for agg based XY

* Fixed line stacked charts and buckets for layers

* fixed bundle size

* Fixed tests

* Fixed tests, decreased bundle size

* Add possibility to return several layers from getColumnsFromVis

* Some refactoring

* updated limits

* Updated limits

* Added tests for configuration

* updated bundle size limit

* Added tests for getCustomBucketColumns

* Added some functional tests

* Fix gauge/goal configurations

* Fixed nits

* Fixed parent pipeline aggregation, multi split series and respect Show values option

* Added support of current time marker, use one layer for metrics with the same chart type and mode

* Added support of assign color

* Fix terms with order by column

* Fixed sibling pipeline aggs

* Fixed test

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:
Uladzislau Lasitsa 2022-10-14 12:31:51 +03:00 committed by GitHub
parent 7ba6093d49
commit 3cb5fedca7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1737 additions and 326 deletions

View file

@ -140,6 +140,6 @@ pageLoadAssetSize:
visTypeTimeseries: 55203
visTypeVega: 153573
visTypeVislib: 242838
visTypeXy: 30000
visTypeXy: 31800
visualizations: 90000
watcher: 43598

View file

@ -28,6 +28,7 @@ describe('layeredXyVis', () => {
args: { ...rest, layers: [sampleExtendedLayer] },
syncColors: false,
syncTooltips: false,
canNavigateToLens: false,
},
});
});

View file

@ -61,6 +61,7 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers)
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
canNavigateToLens: Boolean(handlers.variables.canNavigateToLens),
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
},

View file

@ -38,6 +38,7 @@ describe('xyVis', () => {
},
],
},
canNavigateToLens: false,
syncColors: false,
syncTooltips: false,
},
@ -346,6 +347,7 @@ describe('xyVis', () => {
},
],
},
canNavigateToLens: false,
syncColors: false,
syncTooltips: false,
},

View file

@ -136,6 +136,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
canNavigateToLens: Boolean(handlers.variables.canNavigateToLens),
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
},

View file

@ -18,6 +18,7 @@ export interface XYChartProps {
args: XYProps;
syncTooltips: boolean;
syncColors: boolean;
canNavigateToLens?: boolean;
}
export interface XYRender {

View file

@ -110,7 +110,7 @@ declare global {
}
}
export type XYChartRenderProps = XYChartProps & {
export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
chartsThemeService: ChartsPluginSetup['theme'];
chartsActiveCursorService: ChartsPluginStart['activeCursor'];
data: DataPublicPluginStart;

View file

@ -52,6 +52,7 @@ interface XyChartRendererDeps {
const extractCounterEvents = (
originatingApp: string,
{ layers, yAxisConfigs }: XYChartProps['args'],
canNavigateToLens: boolean,
services: {
getDataLayers: typeof getDataLayers;
}
@ -149,6 +150,7 @@ const extractCounterEvents = (
(aggregateLayers.length === 1 && dataLayer.splitAccessors?.length)
? 'aggregate_bucket'
: undefined,
canNavigateToLens ? `render_${byTypes.mixedXY ? 'mixed_xy' : type}_convertable` : undefined,
]
.filter(Boolean)
.map((item) => `render_${originatingApp}_${item}`);
@ -188,9 +190,14 @@ export const getXyChartRenderer = ({
const visualizationType = extractVisualizationType(executionContext);
if (deps.usageCollection && containerType && visualizationType) {
const uiEvents = extractCounterEvents(visualizationType, config.args, {
getDataLayers,
});
const uiEvents = extractCounterEvents(
visualizationType,
config.args,
Boolean(config.canNavigateToLens),
{
getDataLayers,
}
);
if (uiEvents) {
deps.usageCollection.reportUiCounter(containerType, METRIC_TYPE.COUNT, uiEvents);

View file

@ -60,7 +60,7 @@ describe('getConfiguration', () => {
const metricAccessor = 'metric-id';
const breakdownByAccessor = 'bucket-id';
const metrics = [metricAccessor];
const buckets = [breakdownByAccessor];
const buckets = { all: [breakdownByAccessor], customBuckets: {} };
const maxAccessor = 'max-accessor-id';
const collapseFn = 'sum';
expect(
@ -69,7 +69,7 @@ describe('getConfiguration', () => {
buckets,
maxAccessor,
columnsWithoutReferenced: [],
bucketCollapseFn: { [metricAccessor]: collapseFn },
bucketCollapseFn: { [collapseFn]: [breakdownByAccessor] },
})
).toEqual({
breakdownByAccessor,

View file

@ -22,14 +22,22 @@ export const getConfiguration = (
bucketCollapseFn,
}: {
metrics: string[];
buckets: string[];
buckets: {
all: string[];
customBuckets: Record<string, string>;
};
maxAccessor: string;
columnsWithoutReferenced: Column[];
bucketCollapseFn?: Record<string, string | undefined>;
bucketCollapseFn?: Record<string, string[]>;
}
): MetricVisConfiguration => {
const [metricAccessor] = metrics;
const [breakdownByAccessor] = buckets;
const [breakdownByAccessor] = buckets.all;
const collapseFn = bucketCollapseFn
? Object.keys(bucketCollapseFn).find((key) =>
bucketCollapseFn[key].includes(breakdownByAccessor)
)
: undefined;
return {
layerId,
layerType: 'data',
@ -37,7 +45,7 @@ export const getConfiguration = (
metricAccessor,
breakdownByAccessor,
maxAccessor,
collapseFn: Object.values(bucketCollapseFn ?? {})[0],
collapseFn,
subtitle: gauge.labels.show && gauge.style.subText ? gauge.style.subText : undefined,
};
};

View file

@ -104,22 +104,26 @@ describe('convertToLens', () => {
});
test('should return null if metrics count is more than 1', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1', '2'],
buckets: [],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1', '2'],
buckets: { all: [] },
columns: [{ columnId: '2' }, { columnId: '1' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if metric column data type is different from number', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1'],
buckets: [],
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1'],
buckets: { all: [] },
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
@ -129,15 +133,17 @@ describe('convertToLens', () => {
layerType: 'data',
};
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1'],
buckets: [],
columns: [{ columnId: '1', dataType: 'number' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1'],
buckets: { all: [] },
columns: [{ columnId: '1', dataType: 'number' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
},
]);
mockGetConfiguration.mockReturnValue(config);
const result = await convertToLens(vis, timefilter);

View file

@ -53,7 +53,7 @@ export const convertToLens: ConvertGaugeVisToLensVisualization = async (vis, tim
const percentageModeConfig = getPercentageModeConfig(vis.params.gauge, false);
const result = getColumnsFromVis(
const layers = getColumnsFromVis(
vis,
timefilter,
dataView,
@ -63,17 +63,19 @@ export const convertToLens: ConvertGaugeVisToLensVisualization = async (vis, tim
{ dropEmptyRowsInDateHistogram: true, ...percentageModeConfig }
);
if (result === null) {
if (layers === null) {
return null;
}
const [layerConfig] = layers;
// for now, multiple metrics are not supported
if (result.metrics.length > 1 || result.buckets.length) {
if (layerConfig.metrics.length > 1 || layerConfig.buckets.all.length) {
return null;
}
if (result.metrics[0]) {
const metric = result.columns.find(({ columnId }) => columnId === result.metrics[0]);
if (layerConfig.metrics[0]) {
const metric = layerConfig.columns.find(({ columnId }) => columnId === layerConfig.metrics[0]);
if (metric?.dataType !== 'number') {
return null;
}
@ -82,11 +84,11 @@ export const convertToLens: ConvertGaugeVisToLensVisualization = async (vis, tim
const layerId = uuid();
const indexPatternId = dataView.id!;
const metricAccessor = result.metrics[0];
const metricAccessor = layerConfig.metrics[0];
const { min, max, isPercentageMode } = percentageModeConfig as PercentageModeConfigWithMinMax;
const minColumn = createStaticValueColumn(isPercentageMode ? 0 : min);
const maxColumn = createStaticValueColumn(isPercentageMode ? 1 : max);
const columns = [...result.columns, minColumn, maxColumn];
const columns = [...layerConfig.columns, minColumn, maxColumn];
return {
type: 'lnsGauge',

View file

@ -104,31 +104,37 @@ describe('convertToLens', () => {
});
test('should return null if metrics count is more than 1', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1', '2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1', '2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if buckets count is more than 1', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: [],
buckets: ['1', '2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: [],
buckets: { all: ['1', '2'] },
columns: [{ columnId: '2' }, { columnId: '1' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if metric column data type is different from number', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1'],
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1'],
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
@ -139,15 +145,17 @@ describe('convertToLens', () => {
metricAccessor: '1',
};
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1'],
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1'],
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
},
]);
mockGetConfiguration.mockReturnValue(config);
const result = await convertToLens(vis, timefilter);

View file

@ -53,7 +53,7 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time
const percentageModeConfig = getPercentageModeConfig(vis.params.gauge, false);
const result = getColumnsFromVis(
const layers = getColumnsFromVis(
vis,
timefilter,
dataView,
@ -63,17 +63,19 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time
{ dropEmptyRowsInDateHistogram: true, ...percentageModeConfig }
);
if (result === null) {
if (layers === null) {
return null;
}
const [layerConfig] = layers;
// for now, multiple metrics are not supported
if (result.metrics.length > 1 || result.buckets.length > 1) {
if (layerConfig.metrics.length > 1 || layerConfig.buckets.all.length > 1) {
return null;
}
if (result.metrics[0]) {
const metric = result.columns.find(({ columnId }) => columnId === result.metrics[0]);
if (layerConfig.metrics[0]) {
const metric = layerConfig.columns.find(({ columnId }) => columnId === layerConfig.metrics[0]);
if (metric?.dataType !== 'number') {
return null;
}
@ -81,7 +83,7 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time
const { isPercentageMode, max } = percentageModeConfig as PercentageModeConfigWithMinMax;
const maxColumn = createStaticValueColumn(isPercentageMode ? 1 : max);
const columns = [...result.columns, maxColumn];
const columns = [...layerConfig.columns, maxColumn];
const layerId = uuid();
const indexPatternId = dataView.id!;
@ -100,7 +102,7 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time
vis.params,
getPalette(vis.params.gauge, percentageModeConfig, true),
{
...result,
...layerConfig,
maxAccessor: maxColumn.columnId,
}
),

View file

@ -48,9 +48,9 @@ describe('getConfiguration', () => {
expect(
getConfiguration(layerId, params, palette, {
metrics: [metric],
buckets: [bucket],
buckets: { all: [bucket], customBuckets: { metric: bucket } },
columnsWithoutReferenced: [],
bucketCollapseFn: { [metric]: collapseFn },
bucketCollapseFn: { [collapseFn]: [bucket] },
})
).toEqual({
breakdownByAccessor: bucket,

View file

@ -21,19 +21,27 @@ export const getConfiguration = (
bucketCollapseFn,
}: {
metrics: string[];
buckets: string[];
buckets: {
all: string[];
customBuckets: Record<string, string>;
};
columnsWithoutReferenced: Column[];
bucketCollapseFn?: Record<string, string | undefined>;
bucketCollapseFn?: Record<string, string[]>;
}
): MetricVisConfiguration => {
const [metricAccessor] = metrics;
const [breakdownByAccessor] = buckets;
const [breakdownByAccessor] = buckets.all;
const collapseFn = bucketCollapseFn
? Object.keys(bucketCollapseFn).find((key) =>
bucketCollapseFn[key].includes(breakdownByAccessor)
)
: undefined;
return {
layerId,
layerType: 'data',
palette: params.metric.metricColorMode !== 'None' ? palette : undefined,
metricAccessor,
breakdownByAccessor,
collapseFn: Object.values(bucketCollapseFn ?? {})[0],
collapseFn,
};
};

View file

@ -83,31 +83,37 @@ describe('convertToLens', () => {
});
test('should return null if metrics count is more than 1', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1', '2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1', '2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if buckets count is more than 1', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: [],
buckets: ['1', '2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: [],
buckets: { all: ['1', '2'] },
columns: [{ columnId: '2' }, { columnId: '1' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if metric column data type is different from number', async () => {
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1'],
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1'],
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }],
},
]);
const result = await convertToLens(vis, timefilter);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
@ -118,15 +124,17 @@ describe('convertToLens', () => {
metricAccessor: '1',
};
mockGetColumnsFromVis.mockReturnValue({
metrics: ['1'],
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
});
mockGetColumnsFromVis.mockReturnValue([
{
metrics: ['1'],
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
},
]);
mockGetConfiguration.mockReturnValue(config);
const result = await convertToLens(vis, timefilter);

View file

@ -46,7 +46,7 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti
await Promise.all([convertToLensModule, import('./configurations')]);
const percentageModeConfig = getPercentageModeConfig(vis.params.metric);
const result = getColumnsFromVis(
const layers = getColumnsFromVis(
vis,
timefilter,
dataView,
@ -56,17 +56,19 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti
{ dropEmptyRowsInDateHistogram: true, ...percentageModeConfig }
);
if (result === null) {
if (layers === null) {
return null;
}
const [layerConfig] = layers;
// for now, multiple metrics are not supported
if (result.metrics.length > 1 || result.buckets.length > 1) {
if (layerConfig.metrics.length > 1 || layerConfig.buckets.all.length > 1) {
return null;
}
if (result.metrics[0]) {
const metric = result.columns.find(({ columnId }) => columnId === result.metrics[0]);
if (layerConfig.metrics[0]) {
const metric = layerConfig.columns.find(({ columnId }) => columnId === layerConfig.metrics[0]);
if (metric?.dataType !== 'number') {
return null;
}
@ -81,7 +83,7 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti
{
indexPatternId,
layerId,
columns: result.columns.map(excludeMetaFromColumn),
columns: layerConfig.columns.map(excludeMetaFromColumn),
columnOrder: [],
},
],
@ -89,7 +91,7 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti
layerId,
vis.params,
getPalette(vis.params.metric, percentageModeConfig),
result
layerConfig
),
indexPatternIds: [indexPatternId],
};

View file

@ -19,7 +19,7 @@ describe('getConfiguration', () => {
expect(
getConfiguration('test1', samplePieVis as any, {
metrics: ['metric-1'],
buckets: ['bucket-1'],
buckets: { all: ['bucket-1'], customBuckets: {} },
})
).toEqual({
layers: [
@ -55,7 +55,7 @@ describe('getConfiguration', () => {
{ ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'hide' } } as any,
{
metrics: ['metric-1'],
buckets: ['bucket-1'],
buckets: { all: ['bucket-1'], customBuckets: {} },
}
)
).toEqual({
@ -73,7 +73,7 @@ describe('getConfiguration', () => {
{ ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'show' } } as any,
{
metrics: ['metric-1'],
buckets: ['bucket-1'],
buckets: { all: ['bucket-1'], customBuckets: {} },
}
)
).toEqual({
@ -92,7 +92,7 @@ describe('getConfiguration', () => {
{ ...samplePieVis, params: { ...samplePieVis.params, legendDisplay } } as any,
{
metrics: ['metric-1'],
buckets: ['bucket-1'],
buckets: { all: ['bucket-1'], customBuckets: {} },
}
)
).toEqual({

View file

@ -68,12 +68,15 @@ export const getConfiguration = (
buckets,
}: {
metrics: string[];
buckets: string[];
buckets: {
all: string[];
customBuckets: Record<string, string>;
};
}
): PartitionVisConfiguration => {
return {
shape: vis.params.isDonut ? 'donut' : 'pie',
layers: getLayers(layerId, vis, metrics, buckets),
layers: getLayers(layerId, vis, metrics, buckets.all),
palette: vis.params.palette,
};
};

View file

@ -40,28 +40,34 @@ describe('convertToLens', () => {
});
test('should return null if more than three split slice levels', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: ['1', '2', '3', '4'],
});
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['1', '2', '3', '4'] },
},
]);
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if no one split slices', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: [],
});
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: [] },
},
]);
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should state for valid vis', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1' }],
},
]);
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(mockGetConfiguration).toBeCalledTimes(1);

View file

@ -44,19 +44,21 @@ export const convertToLens: ConvertPieToLensVisualization = async (vis, timefilt
}
const { getColumnsFromVis } = await convertToLensModule;
const result = getColumnsFromVis(vis, timefilter, dataView, {
const layers = getColumnsFromVis(vis, timefilter, dataView, {
buckets: [],
splits: ['segment'],
unsupported: ['split_row', 'split_column'],
});
if (result === null) {
if (layers === null) {
return null;
}
const [layerConfig] = layers;
// doesn't support more than three split slice levels
// doesn't support pie without at least one split slice
if (result.buckets.length > 3 || !result.buckets.length) {
if (layerConfig.buckets.all.length > 3 || !layerConfig.buckets.all.length) {
return null;
}
@ -69,11 +71,11 @@ export const convertToLens: ConvertPieToLensVisualization = async (vis, timefilt
{
indexPatternId,
layerId,
columns: result.columns.map(excludeMetaFromColumn),
columns: layerConfig.columns.map(excludeMetaFromColumn),
columnOrder: [],
},
],
configuration: getConfiguration(layerId, vis, result),
configuration: getConfiguration(layerId, vis, layerConfig),
indexPatternIds: [indexPatternId],
};
};

View file

@ -25,7 +25,7 @@ describe('getConfiguration', () => {
expect(
getConfiguration('test1', params, {
metrics: ['metric-1'],
buckets: ['bucket-1'],
buckets: { all: ['bucket-1'], customBuckets: { 'metric-1': 'bucket-1' } },
columnsWithoutReferenced: [
{
columnId: 'metric-1',
@ -48,7 +48,7 @@ describe('getConfiguration', () => {
},
},
],
bucketCollapseFn: { 'bucket-1': 'sum' },
bucketCollapseFn: { sum: ['bucket-1'] },
})
).toEqual({
columns: [

View file

@ -12,19 +12,21 @@ import { TableVisParams } from '../../../common';
const getColumns = (
params: TableVisParams,
metrics: string[],
buckets: string[],
columns: Column[],
bucketCollapseFn?: Record<string, string | undefined>
bucketCollapseFn?: Record<string, string[]>
) => {
const { showTotal, totalFunc } = params;
return columns.map(({ columnId }) => ({
columnId,
alignment: 'left' as const,
...(showTotal && metrics.includes(columnId) ? { summaryRow: totalFunc } : {}),
...(bucketCollapseFn && bucketCollapseFn[columnId]
? { collapseFn: bucketCollapseFn[columnId] }
: {}),
}));
return columns.map(({ columnId }) => {
const collapseFn = bucketCollapseFn
? Object.keys(bucketCollapseFn).find((key) => bucketCollapseFn[key].includes(columnId))
: undefined;
return {
columnId,
alignment: 'left' as const,
...(showTotal && metrics.includes(columnId) ? { summaryRow: totalFunc } : {}),
...(collapseFn ? { collapseFn } : {}),
};
});
};
const getPagination = ({ perPage }: TableVisParams): PagingState => {
@ -54,15 +56,18 @@ export const getConfiguration = (
bucketCollapseFn,
}: {
metrics: string[];
buckets: string[];
buckets: {
all: string[];
customBuckets: Record<string, string>;
};
columnsWithoutReferenced: Column[];
bucketCollapseFn?: Record<string, string | undefined>;
bucketCollapseFn?: Record<string, string[]>;
}
): TableVisConfiguration => {
return {
layerId,
layerType: 'data',
columns: getColumns(params, metrics, buckets, columnsWithoutReferenced, bucketCollapseFn),
columns: getColumns(params, metrics, columnsWithoutReferenced, bucketCollapseFn),
paging: getPagination(params),
...getRowHeight(params),
};

View file

@ -63,14 +63,16 @@ describe('convertToLens', () => {
});
test('should return null if can not build percentage column', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
});
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
},
]);
mockGetVisSchemas.mockReturnValue({
metric: [{ label: 'Count', aggId: 'agg-1' }],
});
@ -83,14 +85,16 @@ describe('convertToLens', () => {
});
test('should return correct state for valid vis', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
});
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['2'] },
columns: [{ columnId: '2' }, { columnId: '1' }],
columnsWithoutReferenced: [
{ columnId: '1', meta: { aggId: 'agg-1' } },
{ columnId: '2', meta: { aggId: 'agg-2' } },
],
},
]);
mockGetVisSchemas.mockReturnValue({
metric: [{ label: 'Count', aggId: 'agg-1' }],
});

View file

@ -46,7 +46,7 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi
}
const { getColumnsFromVis, getPercentageColumnFormulaColumn } = await convertToLensModule;
const result = getColumnsFromVis(
const layers = getColumnsFromVis(
vis,
timefilter,
dataView,
@ -57,10 +57,12 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi
{ dropEmptyRowsInDateHistogram: true, isPercentageMode: false }
);
if (result === null) {
if (layers === null) {
return null;
}
const [layerConfig] = layers;
if (vis.params.percentageCol) {
const visSchemas = getVisSchemas(vis, {
timefilter,
@ -78,12 +80,12 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi
if (!percentageColumn) {
return null;
}
result.columns.splice(
result.columnsWithoutReferenced.findIndex((c) => c.meta.aggId === metricAgg.aggId) + 1,
layerConfig.columns.splice(
layerConfig.columnsWithoutReferenced.findIndex((c) => c.meta.aggId === metricAgg.aggId) + 1,
0,
percentageColumn
);
result.columnsWithoutReferenced.push(percentageColumn);
layerConfig.columnsWithoutReferenced.push(percentageColumn);
}
const layerId = uuid();
@ -94,11 +96,11 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi
{
indexPatternId,
layerId,
columns: result.columns.map(excludeMetaFromColumn),
columns: layerConfig.columns.map(excludeMetaFromColumn),
columnOrder: [],
},
],
configuration: getConfiguration(layerId, vis.params, result),
configuration: getConfiguration(layerId, vis.params, layerConfig),
indexPatternIds: [indexPatternId],
};
};

View file

@ -3,7 +3,7 @@
"version": "kibana",
"ui": true,
"server": true,
"requiredPlugins": ["charts", "visualizations", "data", "expressions"],
"requiredPlugins": ["charts", "visualizations", "data", "expressions", "dataViews"],
"requiredBundles": ["kibanaUtils", "visDefaultEditor"],
"extraPublicDirs": ["common/index"],
"owner": {

View file

@ -0,0 +1,160 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Column } from '@kbn/visualizations-plugin/common/convert_to_lens';
import { getConfiguration } from '.';
import { Layer } from '..';
import { ChartType } from '../..';
import { sampleAreaVis } from '../../sample_vis.test.mocks';
import { ChartMode, InterpolationMode } from '../../types';
describe('getConfiguration', () => {
const layers: Layer[] = [
{
indexPatternId: '',
layerId: 'layer-1',
columns: [
{ columnId: '1', isBucketed: false },
{ columnId: '2', isBucketed: true, isSplit: false, operationType: 'date_histogram' },
{ columnId: '3', isBucketed: true, isSplit: true },
] as Column[],
metrics: ['1'],
columnOrder: [],
seriesIdsMap: { 1: '1' },
collapseFn: 'max',
isReferenceLineLayer: false,
},
{
indexPatternId: '',
layerId: 'layer-2',
columns: [
{ columnId: '4', isBucketed: false },
{ columnId: '5', isBucketed: true, isSplit: false, operationType: 'date_histogram' },
] as Column[],
metrics: ['4'],
columnOrder: [],
seriesIdsMap: { 4: '2' },
collapseFn: undefined,
isReferenceLineLayer: false,
},
{
indexPatternId: '',
layerId: 'layer-3',
columns: [{ columnId: '7', isBucketed: false }] as Column[],
columnOrder: [],
metrics: ['7'],
seriesIdsMap: {},
collapseFn: undefined,
isReferenceLineLayer: true,
},
];
const series = [
{
show: true,
type: ChartType.Area,
mode: ChartMode.Stacked,
data: {
label: 'Sum of total_quantity',
id: '1',
},
drawLinesBetweenPoints: true,
showCircles: true,
circlesRadius: 5,
interpolate: InterpolationMode.Linear,
valueAxis: 'ValueAxis-1',
},
{
show: true,
type: ChartType.Line,
mode: ChartMode.Stacked,
data: {
label: 'Sum of total_quantity 1',
id: '2',
},
drawLinesBetweenPoints: true,
showCircles: true,
circlesRadius: 5,
interpolate: InterpolationMode.Linear,
valueAxis: 'ValueAxis-1',
},
];
test('should return correct configuration', () => {
expect(getConfiguration(layers, series, sampleAreaVis as any)).toEqual({
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
curveType: 'LINEAR',
fillOpacity: 0.5,
fittingFunction: undefined,
gridlinesVisibilitySettings: { x: false, yLeft: false, yRight: true },
labelsOrientation: { x: -0, yLeft: -0, yRight: -90 },
layers: [
{
accessors: ['1'],
collapseFn: 'max',
isHistogram: true,
layerId: 'layer-1',
layerType: 'data',
palette: { name: 'default' },
seriesType: 'area_stacked',
simpleView: false,
splitAccessor: '3',
xAccessor: '2',
xScaleType: 'ordinal',
yConfig: [{ axisMode: 'left', forAccessor: '1' }],
},
{
accessors: ['4'],
collapseFn: undefined,
isHistogram: true,
layerId: 'layer-2',
layerType: 'data',
palette: { name: 'default' },
seriesType: 'area_stacked',
simpleView: false,
splitAccessor: undefined,
xAccessor: '5',
xScaleType: 'ordinal',
yConfig: [{ axisMode: 'left', forAccessor: '4' }],
},
{
accessors: ['7'],
layerId: 'layer-3',
layerType: 'referenceLine',
yConfig: [
{
axisMode: 'left',
color: '#E7664C',
forAccessor: '7',
lineStyle: 'solid',
lineWidth: 1,
},
],
},
],
legend: {
isVisible: true,
legendSize: 'small',
maxLines: 1,
position: 'top',
shouldTruncate: true,
showSingleSeries: true,
},
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
valueLabels: 'hide',
valuesInLegend: false,
xTitle: undefined,
yLeftExtent: { enforce: true, lowerBound: undefined, mode: 'full', upperBound: undefined },
yLeftScale: 'linear',
yRightExtent: undefined,
yRightScale: 'linear',
yRightTitle: undefined,
yTitle: 'Sum of total_quantity',
showCurrentTimeMarker: false,
});
});
});

View file

@ -0,0 +1,277 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Position, ScaleType as ECScaleType } from '@elastic/charts';
import {
SeriesTypes,
Column,
XYConfiguration,
XYDataLayerConfig,
XYReferenceLineLayerConfig,
} from '@kbn/visualizations-plugin/common/convert_to_lens';
import { Vis } from '@kbn/visualizations-plugin/public';
import { Layer } from '..';
import { ChartType } from '../../../common';
import {
CategoryAxis,
ChartMode,
InterpolationMode,
Scale,
ScaleType,
SeriesParam,
ValueAxis,
VisParams,
} from '../../types';
import { getCurveType, getLineStyle, getMode, getYAxisPosition } from '../../utils/common';
function getYScaleType(scale?: Scale): XYConfiguration['yLeftScale'] | undefined {
const type = scale?.type;
if (type === ScaleType.SquareRoot) {
return ECScaleType.Sqrt;
}
return type;
}
function getXScaleType(xColumn?: Column) {
if (xColumn?.dataType === 'date') return ECScaleType.Time;
if (xColumn?.dataType !== 'number') {
return ECScaleType.Ordinal;
}
return ECScaleType.Linear;
}
function getLabelOrientation(data?: CategoryAxis, isTimeChart = false) {
// lens doesn't support 75 as rotate option, we should use 45 instead
return -(data?.labels.rotate === 75 ? 45 : data?.labels.rotate ?? (isTimeChart ? 0 : 90));
}
function getExtents(axis: ValueAxis, series: SeriesParam[]) {
// for area and bar charts we should include 0 to bounds
const isAssignedToAreaOrBar = series.some(
(s) => s.valueAxis === axis.id && (s.type === 'histogram' || s.type === 'area')
);
return {
mode: getMode(axis.scale),
lowerBound:
axis.scale.min !== null
? isAssignedToAreaOrBar && axis.scale.min && axis.scale.min > 0
? 0
: axis.scale.min
: undefined,
upperBound:
axis.scale.max !== null
? isAssignedToAreaOrBar && axis.scale.max && axis.scale.max < 0
? 0
: axis.scale.max
: undefined,
enforce: true,
};
}
function getSeriesType(
type?: ChartType,
mode?: ChartMode,
isHorizontal?: boolean,
isPercentage?: boolean
): XYDataLayerConfig['seriesType'] {
let seriesType: XYDataLayerConfig['seriesType'] =
type === 'histogram' ? SeriesTypes.BAR : type ?? SeriesTypes.AREA;
// only bar chart supports horizontal mode
if (isHorizontal && seriesType === SeriesTypes.BAR) {
seriesType = (seriesType + '_horizontal') as XYDataLayerConfig['seriesType'];
}
// line percentage should convert to area percentage
if (isPercentage) {
seriesType = ((seriesType !== SeriesTypes.LINE ? seriesType : SeriesTypes.AREA) +
'_percentage') as XYDataLayerConfig['seriesType'];
}
// percentage chart should be stacked
// line stacked should convert to area stacked
if (isPercentage || mode === 'stacked') {
seriesType = ((seriesType !== SeriesTypes.LINE ? seriesType : SeriesTypes.AREA) +
'_stacked') as XYDataLayerConfig['seriesType'];
}
return seriesType;
}
function getDataLayers(
layers: Layer[],
series: SeriesParam[],
vis: Vis<VisParams>
): XYDataLayerConfig[] {
const overwriteColors: Record<string, string> = vis.uiState.get('vis.colors', {});
return layers.map((layer) => {
const xColumn = layer.columns.find((c) => c.isBucketed && !c.isSplit);
const splitAccessor = layer.columns.find(
(column) => column.isBucketed && column.isSplit
)?.columnId;
// as type and mode will be the same for all metrics we can use first to define it
const firstSeries = series.find((s) => s.data.id === layer.seriesIdsMap[layer.metrics[0]]);
const isHistogram =
xColumn?.operationType === 'date_histogram' ||
(xColumn?.operationType === 'range' && xColumn.params.type === 'histogram');
const firstYAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find(
(axis) => axis.id === firstSeries?.valueAxis
);
const isPercentage = firstYAxis?.scale.mode === 'percentage';
const isHorizontal =
firstYAxis?.position !== Position.Left && firstYAxis?.position !== Position.Right;
const seriesType = getSeriesType(
firstSeries?.type,
firstSeries?.mode,
isHorizontal,
isPercentage
);
return {
layerId: layer.layerId,
accessors: layer.metrics,
layerType: 'data',
seriesType,
xAccessor: xColumn?.columnId,
simpleView: false,
splitAccessor,
palette: vis.params.palette ?? vis.type.visConfig.defaults.palette,
yConfig: layer.metrics.map((metricId) => {
const serie = series.find((s) => s.data.id === layer.seriesIdsMap[metricId]);
const yAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find(
(axis) => axis.id === serie?.valueAxis
);
return {
forAccessor: metricId,
axisMode: getYAxisPosition(yAxis?.position ?? 'left'),
color:
!splitAccessor && serie?.data.label ? overwriteColors[serie?.data.label] : undefined,
};
}),
xScaleType: getXScaleType(xColumn),
isHistogram,
collapseFn: layer.collapseFn,
};
});
}
function getReferenceLineLayers(
layers: Layer[],
vis: Vis<VisParams>
): XYReferenceLineLayerConfig[] {
const thresholdLineConfig = vis.params.thresholdLine ?? vis.type.visConfig.defaults.thresholdLine;
// threshold line is always assigned to the first value axis
const yAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes)[0];
return layers.map((layer) => {
return {
layerType: 'referenceLine',
layerId: layer.layerId,
accessors: layer.metrics,
yConfig: layer.metrics.map((metricId) => {
return {
forAccessor: metricId,
axisMode: getYAxisPosition(yAxis?.position ?? 'left'),
color: thresholdLineConfig.color,
lineWidth: thresholdLineConfig.width !== null ? thresholdLineConfig.width : undefined,
lineStyle: getLineStyle(thresholdLineConfig.style),
};
}),
};
});
}
export const getConfiguration = (
layers: Layer[],
series: SeriesParam[],
vis: Vis<VisParams>
): XYConfiguration => {
const legendDisplayFromUiState = vis.uiState.get('vis.legendOpen') ?? true;
const yRightAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find(
(axis) => getYAxisPosition(axis.position) === Position.Right
);
const yLeftAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find(
(axis) => getYAxisPosition(axis.position) === Position.Left
);
// as we have only one x-axis
const xAxis = (vis.params.categoryAxes ?? vis.type.visConfig.defaults.categoryAxes)[0];
const axisTitlesVisibilitySettings = {
x: xAxis.show,
yLeft: yLeftAxis?.show ?? true,
yRight: yRightAxis?.show ?? true,
};
const xColumn = layers[0].columns.find((c) => c.isBucketed && !c.isSplit);
const isTimeChart = xColumn?.operationType === 'date_histogram';
const fittingFunction = vis.params.fittingFunction ?? vis.type.visConfig.defaults.fittingFunction;
return {
layers: [
...getDataLayers(
layers.filter((l) => !l.isReferenceLineLayer),
series,
vis
),
...getReferenceLineLayers(
layers.filter((l) => l.isReferenceLineLayer),
vis
),
],
legend: {
isVisible:
legendDisplayFromUiState && (vis.params.addLegend ?? vis.type.visConfig.defaults.addLegend),
position: vis.params.legendPosition ?? vis.type.visConfig.defaults.legendPosition,
legendSize: vis.params.legendSize ?? vis.type.visConfig.defaults.legendSize,
shouldTruncate: vis.params.truncateLegend ?? vis.type.visConfig.defaults.truncateLegend,
maxLines: vis.params.maxLegendLines ?? vis.type.visConfig.defaults.maxLegendLines,
showSingleSeries: true,
},
fittingFunction: fittingFunction
? fittingFunction[0].toUpperCase() + fittingFunction.slice(1)
: undefined,
fillOpacity: vis.params.fillOpacity ?? vis.type.visConfig.defaults.fillOpacity,
gridlinesVisibilitySettings: {
x: vis.params.grid.categoryLines ?? vis.type.visConfig.defaults.grid?.categoryLines,
yLeft:
(vis.params.grid.valueAxis ?? vis.type.visConfig.defaults.grid?.valueAxis) ===
yLeftAxis?.id,
yRight:
(vis.params.grid.valueAxis ?? vis.type.visConfig.defaults.grid?.valueAxis) ===
yRightAxis?.id,
},
axisTitlesVisibilitySettings,
tickLabelsVisibilitySettings: {
x: axisTitlesVisibilitySettings.x && (xAxis.labels.show ?? true),
yLeft: axisTitlesVisibilitySettings.yLeft && (yLeftAxis?.labels.show ?? true),
yRight: axisTitlesVisibilitySettings.yRight && (yRightAxis?.labels.show ?? true),
},
labelsOrientation: {
x: getLabelOrientation(xAxis, isTimeChart),
yLeft: getLabelOrientation(yLeftAxis),
yRight: getLabelOrientation(yRightAxis),
},
yLeftScale: getYScaleType(yLeftAxis?.scale) ?? ECScaleType.Linear,
yRightScale: getYScaleType(yRightAxis?.scale) ?? ECScaleType.Linear,
yLeftExtent: yLeftAxis?.scale ? getExtents(yLeftAxis, series) : undefined,
yRightExtent: yRightAxis?.scale ? getExtents(yRightAxis, series) : undefined,
yTitle: yLeftAxis?.title.text,
yRightTitle: yRightAxis?.title.text,
xTitle: xAxis.title.text,
valueLabels:
vis.params.labels.show ?? vis.type.visConfig.defaults.labels?.show ? 'show' : 'hide',
valuesInLegend: Boolean(vis.params.labels.show ?? vis.type.visConfig.defaults.labels?.show),
showCurrentTimeMarker: isTimeChart
? Boolean(vis.params.addTimeMarker ?? vis.type.visConfig.defaults.addTimeMarker)
: undefined,
curveType: getCurveType(
series[0].interpolate === InterpolationMode.StepAfter
? InterpolationMode.Linear
: series[0].interpolate
),
};
};

View file

@ -0,0 +1,202 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { convertToLens } from '.';
import { sampleAreaVis } from '../sample_vis.test.mocks';
const mockGetColumnsFromVis = jest.fn();
const mockCreateStaticValueColumn = jest.fn().mockReturnValue({ operationType: 'static_value' });
const mockGetVisSchemas = jest.fn().mockReturnValue({
metric: [{ aggId: '1' }],
});
const mockGetConfiguration = jest.fn().mockReturnValue({});
jest.mock('../services', () => ({
getDataViewsStart: jest.fn(() => ({ get: () => ({}), getDefault: () => ({}) })),
}));
jest.mock('../utils/get_series_params', () => ({
getSeriesParams: jest.fn(() => undefined),
}));
jest.mock('@kbn/visualizations-plugin/public', () => ({
convertToLensModule: Promise.resolve({
getColumnsFromVis: jest.fn(() => mockGetColumnsFromVis()),
createStaticValueColumn: jest.fn(() => mockCreateStaticValueColumn()),
}),
getDataViewByIndexPatternId: jest.fn(() => ({ id: 'index-pattern' })),
getVisSchemas: jest.fn(() => mockGetVisSchemas()),
}));
jest.mock('./configurations', () => ({
getConfiguration: jest.fn(() => mockGetConfiguration()),
}));
describe('convertToLens', () => {
afterEach(() => {
jest.clearAllMocks();
});
test('should return null if getColumnsFromVis returns null', async () => {
mockGetColumnsFromVis.mockReturnValue(null);
const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if multi split series defined', async () => {
mockGetVisSchemas.mockReturnValue({
metric: [{ aggId: '1' }],
group: [{}, {}],
});
const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any);
expect(mockGetVisSchemas).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if sibling pipeline agg defined together with split series', async () => {
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['1'], customBuckets: { metric1: '2' } },
},
]);
mockGetVisSchemas.mockReturnValue({
metric: [{ aggId: '1' }],
group: [{}],
});
const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if defined several layers with terms split series which uses one of the metrics as order agg', async () => {
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['1'], customBuckets: { metric1: '2' } },
columns: [{ isSplit: true, params: { orderBy: { type: 'column' } } }],
},
{
buckets: { all: ['2'], customBuckets: { metric1: '2' } },
columns: [{}],
},
]);
mockGetVisSchemas.mockReturnValue({
metric: [{ aggId: '1' }, { aggId: '2' }],
});
const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if more than one axis left/right/top/bottom defined', async () => {
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['1'], customBuckets: {} },
columns: [],
},
]);
mockGetVisSchemas.mockReturnValue({
metric: [{ aggId: '1' }, { aggId: '2' }],
});
const result = await convertToLens(
{
...sampleAreaVis,
params: {
...sampleAreaVis.params,
valueAxes: [
...sampleAreaVis.params.valueAxes,
{
id: 'ValueAxis-2',
name: 'LeftAxis-2',
type: 'value',
position: 'left',
data: {
id: '2',
},
},
],
seriesParams: [
...sampleAreaVis.params.seriesParams,
{ show: true, valueAxis: 'ValueAxis-2', data: { id: '2' } },
],
},
} as any,
{ getAbsoluteTime: () => {} } as any
);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should state for valid vis', async () => {
mockGetColumnsFromVis.mockReturnValue([
{
buckets: { all: ['2', '3'], customBuckets: { 1: '3' } },
columns: [
{ columnId: '2', isBucketed: true },
{ columnId: '1', meta: { aggId: '1' } },
{ columnId: '3', isBucketed: true },
],
bucketCollapseFn: { sum: ['3'] },
metrics: ['1'],
},
{
buckets: { all: ['2'], customBuckets: {} },
columns: [
{ columnId: '2', isBucketed: true },
{ columnId: '1', meta: { aggId: '2' } },
],
metrics: ['1'],
bucketCollapseFn: {},
},
]);
mockGetVisSchemas.mockReturnValue({
metric: [{ aggId: '1' }],
});
const result = await convertToLens(
{
...sampleAreaVis,
params: {
...sampleAreaVis.params,
valueAxes: [
...sampleAreaVis.params.valueAxes,
{
id: 'ValueAxis-2',
name: 'LeftAxis-2',
type: 'value',
position: 'left',
data: {
id: '2',
},
},
],
seriesParams: [
...sampleAreaVis.params.seriesParams,
{ show: true, valueAxis: 'ValueAxis-2', data: { id: '2' } },
],
thresholdLine: { ...sampleAreaVis.params.thresholdLine, show: true },
},
} as any,
{ getAbsoluteTime: () => {} } as any
);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(mockGetConfiguration).toBeCalledTimes(1);
expect(mockCreateStaticValueColumn).toBeCalledTimes(1);
expect(result?.type).toEqual('lnsXY');
expect(result?.layers.length).toEqual(3);
expect(result?.layers[0].columns).toEqual([
{ columnId: '2', isBucketed: true },
{ columnId: '1' },
{ columnId: '3', isBucketed: true },
]);
expect(result?.layers[1].columns).toEqual([
{ columnId: '2', isBucketed: true },
{ columnId: '1' },
]);
expect(result?.layers[2].columns).toEqual([{ operationType: 'static_value' }]);
});
});

View file

@ -0,0 +1,214 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common';
import {
convertToLensModule,
getVisSchemas,
getDataViewByIndexPatternId,
} from '@kbn/visualizations-plugin/public';
import { getDataViewsStart } from '../services';
import { getSeriesParams } from '../utils/get_series_params';
import { ConvertXYToLensVisualization } from './types';
export interface Layer {
indexPatternId: string;
layerId: string;
columns: Column[];
metrics: string[];
columnOrder: never[];
seriesIdsMap: Record<string, string>;
isReferenceLineLayer: boolean;
collapseFn?: string;
}
const SIBBLING_PIPELINE_AGGS: string[] = [
METRIC_TYPES.AVG_BUCKET,
METRIC_TYPES.SUM_BUCKET,
METRIC_TYPES.MAX_BUCKET,
METRIC_TYPES.MIN_BUCKET,
];
export const isColumnWithMeta = (column: Column): column is ColumnWithMeta => {
if ((column as ColumnWithMeta).meta) {
return true;
}
return false;
};
export const excludeMetaFromColumn = (column: Column) => {
if (isColumnWithMeta(column)) {
const { meta, ...rest } = column;
return rest;
}
return column;
};
export const convertToLens: ConvertXYToLensVisualization = async (vis, timefilter) => {
if (!timefilter) {
return null;
}
const dataViews = getDataViewsStart();
const dataView = await getDataViewByIndexPatternId(vis.data.indexPattern?.id, dataViews);
if (!dataView) {
return null;
}
const visSchemas = getVisSchemas(vis, {
timefilter,
timeRange: timefilter.getAbsoluteTime(),
});
// doesn't support multi split series
if (visSchemas.group && visSchemas.group.length > 1) {
return null;
}
const firstValueAxesId = vis.params.valueAxes[0].id;
const updatedSeries = getSeriesParams(
vis.data.aggs,
vis.params.seriesParams,
'metric',
firstValueAxesId
);
const finalSeriesParams = updatedSeries ?? vis.params.seriesParams;
const visibleSeries = finalSeriesParams.filter(
(param) => param.show && visSchemas.metric.some((m) => m.aggId?.split('.')[0] === param.data.id)
);
const [{ getColumnsFromVis, createStaticValueColumn }, { getConfiguration }] = await Promise.all([
convertToLensModule,
import('./configurations'),
]);
const dataLayers = getColumnsFromVis(
vis,
timefilter,
dataView,
{
buckets: ['segment'],
splits: ['group'],
unsupported: ['split_row', 'split_column', 'radius'],
},
{
dropEmptyRowsInDateHistogram: true,
supportMixedSiblingPipelineAggs: true,
isPercentageMode: false,
},
visibleSeries
.reduce<Array<{ metrics: string[]; type: string; mode: string }>>((acc, s) => {
const series = acc.find(({ type, mode }) => type === s.type && mode === s.mode);
// sibling pipeline agg always generate new layer because of custom bucket
if (
series &&
visSchemas.metric.some(
(m) =>
m.aggId?.split('.')[0] === s.data.id && !SIBBLING_PIPELINE_AGGS.includes(m.aggType)
)
) {
series.metrics.push(s.data.id);
} else {
acc.push({ metrics: [s.data.id], type: s.type, mode: s.mode });
}
return acc;
}, [])
.map(({ metrics }) => ({ metrics }))
);
if (dataLayers === null) {
return null;
}
// doesn't support several layers with terms split series which uses one of the metrics as order agg
if (
dataLayers.length > 1 &&
dataLayers.some((l) =>
l.columns.some(
(c) => c.isSplit && 'orderBy' in c.params && c.params.orderBy.type === 'column'
)
)
) {
return null;
}
// doesn't support sibling pipeline aggs and split series together
if (
visSchemas.group?.length &&
dataLayers.some((l) => Object.keys(l.buckets.customBuckets).length)
) {
return null;
}
const visibleYAxes = vis.params.valueAxes.filter((axis) =>
visibleSeries.some((seriesParam) => seriesParam.valueAxis === axis.id)
);
const positions = visibleYAxes.map((axis) => axis.position);
const uniqPoisitions = new Set(positions);
// doesn't support more than one axis left/right/top/bottom
if (visibleYAxes.length > 1 && uniqPoisitions.size !== positions.length) {
return null;
}
const indexPatternId = dataView.id!;
const uuid = await import('uuid/v4');
const layers = dataLayers.map<Layer>((l) => {
const layerId = uuid.default();
const seriesIdsMap: Record<string, string> = {};
visibleSeries.forEach((s) => {
const column = l.columns.find(
(c) => !c.isBucketed && c.meta.aggId.split('.')[0] === s.data.id
);
if (column) {
seriesIdsMap[column.columnId] = s.data.id;
}
});
const collapseFn = l.bucketCollapseFn
? Object.keys(l.bucketCollapseFn).find((key) =>
l.bucketCollapseFn[key].includes(l.buckets.customBuckets[l.metrics[0]])
)
: undefined;
return {
indexPatternId,
layerId,
columns: l.columns.map(excludeMetaFromColumn),
metrics: l.metrics,
columnOrder: [],
seriesIdsMap,
collapseFn,
isReferenceLineLayer: false,
};
});
if (vis.params.thresholdLine.show) {
const staticValueColumn = createStaticValueColumn(vis.params.thresholdLine.value || 0);
layers.push({
indexPatternId,
layerId: uuid.default(),
columns: [staticValueColumn],
columnOrder: [],
metrics: [staticValueColumn.columnId],
isReferenceLineLayer: true,
collapseFn: undefined,
seriesIdsMap: {},
});
}
return {
type: 'lnsXY',
layers: layers.map(({ seriesIdsMap, collapseFn, isReferenceLineLayer, ...rest }) => rest),
configuration: getConfiguration(layers, visibleSeries, vis),
indexPatternIds: [indexPatternId],
};
};

View file

@ -0,0 +1,17 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TimefilterContract } from '@kbn/data-plugin/public';
import { NavigateToLensContext, XYConfiguration } from '@kbn/visualizations-plugin/common';
import { Vis } from '@kbn/visualizations-plugin/public';
import { VisParams } from '../types';
export type ConvertXYToLensVisualization = (
vis: Vis<VisParams>,
timefilter?: TimefilterContract
) => Promise<NavigateToLensContext<XYConfiguration> | null>;

View file

@ -6,10 +6,11 @@
* Side Public License, v 1.
*/
import type { CoreSetup, Plugin } from '@kbn/core/public';
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import type { VisualizationsSetup } from '@kbn/visualizations-plugin/public';
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import { setUISettings, setPalettesService } from './services';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { setUISettings, setPalettesService, setDataViewsStart } from './services';
import { visTypesDefinitions } from './vis_types';
@ -24,6 +25,11 @@ export interface VisTypeXyPluginSetupDependencies {
charts: ChartsPluginSetup;
}
/** @internal */
export interface VisTypeXyPluginStartDependencies {
dataViews: DataViewsPublicPluginStart;
}
type VisTypeXyCoreSetup = CoreSetup<{}, VisTypeXyPluginStart>;
/** @internal */
@ -42,7 +48,8 @@ export class VisTypeXyPlugin
return {};
}
public start() {
public start(core: CoreStart, { dataViews }: VisTypeXyPluginStartDependencies) {
setDataViewsStart(dataViews);
return {};
}
}

View file

@ -8,6 +8,8 @@
import { LegendSize } from '@kbn/visualizations-plugin/common';
const mockUiStateGet = jest.fn().mockReturnValue(() => false);
export const sampleAreaVis = {
type: {
name: 'area',
@ -1918,5 +1920,10 @@ export const sampleAreaVis = {
},
},
isHierarchical: () => false,
uiState: {},
uiState: {
vis: {
legendOpen: false,
},
get: mockUiStateGet,
},
};

View file

@ -8,6 +8,7 @@
import type { CoreSetup } from '@kbn/core/public';
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
@ -16,3 +17,6 @@ export const [getUISettings, setUISettings] =
export const [getPalettesService, setPalettesService] =
createGetterSetter<ChartsPluginSetup['palettes']>('xy charts.palette');
export const [getDataViewsStart, setDataViewsStart] =
createGetterSetter<DataViewsPublicPluginStart>('dataViews');

View file

@ -18,8 +18,8 @@ import {
} from '@kbn/visualizations-plugin/public';
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public';
import { BUCKET_TYPES } from '@kbn/data-plugin/public';
import { TimeRangeBounds } from '@kbn/data-plugin/common';
import { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types';
import type { TimeRangeBounds } from '@kbn/data-plugin/common';
import type { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types';
import {
Dimensions,
Dimension,
@ -30,32 +30,15 @@ import {
ValueAxis,
Scale,
ChartMode,
InterpolationMode,
ScaleType,
} from './types';
import { ChartType } from '../common';
import { getSeriesParams } from './utils/get_series_params';
import { getSafeId } from './utils/accessors';
interface Bounds {
min?: string | number;
max?: string | number;
}
import { Bounds, getCurveType, getLineStyle, getMode, getYAxisPosition } from './utils/common';
type YDimension = Omit<Dimension, 'accessor'> & { accessor: string };
const getCurveType = (type?: InterpolationMode) => {
switch (type) {
case 'cardinal':
return 'CURVE_MONOTONE_X';
case 'step-after':
return 'CURVE_STEP_AFTER';
case 'linear':
default:
return 'LINEAR';
}
};
const prepareLengend = (params: VisParams, legendSize?: LegendSize) => {
const legend = buildExpressionFunction('legendConfig', {
isVisible: params.addLegend,
@ -162,16 +145,6 @@ const prepareLayers = (
return buildExpression([dataLayer]);
};
const getMode = (scale: Scale, bounds?: Bounds) => {
if (scale.defaultYExtents) {
return 'dataBounds';
}
if (scale.setYExtents || bounds) {
return 'custom';
}
};
const getLabelArgs = (data: CategoryAxis, isTimeChart?: boolean) => {
return {
truncate: data.labels.truncate,
@ -215,18 +188,6 @@ function getScaleType(
return type;
}
function getYAxisPosition(position: Position) {
if (position === Position.Top) {
return Position.Right;
}
if (position === Position.Bottom) {
return Position.Left;
}
return position;
}
function getXAxisPosition(position: Position) {
if (position === Position.Left) {
return Position.Bottom;
@ -274,16 +235,6 @@ const prepareYAxis = (data: ValueAxis, showGridLines?: boolean) => {
return buildExpression([yAxisConfig]);
};
const getLineStyle = (style: ThresholdLine['style']) => {
switch (style) {
case 'full':
return 'solid';
case 'dashed':
case 'dot-dashed':
return style;
}
};
const prepareReferenceLine = (thresholdLine: ThresholdLine, axisId: string) => {
const referenceLine = buildExpressionFunction('referenceLine', {
value: thresholdLine.value,
@ -483,6 +434,7 @@ export const toExpressionAst: VisToExpressionAst<VisParams> = async (vis, params
splitColumnAccessor: dimensions.splitColumn?.map(prepareVisDimension),
splitRowAccessor: dimensions.splitRow?.map(prepareVisDimension),
valueLabels: vis.params.labels.show ? 'show' : 'hide',
valuesInLegend: vis.params.labels.show,
singleTable: true,
});

View file

@ -0,0 +1,62 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Position } from '@elastic/charts';
import type { AxisExtentConfig } from '@kbn/visualizations-plugin/common/convert_to_lens';
import type { InterpolationMode, Scale, ThresholdLine } from '../types';
export interface Bounds {
min?: string | number;
max?: string | number;
}
export const getCurveType = (type?: InterpolationMode) => {
switch (type) {
case 'cardinal':
return 'CURVE_MONOTONE_X';
case 'step-after':
return 'CURVE_STEP_AFTER';
case 'linear':
default:
return 'LINEAR';
}
};
export const getMode = (scale: Scale, bounds?: Bounds): AxisExtentConfig['mode'] => {
if (scale.defaultYExtents) {
return 'dataBounds';
}
if (scale.setYExtents || bounds) {
return 'custom';
}
return 'full';
};
export const getYAxisPosition = (position: Position) => {
if (position === Position.Top) {
return Position.Right;
}
if (position === Position.Bottom) {
return Position.Left;
}
return position;
};
export const getLineStyle = (style: ThresholdLine['style']) => {
switch (style) {
case 'full':
return 'solid';
case 'dashed':
case 'dot-dashed':
return style;
}
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { VisParams } from '@kbn/visualizations-plugin/common';
import type { VisParams } from '@kbn/visualizations-plugin/common';
export const getVisTypeFromParams = (params?: VisParams) => {
let type = params?.seriesParams?.[0]?.type;

View file

@ -6,14 +6,27 @@
* Side Public License, v 1.
*/
import type { VisTypeDefinition } from '@kbn/visualizations-plugin/public';
import type { VisParams } from '../types';
import { areaVisTypeDefinition } from './area';
import { lineVisTypeDefinition } from './line';
import { histogramVisTypeDefinition } from './histogram';
import { horizontalBarVisTypeDefinition } from './horizontal_bar';
import { convertToLens } from '../convert_to_lens';
export const visTypesDefinitions = [
areaVisTypeDefinition,
lineVisTypeDefinition,
histogramVisTypeDefinition,
horizontalBarVisTypeDefinition,
];
].map<VisTypeDefinition<VisParams>>((defenition) => {
return {
...defenition,
navigateToLens: async (vis, timefilter) => (vis ? convertToLens(vis, timefilter) : null),
getExpressionVariables: async (vis, timeFilter) => {
return {
canNavigateToLens: Boolean(vis?.params ? await convertToLens(vis, timeFilter) : null),
};
},
};
});

View file

@ -13,7 +13,7 @@ import { convertToSchemaConfig } from '../../../vis_schemas';
export const convertToSiblingPipelineColumns = (
columnConverterArgs: ExtendedColumnConverterArgs<SiblingPipelineMetric>
): AggBasedColumn | null => {
const { aggParams, label } = columnConverterArgs.agg;
const { aggParams, label, aggId } = columnConverterArgs.agg;
if (!aggParams) {
return null;
}
@ -23,7 +23,7 @@ export const convertToSiblingPipelineColumns = (
}
const customMetricColumn = convertMetricToColumns(
{ ...convertToSchemaConfig(aggParams.customMetric), label },
{ ...convertToSchemaConfig(aggParams.customMetric), label, aggId },
columnConverterArgs.dataView,
columnConverterArgs.aggs
);

View file

@ -58,7 +58,7 @@ export type SiblingPipelineMetric =
| METRIC_TYPES.MIN_BUCKET
| METRIC_TYPES.MAX_BUCKET;
export type BucketColumn = DateHistogramColumn | TermsColumn | FiltersColumn;
export type BucketColumn = DateHistogramColumn | TermsColumn | FiltersColumn | RangeColumn;
export interface CommonColumnConverterArgs<
Agg extends SupportedAggregation = SupportedAggregation
> {

View file

@ -365,7 +365,7 @@ describe('getCustomBucketsFromSiblingAggs', () => {
},
params: {},
aggType: METRIC_TYPES.AVG_BUCKET,
aggId: 'some-agg-id',
aggId: 'some-agg-id-1',
aggParams: {
customBucket: bucketWithSerialize1,
},
@ -381,7 +381,7 @@ describe('getCustomBucketsFromSiblingAggs', () => {
},
params: {},
aggType: METRIC_TYPES.AVG_BUCKET,
aggId: 'some-agg-id',
aggId: 'some-agg-id-2',
aggParams: {
customBucket: bucketWithSerialize2,
},
@ -399,7 +399,7 @@ describe('getCustomBucketsFromSiblingAggs', () => {
},
params: {},
aggType: METRIC_TYPES.AVG_BUCKET,
aggId: 'some-agg-id',
aggId: 'some-agg-id-3',
aggParams: {
customBucket: bucketWithSerialize3,
},
@ -407,8 +407,8 @@ describe('getCustomBucketsFromSiblingAggs', () => {
test("should filter out duplicated custom buckets, ignoring id's", () => {
expect(getCustomBucketsFromSiblingAggs([metric1, metric2, metric3])).toEqual([
bucketWithSerialize1,
bucketWithSerialize2,
{ customBucket: bucketWithSerialize1, metricIds: ['some-agg-id-1', 'some-agg-id-3'] },
{ customBucket: bucketWithSerialize2, metricIds: ['some-agg-id-2'] },
]);
});
});

View file

@ -151,19 +151,19 @@ export const isStdDevAgg = (metric: SchemaConfig): metric is SchemaConfig<METRIC
};
export const getCustomBucketsFromSiblingAggs = (metrics: SchemaConfig[]) => {
return metrics.reduce<IAggConfig[]>((acc, metric) => {
if (
isSiblingPipeline(metric) &&
metric.aggParams?.customBucket &&
acc.every(
(bucket) =>
!isEqual(
omit(metric.aggParams?.customBucket?.serialize(), ['id']),
omit(bucket.serialize(), ['id'])
)
)
) {
acc.push(metric.aggParams.customBucket);
return metrics.reduce<Array<{ customBucket: IAggConfig; metricIds: string[] }>>((acc, metric) => {
if (isSiblingPipeline(metric) && metric.aggParams?.customBucket && metric.aggId) {
const customBucket = acc.find((bucket) =>
isEqual(
omit(metric.aggParams?.customBucket?.serialize(), ['id']),
omit(bucket.customBucket.serialize(), ['id'])
)
);
if (customBucket) {
customBucket.metricIds.push(metric.aggId);
} else {
acc.push({ customBucket: metric.aggParams.customBucket, metricIds: [metric.aggId] });
}
}
return acc;

View file

@ -162,6 +162,7 @@ export interface XYConfiguration {
fillOpacity?: number;
hideEndzones?: boolean;
valuesInLegend?: boolean;
showCurrentTimeMarker?: boolean;
}
export interface SortingState {

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { DataViewField } from '@kbn/data-views-plugin/common';
import { SupportedMetric } from './lib/convert/supported_metrics';
import { Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import type { SupportedMetric } from './lib/convert/supported_metrics';
import type { Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types';
export const isAnnotationsLayer = (
layer: Pick<XYLayerConfig, 'layerType'>

View file

@ -21,6 +21,7 @@ import { getColumnsFromVis } from './schemas';
const mockConvertMetricToColumns = jest.fn();
const mockConvertBucketToColumns = jest.fn();
const mockGetCutomBucketsFromSiblingAggs = jest.fn();
const mockGetCustomBucketColumns = jest.fn();
const mockGetVisSchemas = jest.fn();
const mockGetBucketCollapseFn = jest.fn();
@ -55,6 +56,7 @@ jest.mock('./utils', () => ({
getMetricsWithoutDuplicates: jest.fn(() => mockGetMetricsWithoutDuplicates()),
isValidVis: jest.fn(() => mockIsValidVis()),
sortColumns: jest.fn(() => mockSortColumns()),
getCustomBucketColumns: jest.fn(() => mockGetCustomBucketColumns()),
}));
describe('getColumnsFromVis', () => {
@ -73,6 +75,7 @@ describe('getColumnsFromVis', () => {
jest.clearAllMocks();
mockGetVisSchemas.mockReturnValue({});
mockIsValidVis.mockReturnValue(true);
mockGetCustomBucketColumns.mockReturnValue({ customBucketColumns: [], customBucketsMap: {} });
});
test('should return null if vis is not valid', () => {
@ -107,7 +110,10 @@ describe('getColumnsFromVis', () => {
test('should return null if one sibling agg was provided and it is not supported', () => {
const buckets: AggConfig[] = [aggConfig];
mockGetCutomBucketsFromSiblingAggs.mockReturnValue(buckets);
mockConvertBucketToColumns.mockReturnValue(null);
mockGetCustomBucketColumns.mockReturnValue({
customBucketColumns: [null],
customBucketsMap: {},
});
mockGetMetricsWithoutDuplicates.mockReturnValue([{}]);
const result = getColumnsFromVis(vis, dataServiceMock.query.timefilter.timefilter, dataView, {
@ -120,7 +126,7 @@ describe('getColumnsFromVis', () => {
expect(mockIsValidVis).toBeCalledTimes(1);
expect(mockGetCutomBucketsFromSiblingAggs).toBeCalledTimes(1);
expect(mockGetMetricsWithoutDuplicates).toBeCalledTimes(1);
expect(mockConvertBucketToColumns).toBeCalledTimes(1);
expect(mockGetCustomBucketColumns).toBeCalledTimes(1);
expect(mockGetBucketColumns).toBeCalledTimes(0);
});
@ -190,7 +196,7 @@ describe('getColumnsFromVis', () => {
expect(mockSortColumns).toBeCalledTimes(0);
});
test('should return columns', () => {
test('should return one layer with columns', () => {
const buckets: AggConfig[] = [aggConfig];
const bucketColumns = [
{
@ -238,13 +244,18 @@ describe('getColumnsFromVis', () => {
buckets: [],
});
expect(result).toEqual({
bucketCollapseFn,
buckets: [bucketId],
columns: [...metrics, ...buckets],
columnsWithoutReferenced,
metrics: [metricId],
});
expect(result).toEqual([
{
bucketCollapseFn,
buckets: {
all: [bucketId],
customBuckets: {},
},
columns: [...metrics, ...buckets],
columnsWithoutReferenced,
metrics: [metricId],
},
]);
expect(mockGetVisSchemas).toBeCalledTimes(1);
expect(mockIsValidVis).toBeCalledTimes(1);
expect(mockGetCutomBucketsFromSiblingAggs).toBeCalledTimes(1);
@ -254,4 +265,84 @@ describe('getColumnsFromVis', () => {
expect(mockSortColumns).toBeCalledTimes(1);
expect(mockGetColumnsWithoutReferenced).toBeCalledTimes(1);
});
test('should return several layer with columns if series is provided', () => {
const buckets: AggConfig[] = [aggConfig];
const bucketColumns = [
{
sourceField: 'some-field',
columnId: 'col3',
operationType: 'date_histogram',
isBucketed: false,
isSplit: false,
dataType: 'string',
params: { interval: '1h' },
meta: { aggId: 'agg-id-1' },
},
];
const mectricAggs = [{ aggId: 'col-id-3' }, { aggId: 'col-id-4' }];
const metrics = [
{
sourceField: 'some-field',
columnId: 'col2',
operationType: 'max',
isBucketed: false,
isSplit: false,
dataType: 'string',
params: {},
meta: { aggId: 'col-id-3' },
},
{
sourceField: 'some-field',
columnId: 'col3',
operationType: 'max',
isBucketed: false,
isSplit: false,
dataType: 'string',
params: {},
meta: { aggId: 'col-id-4' },
},
];
const columnsWithoutReferenced = ['col2'];
const metricId = 'metric1';
const bucketId = 'bucket1';
const bucketCollapseFn = 'max';
mockGetCutomBucketsFromSiblingAggs.mockReturnValue([]);
mockGetMetricsWithoutDuplicates.mockReturnValue(mectricAggs);
mockConvertMetricToColumns.mockReturnValue(metrics);
mockConvertBucketToColumns.mockReturnValue(bucketColumns);
mockGetBucketColumns.mockReturnValue(bucketColumns);
mockGetColumnsWithoutReferenced.mockReturnValue(columnsWithoutReferenced);
mockSortColumns.mockReturnValue([...metrics, ...buckets]);
mockGetColumnIds.mockReturnValueOnce([metricId]);
mockGetColumnIds.mockReturnValueOnce([bucketId]);
mockGetColumnIds.mockReturnValueOnce([metricId]);
mockGetColumnIds.mockReturnValueOnce([bucketId]);
mockGetBucketCollapseFn.mockReturnValueOnce(bucketCollapseFn);
mockGetBucketCollapseFn.mockReturnValueOnce(bucketCollapseFn);
const result = getColumnsFromVis(
vis,
dataServiceMock.query.timefilter.timefilter,
dataView,
{
splits: [],
buckets: [],
},
undefined,
[{ metrics: ['col-id-3'] }, { metrics: ['col-id-4'] }]
);
expect(result?.length).toEqual(2);
expect(mockGetVisSchemas).toBeCalledTimes(1);
expect(mockIsValidVis).toBeCalledTimes(1);
expect(mockGetCutomBucketsFromSiblingAggs).toBeCalledTimes(1);
expect(mockGetMetricsWithoutDuplicates).toBeCalledTimes(1);
expect(mockConvertMetricToColumns).toBeCalledTimes(2);
expect(mockGetBucketColumns).toBeCalledTimes(4);
expect(mockSortColumns).toBeCalledTimes(2);
expect(mockGetColumnsWithoutReferenced).toBeCalledTimes(2);
});
});

View file

@ -7,16 +7,17 @@
*/
import type { DataView } from '@kbn/data-views-plugin/common';
import { METRIC_TYPES, TimefilterContract } from '@kbn/data-plugin/public';
import { IAggConfig, METRIC_TYPES, TimefilterContract } from '@kbn/data-plugin/public';
import { AggBasedColumn, PercentageModeConfig, SchemaConfig } from '../../common';
import { convertMetricToColumns } from '../../common/convert_to_lens/lib/metrics';
import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets';
import { getCustomBucketsFromSiblingAggs } from '../../common/convert_to_lens/lib/utils';
import { BucketColumn } from '../../common/convert_to_lens/lib';
import type { Vis } from '../types';
import { getVisSchemas, Schemas } from '../vis_schemas';
import {
getBucketCollapseFn,
getBucketColumns,
getCustomBucketColumns,
getColumnIds,
getColumnsWithoutReferenced,
getMetricsWithoutDuplicates,
@ -31,65 +32,43 @@ const areVisSchemasValid = (visSchemas: Schemas, unsupported: Array<keyof Schema
return !usedUnsupportedSchemas.length;
};
export const getColumnsFromVis = <T>(
vis: Vis<T>,
timefilter: TimefilterContract,
const createLayer = (
visSchemas: Schemas,
allMetrics: Array<SchemaConfig<METRIC_TYPES>>,
metricsForLayer: Array<SchemaConfig<METRIC_TYPES>>,
customBucketsWithMetricIds: Array<{
customBucket: IAggConfig;
metricIds: string[];
}>,
dataView: DataView,
{
splits = [],
buckets = [],
unsupported = [],
}: {
splits?: Array<keyof Schemas>;
buckets?: Array<keyof Schemas>;
unsupported?: Array<keyof Schemas>;
} = {},
config?: {
dropEmptyRowsInDateHistogram?: boolean;
} & (PercentageModeConfig | void)
percentageModeConfig: PercentageModeConfig,
dropEmptyRowsInDateHistogram?: boolean
) => {
const { dropEmptyRowsInDateHistogram, ...percentageModeConfig } = config ?? {
isPercentageMode: false,
};
const visSchemas = getVisSchemas(vis, {
timefilter,
timeRange: timefilter.getAbsoluteTime(),
});
if (!isValidVis(visSchemas) || !areVisSchemasValid(visSchemas, unsupported)) {
return null;
}
const customBuckets = getCustomBucketsFromSiblingAggs(visSchemas.metric);
// doesn't support sibbling pipeline aggs with different bucket aggs
if (customBuckets.length > 1) {
return null;
}
const metricsWithoutDuplicates = getMetricsWithoutDuplicates(visSchemas.metric);
const aggs = metricsWithoutDuplicates as Array<SchemaConfig<METRIC_TYPES>>;
const metricColumns = aggs.flatMap((m) =>
convertMetricToColumns(m, dataView, aggs, percentageModeConfig)
const metricColumns = metricsForLayer.flatMap((m) =>
convertMetricToColumns(m, dataView, allMetrics, percentageModeConfig)
);
if (metricColumns.includes(null)) {
return null;
}
const metrics = metricColumns as AggBasedColumn[];
const customBucketColumns = [];
const metricColumnsWithoutNull = metricColumns as AggBasedColumn[];
if (customBuckets.length) {
const customBucketColumn = convertBucketToColumns(
{ agg: customBuckets[0], dataView, metricColumns: metrics, aggs },
false,
dropEmptyRowsInDateHistogram
);
if (!customBucketColumn) {
return null;
}
customBucketColumns.push(customBucketColumn);
const { customBucketColumns, customBucketsMap } = getCustomBucketColumns(
customBucketsWithMetricIds,
metricColumnsWithoutNull,
dataView,
allMetrics,
dropEmptyRowsInDateHistogram
);
if (customBucketColumns.includes(null)) {
return null;
}
const bucketColumns = getBucketColumns(
@ -117,19 +96,121 @@ export const getColumnsFromVis = <T>(
}
const columns = sortColumns(
[...metrics, ...bucketColumns, ...splitBucketColumns, ...customBucketColumns],
[
...metricColumnsWithoutNull,
...bucketColumns,
...splitBucketColumns,
...(customBucketColumns as BucketColumn[]),
],
visSchemas,
[...buckets, ...splits],
metricsWithoutDuplicates
metricsForLayer
);
const columnsWithoutReferenced = getColumnsWithoutReferenced(columns);
return {
metrics: getColumnIds(columnsWithoutReferenced.filter((с) => !с.isBucketed)),
buckets: getColumnIds(columnsWithoutReferenced.filter((c) => c.isBucketed)),
bucketCollapseFn: getBucketCollapseFn(visSchemas.metric, customBucketColumns),
buckets: {
all: getColumnIds(columnsWithoutReferenced.filter((c) => c.isBucketed)),
customBuckets: customBucketsMap,
},
bucketCollapseFn: getBucketCollapseFn(
visSchemas.metric,
customBucketColumns as BucketColumn[],
customBucketsMap,
metricColumnsWithoutNull
),
columnsWithoutReferenced,
columns,
};
};
export const getColumnsFromVis = <T>(
vis: Vis<T>,
timefilter: TimefilterContract,
dataView: DataView,
{
splits = [],
buckets = [],
unsupported = [],
}: {
splits?: Array<keyof Schemas>;
buckets?: Array<keyof Schemas>;
unsupported?: Array<keyof Schemas>;
} = {},
config?: {
dropEmptyRowsInDateHistogram?: boolean;
supportMixedSiblingPipelineAggs?: boolean;
} & (PercentageModeConfig | void),
series?: Array<{ metrics: string[] }>
) => {
const { dropEmptyRowsInDateHistogram, supportMixedSiblingPipelineAggs, ...percentageModeConfig } =
config ?? {
isPercentageMode: false,
};
const visSchemas = getVisSchemas(vis, {
timefilter,
timeRange: timefilter.getAbsoluteTime(),
});
if (
!isValidVis(visSchemas, supportMixedSiblingPipelineAggs) ||
!areVisSchemasValid(visSchemas, unsupported)
) {
return null;
}
const customBucketsWithMetricIds = getCustomBucketsFromSiblingAggs(visSchemas.metric);
// doesn't support sibbling pipeline aggs with different bucket aggs
if (!supportMixedSiblingPipelineAggs && customBucketsWithMetricIds.length > 1) {
return null;
}
const metricsWithoutDuplicates = getMetricsWithoutDuplicates(visSchemas.metric);
const aggs = metricsWithoutDuplicates as Array<SchemaConfig<METRIC_TYPES>>;
const layers = [];
if (series && series.length) {
for (const { metrics: metricAggIds } of series) {
const metrics = aggs.filter(
(agg) => agg.aggId && metricAggIds.includes(agg.aggId.split('.')[0])
);
const customBucketsForLayer = customBucketsWithMetricIds.filter((c) =>
c.metricIds.some((m) => metricAggIds.includes(m))
);
const layer = createLayer(
visSchemas,
aggs,
metrics,
customBucketsForLayer,
dataView,
{ splits, buckets },
percentageModeConfig,
dropEmptyRowsInDateHistogram
);
if (!layer) {
return null;
}
layers.push(layer);
}
} else {
const layer = createLayer(
visSchemas,
aggs,
aggs,
customBucketsWithMetricIds,
dataView,
{ splits, buckets },
percentageModeConfig,
dropEmptyRowsInDateHistogram
);
if (!layer) {
return null;
}
layers.push(layer);
}
return layers;
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { BUCKET_TYPES, METRIC_TYPES } from '@kbn/data-plugin/common';
import { BUCKET_TYPES, IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import {
AggBasedColumn,
@ -27,6 +27,7 @@ import {
getBucketColumns,
getColumnIds,
getColumnsWithoutReferenced,
getCustomBucketColumns,
getMetricsWithoutDuplicates,
isReferenced,
isValidVis,
@ -104,6 +105,7 @@ describe('getColumnsWithoutReferenced', () => {
describe('getBucketCollapseFn', () => {
const metric1: SchemaConfig<METRIC_TYPES.AVG> = {
accessor: 0,
aggId: '1',
label: '',
format: {
id: undefined,
@ -115,21 +117,25 @@ describe('getBucketCollapseFn', () => {
const metric2: SchemaConfig<METRIC_TYPES.AVG_BUCKET> = {
...metric1,
aggId: '2',
aggType: METRIC_TYPES.AVG_BUCKET,
};
const metric3: SchemaConfig<METRIC_TYPES.MAX_BUCKET> = {
...metric1,
aggId: '3',
aggType: METRIC_TYPES.MAX_BUCKET,
};
const metric4: SchemaConfig<METRIC_TYPES.MIN_BUCKET> = {
...metric1,
aggId: '4',
aggType: METRIC_TYPES.MIN_BUCKET,
};
const metric5: SchemaConfig<METRIC_TYPES.SUM_BUCKET> = {
...metric1,
aggId: '5',
aggType: METRIC_TYPES.SUM_BUCKET,
};
@ -151,18 +157,54 @@ describe('getBucketCollapseFn', () => {
test.each<
[
string,
[Array<SchemaConfig<SupportedAggregation>>, AggBasedColumn[]],
Record<string, string | undefined>
[
Array<SchemaConfig<SupportedAggregation>>,
AggBasedColumn[],
Record<string, string>,
AggBasedColumn[]
],
Record<string, string[]>
]
>([
['avg', [[metric1, metric2], [customBucketColum]], { [customBucketColum.columnId]: 'avg' }],
['max', [[metric1, metric3], [customBucketColum]], { [customBucketColum.columnId]: 'max' }],
['min', [[metric1, metric4], [customBucketColum]], { [customBucketColum.columnId]: 'min' }],
['sum', [[metric1, metric5], [customBucketColum]], { [customBucketColum.columnId]: 'sum' }],
[
'undefined if no sibling pipeline agg is provided',
[[metric1], [customBucketColum]],
{ [customBucketColum.columnId]: undefined },
'avg',
[
[metric1, metric2],
[customBucketColum],
{ test: 'bucket-1' },
[{ columnId: 'test', meta: { aggId: metric2.aggId } } as AggBasedColumn],
],
{ sum: [], min: [], max: [], avg: [customBucketColum.columnId] },
],
[
'max',
[
[metric1, metric3],
[customBucketColum],
{ test: 'bucket-1' },
[{ columnId: 'test', meta: { aggId: metric3.aggId } } as AggBasedColumn],
],
{ sum: [], min: [], max: [customBucketColum.columnId], avg: [] },
],
[
'min',
[
[metric1, metric4],
[customBucketColum],
{ test: 'bucket-1' },
[{ columnId: 'test', meta: { aggId: metric4.aggId } } as AggBasedColumn],
],
{ sum: [], min: [customBucketColum.columnId], max: [], avg: [] },
],
[
'sum',
[
[metric1, metric5],
[customBucketColum],
{ test: 'bucket-1' },
[{ columnId: 'test', meta: { aggId: metric5.aggId } } as AggBasedColumn],
],
{ sum: [customBucketColum.columnId], min: [], max: [], avg: [] },
],
])('should return%s', (_, input, expected) => {
expect(getBucketCollapseFn(...input)).toEqual(expected);
@ -607,4 +649,77 @@ describe('getColumnIds', () => {
colId4,
]);
});
describe('getCustomBucketColumns', () => {
const dataView = stubLogstashDataView;
const baseMetric = {
accessor: 0,
label: '',
format: {
id: undefined,
params: undefined,
},
params: {},
};
const metric1: SchemaConfig<METRIC_TYPES.COUNT> = {
...baseMetric,
accessor: 2,
aggType: METRIC_TYPES.COUNT,
aggId: '3',
};
const metric2: SchemaConfig<METRIC_TYPES.MAX> = {
...baseMetric,
accessor: 3,
aggType: METRIC_TYPES.MAX,
aggId: '4',
};
const customBucketsWithMetricIds = [
{
customBucket: {} as IAggConfig,
metricIds: ['3', '4'],
},
{
customBucket: {} as IAggConfig,
metricIds: ['5'],
},
];
test('return custom buckets columns and map', () => {
mockConvertBucketToColumns.mockReturnValueOnce({
columnId: 'col-1',
operationType: 'date_histogram',
});
mockConvertBucketToColumns.mockReturnValueOnce({
columnId: 'col-2',
operationType: 'terms',
});
expect(
getCustomBucketColumns(
customBucketsWithMetricIds,
[
{ columnId: 'col-3', meta: { aggId: '3' } },
{ columnId: 'col-4', meta: { aggId: '4' } },
{ columnId: 'col-5', meta: { aggId: '5' } },
] as AggBasedColumn[],
dataView,
[metric1, metric2]
)
).toEqual({
customBucketColumns: [
{
columnId: 'col-1',
operationType: 'date_histogram',
},
{
columnId: 'col-2',
operationType: 'terms',
},
],
customBucketsMap: {
'col-3': 'col-1',
'col-4': 'col-1',
'col-5': 'col-2',
},
});
});
});
});

View file

@ -7,10 +7,11 @@
*/
import type { DataView } from '@kbn/data-views-plugin/common';
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/public';
import { AggBasedColumn, SchemaConfig, SupportedAggregation } from '../../common';
import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets';
import { isSiblingPipeline } from '../../common/convert_to_lens/lib/utils';
import { BucketColumn } from '../../common/convert_to_lens/lib';
import { Schemas } from '../vis_schemas';
export const isReferenced = (columnId: string, references: string[]) =>
@ -25,14 +26,31 @@ export const getColumnsWithoutReferenced = (columns: AggBasedColumn[]) => {
export const getBucketCollapseFn = (
metrics: Array<SchemaConfig<SupportedAggregation>>,
customBucketColumns: AggBasedColumn[]
customBucketColumns: AggBasedColumn[],
customBucketsMap: Record<string, string>,
metricColumns: AggBasedColumn[]
) => {
const collapseFn = metrics.find((m) => isSiblingPipeline(m))?.aggType.split('_')[0];
return customBucketColumns.length
? {
[customBucketColumns[0].columnId]: collapseFn,
const collapseFnMap: Record<string, string[]> = {
min: [],
max: [],
sum: [],
avg: [],
};
customBucketColumns.forEach((bucket) => {
const metricColumnsIds = Object.keys(customBucketsMap).filter(
(key) => customBucketsMap[key] === bucket.columnId
);
metricColumnsIds.forEach((metricColumnsId) => {
const metricColumn = metricColumns.find((c) => c.columnId === metricColumnsId)!;
const collapseFn = metrics
.find((m) => m.aggId === metricColumn.meta.aggId)
?.aggType.split('_')[0];
if (collapseFn) {
collapseFnMap[collapseFn].push(bucket.columnId);
}
: {};
});
});
return collapseFnMap;
};
export const getBucketColumns = (
@ -67,7 +85,7 @@ export const getBucketColumns = (
return columns;
};
export const isValidVis = (visSchemas: Schemas) => {
export const isValidVis = (visSchemas: Schemas, supportMixedSiblingPipelineAggs?: boolean) => {
const { metric } = visSchemas;
const siblingPipelineAggs = metric.filter((m) => isSiblingPipeline(m));
@ -76,7 +94,10 @@ export const isValidVis = (visSchemas: Schemas) => {
}
// doesn't support mixed sibling pipeline aggregations
if (siblingPipelineAggs.some((agg) => agg.aggType !== siblingPipelineAggs[0].aggType)) {
if (
siblingPipelineAggs.some((agg) => agg.aggType !== siblingPipelineAggs[0].aggType) &&
!supportMixedSiblingPipelineAggs
) {
return false;
}
@ -103,7 +124,8 @@ export const sortColumns = (
...acc,
...(key === 'metric' ? metricsWithoutDuplicates : visSchemas[key])?.reduce(
(newAcc, schema) => {
newAcc[schema.aggId] = schema.accessor;
// metrics should always have sort more than buckets
newAcc[schema.aggId] = key === 'metric' ? schema.accessor : 1000 + schema.accessor;
return newAcc;
},
{}
@ -121,3 +143,36 @@ export const sortColumns = (
};
export const getColumnIds = (columns: AggBasedColumn[]) => columns.map(({ columnId }) => columnId);
export const getCustomBucketColumns = (
customBucketsWithMetricIds: Array<{
customBucket: IAggConfig;
metricIds: string[];
}>,
metricColumns: AggBasedColumn[],
dataView: DataView,
aggs: Array<SchemaConfig<METRIC_TYPES>>,
dropEmptyRowsInDateHistogram?: boolean
) => {
const customBucketColumns: Array<BucketColumn | null> = [];
const customBucketsMap: Record<string, string> = {};
customBucketsWithMetricIds.forEach((customBucketWithMetricIds) => {
const customBucketColumn = convertBucketToColumns(
{ agg: customBucketWithMetricIds.customBucket, dataView, metricColumns, aggs },
true,
dropEmptyRowsInDateHistogram
);
customBucketColumns.push(customBucketColumn);
if (customBucketColumn) {
customBucketWithMetricIds.metricIds.forEach((metricAggId) => {
const metricColumnId = metricColumns.find(
(metricColumn) => metricColumn?.meta.aggId === metricAggId
)?.columnId;
if (metricColumnId) {
customBucketsMap[metricColumnId] = customBucketColumn.columnId;
}
});
}
});
return { customBucketColumns, customBucketsMap };
};

View file

@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('Agg based Vis to Lens', function () {
loadTestFile(require.resolve('./pie'));
loadTestFile(require.resolve('./metric'));
loadTestFile(require.resolve('./xy'));
loadTestFile(require.resolve('./gauge'));
loadTestFile(require.resolve('./goal'));
});

View file

@ -0,0 +1,73 @@
/*
* 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 ({ getPageObjects, getService }: FtrProviderContext) {
const { visualize, visEditor, lens, timePicker, header, visChart } = getPageObjects([
'visualize',
'lens',
'visEditor',
'timePicker',
'header',
'visChart',
]);
const testSubjects = getService('testSubjects');
const retry = getService('retry');
describe('XY', function describeIndexTests() {
const isNewChartsLibraryEnabled = true;
before(async () => {
await visualize.initTests(isNewChartsLibraryEnabled);
});
beforeEach(async () => {
await visualize.navigateToNewAggBasedVisualization();
await visualize.clickLineChart();
await visualize.clickNewSearch();
await timePicker.setDefaultAbsoluteRange();
});
it('should show the "Edit Visualization in Lens" menu item', async () => {
const button = await testSubjects.exists('visualizeEditInLensButton');
expect(button).to.eql(true);
});
it('should hide the "Edit Visualization in Lens" menu item if dot size aggregation is defined', async () => {
await visEditor.clickBucket('Dot size', 'metrics');
await visEditor.selectAggregation('Max', 'metrics');
await visEditor.selectField('memory', 'metrics');
await visEditor.clickGo(isNewChartsLibraryEnabled);
const button = await testSubjects.exists('visualizeEditInLensButton');
expect(button).to.eql(false);
});
it('should convert to Lens', async () => {
await visEditor.clickBucket('Split series');
await visEditor.selectAggregation('Terms');
await visEditor.selectField('machine.os.raw');
await header.waitUntilLoadingHasFinished();
await visEditor.clickGo(isNewChartsLibraryEnabled);
const expectedData = await visChart.getLegendEntriesXYCharts('xyVisChart');
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
const data = await lens.getCurrentChartDebugState('xyVisChart');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(dimensions).to.have.length(2);
expect(await dimensions[0].getVisibleText()).to.be('Count');
expect(await dimensions[1].getVisibleText()).to.be('machine.os.raw: Descending');
});
expect(data?.legend?.items.map((item) => item.name)).to.eql(expectedData);
});
});
}