mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
7ba6093d49
commit
3cb5fedca7
51 changed files with 1737 additions and 326 deletions
|
@ -140,6 +140,6 @@ pageLoadAssetSize:
|
|||
visTypeTimeseries: 55203
|
||||
visTypeVega: 153573
|
||||
visTypeVislib: 242838
|
||||
visTypeXy: 30000
|
||||
visTypeXy: 31800
|
||||
visualizations: 90000
|
||||
watcher: 43598
|
||||
|
|
|
@ -28,6 +28,7 @@ describe('layeredXyVis', () => {
|
|||
args: { ...rest, layers: [sampleExtendedLayer] },
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
canNavigateToLens: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -38,6 +38,7 @@ describe('xyVis', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
canNavigateToLens: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
},
|
||||
|
@ -346,6 +347,7 @@ describe('xyVis', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
canNavigateToLens: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface XYChartProps {
|
|||
args: XYProps;
|
||||
syncTooltips: boolean;
|
||||
syncColors: boolean;
|
||||
canNavigateToLens?: boolean;
|
||||
}
|
||||
|
||||
export interface XYRender {
|
||||
|
|
|
@ -110,7 +110,7 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export type XYChartRenderProps = XYChartProps & {
|
||||
export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
|
||||
chartsThemeService: ChartsPluginSetup['theme'];
|
||||
chartsActiveCursorService: ChartsPluginStart['activeCursor'];
|
||||
data: DataPublicPluginStart;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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' }],
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
),
|
||||
};
|
||||
};
|
202
src/plugins/vis_types/xy/public/convert_to_lens/index.test.ts
Normal file
202
src/plugins/vis_types/xy/public/convert_to_lens/index.test.ts
Normal 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' }]);
|
||||
});
|
||||
});
|
214
src/plugins/vis_types/xy/public/convert_to_lens/index.ts
Normal file
214
src/plugins/vis_types/xy/public/convert_to_lens/index.ts
Normal 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],
|
||||
};
|
||||
};
|
17
src/plugins/vis_types/xy/public/convert_to_lens/types.ts
Normal file
17
src/plugins/vis_types/xy/public/convert_to_lens/types.ts
Normal 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>;
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
62
src/plugins/vis_types/xy/public/utils/common.ts
Normal file
62
src/plugins/vis_types/xy/public/utils/common.ts
Normal 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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
> {
|
||||
|
|
|
@ -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'] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -162,6 +162,7 @@ export interface XYConfiguration {
|
|||
fillOpacity?: number;
|
||||
hideEndzones?: boolean;
|
||||
valuesInLegend?: boolean;
|
||||
showCurrentTimeMarker?: boolean;
|
||||
}
|
||||
|
||||
export interface SortingState {
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue