[Lens] New lens config builder api (#169750)

## Summary

resolves https://github.com/elastic/kibana/issues/163293

Exposes config builder API to build lens configurations via much simpler
API which hides the complexity of lens and allows developers to easily
configure the chart.

sample usage:
```

const builder = new LensConfigBuilder(formulaPublicAPI, dataViewsPublicAPI);
const embeddableInput = await builder.build(
    {
      chartType: 'heatmap',
      title: 'test',
      dataset: {
        esql: 'from kibana_sample_data_ecommerce | count=count() by order_date, product.category.keyword',
      },
      layers: [
        {
          label: 'test',
          breakdown: 'product.category.keyword',
          xAxis: 'order_date',
          value: 'count',
        },
      ],
    }, {
      embeddable: true,
    }
  );
```

pr with sample app: https://github.com/elastic/kibana/pull/171282

---------

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Peter Pisljar 2023-12-06 11:10:58 +01:00 committed by GitHub
parent 1022ccdf78
commit 11451b48b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 3787 additions and 3 deletions

2
.github/CODEOWNERS vendored
View file

@ -481,7 +481,7 @@ src/plugins/kibana_usage_collection @elastic/kibana-core
src/plugins/kibana_utils @elastic/appex-sharedux
x-pack/plugins/kubernetes_security @elastic/kibana-cloud-security-posture
packages/kbn-language-documentation-popover @elastic/kibana-visualizations
packages/kbn-lens-embeddable-utils @elastic/obs-ux-infra_services-team
packages/kbn-lens-embeddable-utils @elastic/obs-ux-infra_services-team @elastic/kibana-visualizations
x-pack/plugins/lens @elastic/kibana-visualizations
x-pack/plugins/license_api_guard @elastic/platform-deployment-management
x-pack/plugins/license_management @elastic/platform-deployment-management

View file

@ -0,0 +1,127 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildGauge } from './gauge';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates gauge chart config', async () => {
const result = await buildGauge(
{
chartType: 'gauge',
title: 'test',
dataset: {
esql: 'from test | count=count()',
},
value: 'count',
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count()",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"labelMajorMode": "auto",
"layerId": "layer_0",
"layerType": "data",
"metricAccessor": "metric_formula_accessor",
"shape": "horizontalBullet",
"ticksPosition": "auto",
},
},
"title": "test",
"visualizationType": "lnsGauge",
}
`);
});

View file

@ -0,0 +1,168 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
GaugeVisualizationState,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { BuildDependencies, DEFAULT_LAYER_ID, LensAttributes, LensGaugeConfig } from '../types';
import {
addLayerFormulaColumns,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import { getFormulaColumn, getValueColumn } from '../columns';
const ACCESSOR = 'metric_formula_accessor';
function getAccessorName(type: 'goal' | 'max' | 'min' | 'secondary') {
return `${ACCESSOR}_${type}`;
}
function buildVisualizationState(config: LensGaugeConfig): GaugeVisualizationState {
const layer = config;
return {
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
ticksPosition: 'auto',
shape: layer.shape || 'horizontalBullet',
labelMajorMode: 'auto',
metricAccessor: ACCESSOR,
...(layer.queryGoalValue
? {
goalAccessor: getAccessorName('goal'),
}
: {}),
...(layer.queryMaxValue
? {
maxAccessor: getAccessorName('max'),
showBar: true,
}
: {}),
...(layer.queryMinValue
? {
minAccessor: getAccessorName('min'),
}
: {}),
};
}
function buildFormulaLayer(
layer: LensGaugeConfig,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const layers = {
[DEFAULT_LAYER_ID]: {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
},
};
const defaultLayer = layers[DEFAULT_LAYER_ID];
if (layer.queryGoalValue) {
const columnName = getAccessorName('goal');
const formulaColumn = getFormulaColumn(
columnName,
{
value: layer.queryGoalValue,
},
dataView,
formulaAPI
);
addLayerFormulaColumns(defaultLayer, formulaColumn);
}
if (layer.queryMinValue) {
const columnName = getAccessorName('min');
const formulaColumn = getFormulaColumn(
columnName,
{
value: layer.queryMinValue,
},
dataView,
formulaAPI
);
addLayerFormulaColumns(defaultLayer, formulaColumn);
}
if (layer.queryMaxValue) {
const columnName = getAccessorName('max');
const formulaColumn = getFormulaColumn(
columnName,
{
value: layer.queryMaxValue,
},
dataView,
formulaAPI
);
addLayerFormulaColumns(defaultLayer, formulaColumn);
}
return defaultLayer;
}
function getValueColumns(layer: LensGaugeConfig) {
return [
getValueColumn(ACCESSOR, layer.value),
...(layer.queryMaxValue ? [getValueColumn(getAccessorName('max'), layer.queryMaxValue)] : []),
...(layer.queryMinValue
? [getValueColumn(getAccessorName('secondary'), layer.queryMinValue)]
: []),
...(layer.queryGoalValue
? [getValueColumn(getAccessorName('secondary'), layer.queryGoalValue)]
: []),
];
}
export async function buildGauge(
config: LensGaugeConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: unknown, i: number, dataView: DataView) =>
buildFormulaLayer(cfg as LensGaugeConfig, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsGauge',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,158 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildHeatmap } from './heatmap';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildHeatmap(
{
chartType: 'heatmap',
title: 'test',
dataset: {
esql: 'from test | count=count() by @timestamp, category',
},
breakdown: 'category',
xAxis: '@timestamp',
value: 'count',
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor_y",
"fieldName": "category",
},
Object {
"columnId": "metric_formula_accessor_x",
"fieldName": "@timestamp",
},
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor_y",
"fieldName": "category",
},
Object {
"columnId": "metric_formula_accessor_x",
"fieldName": "@timestamp",
},
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count() by @timestamp, category",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"gridConfig": Object {
"isCellLabelVisible": false,
"isXAxisLabelVisible": false,
"isXAxisTitleVisible": false,
"isYAxisLabelVisible": false,
"isYAxisTitleVisible": false,
"type": "heatmap_grid",
},
"layerId": "layer_0",
"layerType": "data",
"legend": Object {
"isVisible": true,
"position": "left",
"type": "heatmap_legend",
},
"shape": "heatmap",
"valueAccessor": "metric_formula_accessor",
"xAccessor": "metric_formula_accessor_x",
"yAccessor": "metric_formula_accessor_y",
},
},
"title": "test",
"visualizationType": "lnsHeatmap",
}
`);
});

View file

@ -0,0 +1,146 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
HeatmapVisualizationState,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { BuildDependencies, DEFAULT_LAYER_ID, LensAttributes, LensHeatmapConfig } from '../types';
import {
addLayerColumn,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
const ACCESSOR = 'metric_formula_accessor';
function getAccessorName(type: 'x' | 'y') {
return `${ACCESSOR}_${type}`;
}
function buildVisualizationState(config: LensHeatmapConfig): HeatmapVisualizationState {
const layer = config;
return {
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
shape: 'heatmap',
valueAccessor: ACCESSOR,
...(layer.xAxis
? {
xAccessor: getAccessorName('x'),
}
: {}),
...(layer.breakdown
? {
yAccessor: getAccessorName('y'),
}
: {}),
gridConfig: {
type: 'heatmap_grid',
isCellLabelVisible: false,
isXAxisLabelVisible: false,
isXAxisTitleVisible: false,
isYAxisLabelVisible: false,
isYAxisTitleVisible: false,
},
legend: {
isVisible: config.legend?.show || true,
position: config.legend?.position || 'left',
type: 'heatmap_legend',
},
};
}
function buildFormulaLayer(
layer: LensHeatmapConfig,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const defaultLayer = {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
};
if (layer.xAxis) {
const columnName = getAccessorName('x');
const breakdownColumn = getBreakdownColumn({
options: layer.xAxis,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
}
if (layer.breakdown) {
const columnName = getAccessorName('y');
const breakdownColumn = getBreakdownColumn({
options: layer.breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
}
return defaultLayer;
}
function getValueColumns(layer: LensHeatmapConfig) {
if (layer.breakdown && typeof layer.breakdown !== 'string') {
throw new Error('breakdown must be a field name when not using index source');
}
if (typeof layer.xAxis !== 'string') {
throw new Error('xAxis must be a field name when not using index source');
}
return [
...(layer.breakdown ? [getValueColumn(getAccessorName('y'), layer.breakdown as string)] : []),
getValueColumn(getAccessorName('x'), layer.xAxis as string),
getValueColumn(ACCESSOR, layer.value),
];
}
export async function buildHeatmap(
config: LensHeatmapConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: unknown, i: number, dataView: DataView) =>
buildFormulaLayer(cfg as LensHeatmapConfig, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsHeatmap',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
export * from './tag_cloud';
export * from './metric';
export * from './partition';
export * from './gauge';
export * from './heatmap';
export * from './region_map';
export * from './table';
export * from './xy';

View file

@ -0,0 +1,126 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildMetric } from './metric';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildMetric(
{
chartType: 'metric',
title: 'test',
dataset: {
esql: 'from test | count=count()',
},
value: 'count',
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count()",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"color": undefined,
"layerId": "layer_0",
"layerType": "data",
"metricAccessor": "metric_formula_accessor",
"showBar": false,
},
},
"title": "test",
"visualizationType": "lnsMetric",
}
`);
});

View file

@ -0,0 +1,245 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
MetricVisualizationState,
PersistedIndexPatternLayer,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { BuildDependencies, DEFAULT_LAYER_ID, LensAttributes, LensMetricConfig } from '../types';
import {
addLayerColumn,
addLayerFormulaColumns,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import {
getBreakdownColumn,
getFormulaColumn,
getHistogramColumn,
getValueColumn,
} from '../columns';
const ACCESSOR = 'metric_formula_accessor';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
const TRENDLINE_LAYER_ID = `layer_trendline`;
function getAccessorName(type: 'max' | 'breakdown' | 'secondary') {
return `${ACCESSOR}_${type}`;
}
function buildVisualizationState(config: LensMetricConfig): MetricVisualizationState {
const layer = config;
return {
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
metricAccessor: ACCESSOR,
color: layer.seriesColor,
// subtitle: layer.subtitle,
showBar: false,
...(layer.querySecondaryMetric
? {
secondaryMetricAccessor: getAccessorName('secondary'),
}
: {}),
...(layer.queryMaxValue
? {
maxAccessor: getAccessorName('max'),
showBar: true,
}
: {}),
...(layer.breakdown
? {
breakdownByAccessor: getAccessorName('breakdown'),
}
: {}),
...(layer.trendLine
? {
trendlineLayerId: `${DEFAULT_LAYER_ID}_trendline`,
trendlineLayerType: 'metricTrendline',
trendlineMetricAccessor: `${ACCESSOR}_trendline`,
trendlineTimeAccessor: HISTOGRAM_COLUMN_NAME,
...(layer.querySecondaryMetric
? {
trendlineSecondaryMetricAccessor: `${ACCESSOR}_secondary_trendline`,
}
: {}),
...(layer.queryMaxValue
? {
trendlineMaxAccessor: `${ACCESSOR}_max_trendline`,
}
: {}),
...(layer.breakdown
? {
trendlineBreakdownByAccessor: `${ACCESSOR}_breakdown_trendline`,
}
: {}),
}
: {}),
};
}
function buildFormulaLayer(
layer: LensMetricConfig,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [ACCESSOR, HISTOGRAM_COLUMN_NAME],
columns: {
[HISTOGRAM_COLUMN_NAME]: getHistogramColumn({
options: {
sourceField: dataView.timeFieldName,
params: {
interval: 'auto',
includeEmptyRows: true,
},
},
}),
},
sampling: 1,
};
const layers: {
layer_0: PersistedIndexPatternLayer;
layer_trendline?: PersistedIndexPatternLayer;
} = {
[DEFAULT_LAYER_ID]: {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
},
...(layer.trendLine
? {
[TRENDLINE_LAYER_ID]: {
linkToLayers: [DEFAULT_LAYER_ID],
...getFormulaColumn(
`${ACCESSOR}_trendline`,
{ value: layer.value },
dataView,
formulaAPI,
baseLayer
),
},
}
: {}),
};
const defaultLayer = layers[DEFAULT_LAYER_ID];
const trendLineLayer = layers[TRENDLINE_LAYER_ID];
if (layer.breakdown) {
const columnName = getAccessorName('breakdown');
const breakdownColumn = getBreakdownColumn({
options: layer.breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
if (trendLineLayer) {
addLayerColumn(trendLineLayer, `${columnName}_trendline`, breakdownColumn, true);
}
}
if (layer.querySecondaryMetric) {
const columnName = getAccessorName('secondary');
const formulaColumn = getFormulaColumn(
columnName,
{
value: layer.querySecondaryMetric,
},
dataView,
formulaAPI
);
addLayerFormulaColumns(defaultLayer, formulaColumn);
if (trendLineLayer) {
addLayerFormulaColumns(trendLineLayer, formulaColumn, 'X0');
}
}
if (layer.queryMaxValue) {
const columnName = getAccessorName('max');
const formulaColumn = getFormulaColumn(
columnName,
{
value: layer.queryMaxValue,
},
dataView,
formulaAPI
);
addLayerFormulaColumns(defaultLayer, formulaColumn);
if (trendLineLayer) {
addLayerFormulaColumns(trendLineLayer, formulaColumn, 'X0');
}
}
return layers[DEFAULT_LAYER_ID];
}
function getValueColumns(layer: LensMetricConfig) {
if (layer.breakdown && typeof layer.breakdown !== 'string') {
throw new Error('breakdown must be a field name when not using index source');
}
return [
...(layer.breakdown
? [getValueColumn(getAccessorName('breakdown'), layer.breakdown as string)]
: []),
getValueColumn(ACCESSOR, layer.value),
...(layer.queryMaxValue ? [getValueColumn(getAccessorName('max'), layer.queryMaxValue)] : []),
...(layer.querySecondaryMetric
? [getValueColumn(getAccessorName('secondary'), layer.querySecondaryMetric)]
: []),
];
}
export async function buildMetric(
config: LensMetricConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: unknown, i: number, dataView: DataView) =>
buildFormulaLayer(cfg as LensMetricConfig, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsMetric',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,157 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildPartitionChart } from './partition';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildPartitionChart(
{
chartType: 'treemap',
title: 'test',
dataset: {
esql: 'from test | count=count() by @timestamp, category',
},
value: 'count',
breakdown: ['@timestamp', 'category'],
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor_breakdown_0",
"fieldName": "@timestamp",
},
Object {
"columnId": "metric_formula_accessor_breakdown_1",
"fieldName": "category",
},
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor_breakdown_0",
"fieldName": "@timestamp",
},
Object {
"columnId": "metric_formula_accessor_breakdown_1",
"fieldName": "category",
},
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count() by @timestamp, category",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"layers": Array [
Object {
"allowMultipleMetrics": false,
"categoryDisplay": "default",
"layerId": "layer_0",
"layerType": "data",
"legendDisplay": "default",
"legendPosition": "right",
"metrics": Array [
"metric_formula_accessor",
],
"numberDisplay": "percent",
"primaryGroups": Array [
"metric_formula_accessor_breakdown_0",
"metric_formula_accessor_breakdown_1",
],
},
],
"shape": "treemap",
},
},
"title": "test",
"visualizationType": "lnsPie",
}
`);
});

View file

@ -0,0 +1,149 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
PieVisualizationState,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
BuildDependencies,
DEFAULT_LAYER_ID,
LensAttributes,
LensPieConfig,
LensTreeMapConfig,
LensMosaicConfig,
LensLegendConfig,
} from '../types';
import {
addLayerColumn,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
const ACCESSOR = 'metric_formula_accessor';
function buildVisualizationState(
config: LensTreeMapConfig | LensPieConfig | LensMosaicConfig
): PieVisualizationState {
const layer = config;
const layerBreakdown = Array.isArray(layer.breakdown) ? layer.breakdown : [layer.breakdown];
let legendDisplay: 'default' | 'hide' | 'show' = 'default';
let legendPosition: LensLegendConfig['position'] = 'right';
if ('legend' in config && config.legend) {
if ('show' in config.legend) {
legendDisplay = config.legend ? 'show' : 'hide';
}
legendPosition = config.legend.position || 'right';
}
return {
shape: config.chartType,
layers: [
{
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
metrics: [ACCESSOR],
allowMultipleMetrics: false,
numberDisplay: 'percent',
categoryDisplay: 'default',
legendDisplay,
legendPosition,
primaryGroups: layerBreakdown.map((breakdown, i) => `${ACCESSOR}_breakdown_${i}`),
},
],
};
}
function buildFormulaLayer(
layer: LensTreeMapConfig | LensPieConfig | LensMosaicConfig,
layerNr: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const layers = {
[DEFAULT_LAYER_ID]: {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
},
};
const defaultLayer = layers[DEFAULT_LAYER_ID];
if (layer.breakdown) {
const layerBreakdown = Array.isArray(layer.breakdown) ? layer.breakdown : [layer.breakdown];
layerBreakdown.reverse().forEach((breakdown, i) => {
const columnName = `${ACCESSOR}_breakdown_${i}`;
const breakdownColumn = getBreakdownColumn({
options: breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
});
} else {
throw new Error('breakdown must be defined!');
}
return defaultLayer;
}
function getValueColumns(layer: LensTreeMapConfig) {
if (layer.breakdown && layer.breakdown.filter((b) => typeof b !== 'string').length) {
throw new Error('breakdown must be a field name when not using index source');
}
return [
...(layer.breakdown
? layer.breakdown.map((b, i) => {
return getValueColumn(`${ACCESSOR}_breakdown_${i}`, b as string);
})
: []),
getValueColumn(ACCESSOR, layer.value),
];
}
export async function buildPartitionChart(
config: LensTreeMapConfig | LensPieConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: any, i: number, dataView: DataView) =>
buildFormulaLayer(cfg, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsPie',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,134 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildRegionMap } from './region_map';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildRegionMap(
{
chartType: 'regionmap',
title: 'test',
dataset: {
esql: 'from test | count=count() by category',
},
value: 'count',
breakdown: 'category',
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
Object {
"columnId": "metric_formula_accessor_breakdown",
"fieldName": "category",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
Object {
"columnId": "metric_formula_accessor_breakdown",
"fieldName": "category",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count() by category",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"layerId": "layer_0",
"layerType": "data",
"regionAccessor": "metric_formula_accessor_breakdown",
"valueAccessor": "metric_formula_accessor",
},
},
"title": "test",
"visualizationType": "lnsChoropleth",
}
`);
});

View file

@ -0,0 +1,123 @@
/*
* 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 type { FormBasedPersistedState, FormulaPublicApi } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { ChoroplethChartState } from '@kbn/maps-plugin/public/lens/choropleth_chart/types';
import {
BuildDependencies,
DEFAULT_LAYER_ID,
LensAttributes,
LensRegionMapConfig,
LensTagCloudConfig,
} from '../types';
import {
addLayerColumn,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
const ACCESSOR = 'metric_formula_accessor';
function getAccessorName(type: 'breakdown') {
return `${ACCESSOR}_${type}`;
}
function buildVisualizationState(
config: LensRegionMapConfig
): ChoroplethChartState & { layerType: 'data' } {
const layer = config;
return {
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
valueAccessor: ACCESSOR,
...(layer.breakdown
? {
regionAccessor: getAccessorName('breakdown'),
}
: {}),
};
}
function buildFormulaLayer(
layer: LensRegionMapConfig,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const layers = {
[DEFAULT_LAYER_ID]: {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
},
};
const defaultLayer = layers[DEFAULT_LAYER_ID];
if (layer.breakdown) {
const columnName = getAccessorName('breakdown');
const breakdownColumn = getBreakdownColumn({
options: layer.breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
} else {
throw new Error('breakdown must be defined for regionmap!');
}
return defaultLayer;
}
function getValueColumns(layer: LensTagCloudConfig) {
if (typeof layer.breakdown !== 'string') {
throw new Error('breakdown must be a field name when not using index source');
}
return [
getValueColumn(ACCESSOR, layer.value),
getValueColumn(getAccessorName('breakdown'), layer.breakdown as string),
];
}
export async function buildRegionMap(
config: LensRegionMapConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: unknown, i: number, dataView: DataView) =>
buildFormulaLayer(cfg as LensRegionMapConfig, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsChoropleth',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,140 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildTable } from './table';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildTable(
{
chartType: 'table',
title: 'test',
dataset: {
esql: 'from test | count=count() by category',
},
value: 'count',
breakdown: ['category'],
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor_breakdown_0",
"fieldName": "category",
},
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor_breakdown_0",
"fieldName": "category",
},
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count() by category",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"columns": Array [
Object {
"columnId": "metric_formula_accessor",
},
Object {
"columnId": "metric_formula_accessor_breakdown_0",
},
],
"layerId": "layer_0",
"layerType": "data",
},
},
"title": "test",
"visualizationType": "lnsDatatable",
}
`);
});

View file

@ -0,0 +1,138 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
DatatableVisualizationState,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { BuildDependencies, DEFAULT_LAYER_ID, LensAttributes, LensTableConfig } from '../types';
import {
addLayerColumn,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
const ACCESSOR = 'metric_formula_accessor';
function buildVisualizationState(config: LensTableConfig): DatatableVisualizationState {
const layer = config;
return {
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
columns: [
{ columnId: ACCESSOR },
...(layer.breakdown || []).map((breakdown, i) => ({
columnId: `${ACCESSOR}_breakdown_${i}`,
})),
...(layer.splitBy || []).map((breakdown, i) => ({ columnId: `${ACCESSOR}_splitby_${i}` })),
],
};
}
function buildFormulaLayer(
layer: LensTableConfig,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const layers = {
[DEFAULT_LAYER_ID]: {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
},
};
const defaultLayer = layers[DEFAULT_LAYER_ID];
if (layer.breakdown) {
layer.breakdown.reverse().forEach((breakdown, x) => {
const columnName = `${ACCESSOR}_breakdown_${x}`;
const breakdownColumn = getBreakdownColumn({
options: breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
});
} else {
throw new Error('breakdown must be defined for table!');
}
if (layer.splitBy) {
layer.splitBy.forEach((breakdown, x) => {
const columnName = `${ACCESSOR}_splitby_${x}`;
const breakdownColumn = getBreakdownColumn({
options: breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
});
}
return defaultLayer;
}
function getValueColumns(layer: LensTableConfig) {
if (layer.breakdown && layer.breakdown.filter((b) => typeof b !== 'string').length) {
throw new Error('breakdown must be a field name when not using index source');
}
if (layer.splitBy && layer.splitBy.filter((s) => typeof s !== 'string').length) {
throw new Error('xAxis must be a field name when not using index source');
}
return [
...(layer.breakdown
? layer.breakdown.map((b, i) => {
return getValueColumn(`${ACCESSOR}_breakdown_${i}`, b as string);
})
: []),
...(layer.splitBy
? layer.splitBy.map((b, i) => {
return getValueColumn(`${ACCESSOR}_splitby_${i}`, b as string);
})
: []),
getValueColumn(ACCESSOR, layer.value),
];
}
export async function buildTable(
config: LensTableConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: unknown, i: number, dataView: DataView) =>
buildFormulaLayer(cfg as LensTableConfig, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsDatatable',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,137 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildTagCloud } from './tag_cloud';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildTagCloud(
{
chartType: 'tagcloud',
title: 'test',
dataset: {
esql: 'from test | count=count() by category',
},
value: 'count',
breakdown: 'category',
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
Object {
"columnId": "metric_formula_accessor_breakdown",
"fieldName": "category",
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor",
"fieldName": "count",
},
Object {
"columnId": "metric_formula_accessor_breakdown",
"fieldName": "category",
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count() by category",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"layerId": "layer_0",
"maxFontSize": 72,
"minFontSize": 12,
"orientation": "single",
"showLabel": true,
"tagAccessor": "category",
"valueAccessor": "count",
},
},
"title": "test",
"visualizationType": "lnsTagcloud",
}
`);
});

View file

@ -0,0 +1,123 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
TagcloudState,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { BuildDependencies, DEFAULT_LAYER_ID, LensAttributes, LensTagCloudConfig } from '../types';
import {
addLayerColumn,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
isFormulaDataset,
} from '../utils';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
const ACCESSOR = 'metric_formula_accessor';
function getAccessorName(type: 'breakdown') {
return `${ACCESSOR}_${type}`;
}
function buildVisualizationState(config: LensTagCloudConfig): TagcloudState {
const layer = config;
const isFormula = isFormulaDataset(config.dataset) || isFormulaDataset(layer.dataset);
return {
layerId: DEFAULT_LAYER_ID,
valueAccessor: !isFormula ? layer.value : ACCESSOR,
maxFontSize: 72,
minFontSize: 12,
orientation: 'single',
showLabel: true,
...(layer.breakdown
? {
tagAccessor: !isFormula ? (layer.breakdown as string) : getAccessorName('breakdown'),
}
: {}),
};
}
function buildFormulaLayer(
layer: LensTagCloudConfig,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
const layers = {
[DEFAULT_LAYER_ID]: {
...getFormulaColumn(
ACCESSOR,
{
value: layer.value,
},
dataView,
formulaAPI
),
},
};
const defaultLayer = layers[DEFAULT_LAYER_ID];
if (layer.breakdown) {
const columnName = getAccessorName('breakdown');
const breakdownColumn = getBreakdownColumn({
options: layer.breakdown,
dataView,
});
addLayerColumn(defaultLayer, columnName, breakdownColumn, true);
} else {
throw new Error('breakdown must be defined on tagcloud!');
}
return defaultLayer;
}
function getValueColumns(layer: LensTagCloudConfig) {
if (layer.breakdown && typeof layer.breakdown !== 'string') {
throw new Error('breakdown must be a field name when not using index source');
}
return [
getValueColumn(ACCESSOR, layer.value),
getValueColumn(getAccessorName('breakdown'), layer.breakdown as string),
];
}
export async function buildTagCloud(
config: LensTagCloudConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: unknown, i: number, dataView: DataView) =>
buildFormulaLayer(cfg as LensTagCloudConfig, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
return {
title: config.title,
visualizationType: 'lnsTagcloud',
references: buildReferences(dataviews),
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,181 @@
/*
* 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 type { DataView, DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { buildXY } from './xy';
const dataViews: Record<string, DataView> = {
test: {
id: 'test',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return {
type: 'datetime',
} as unknown as DataViewField;
case 'category':
return {
type: 'string',
} as unknown as DataViewField;
case 'price':
return {
type: 'number',
} as unknown as DataViewField;
default:
return undefined;
}
},
} as any,
} as unknown as DataView,
};
function mockDataViewsService() {
return {
get: jest.fn(async (id: '1' | '2') => {
const result = {
...dataViews[id],
metaFields: [],
isPersisted: () => true,
toSpec: () => ({}),
};
return result;
}),
create: jest.fn(),
} as unknown as Pick<DataViewsContract, 'get' | 'create'>;
}
test('generates metric chart config', async () => {
const result = await buildXY(
{
chartType: 'xy',
title: 'test',
dataset: {
esql: 'from test | count=count() by @timestamp',
},
layers: [
{
type: 'series',
seriesType: 'bar',
label: 'test',
value: 'count',
xAxis: '@timestamp',
},
],
},
{
dataViewsAPI: mockDataViewsService() as any,
formulaAPI: {} as any,
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
"id": "test",
"name": "indexpattern-datasource-layer-layer_0",
"type": "index-pattern",
},
],
"state": Object {
"adHocDataViews": Object {
"test": Object {},
},
"datasourceStates": Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [
Object {
"columnId": "metric_formula_accessor0_x",
"fieldName": "@timestamp",
},
Object {
"columnId": "metric_formula_accessor0",
"fieldName": "count",
"meta": Object {
"type": "number",
},
},
],
"columns": Array [
Object {
"columnId": "metric_formula_accessor0_x",
"fieldName": "@timestamp",
},
Object {
"columnId": "metric_formula_accessor0",
"fieldName": "count",
"meta": Object {
"type": "number",
},
},
],
"index": "test",
"query": Object {
"esql": "from test | count=count() by @timestamp",
},
},
},
},
},
"filters": Array [],
"internalReferences": Array [],
"query": Object {
"language": "kuery",
"query": "",
},
"visualization": Object {
"axisTitlesVisibilitySettings": Object {
"x": true,
"yLeft": true,
"yRight": true,
},
"fittingFunction": "None",
"gridlinesVisibilitySettings": Object {
"x": true,
"yLeft": true,
"yRight": true,
},
"labelsOrientation": Object {
"x": 0,
"yLeft": 0,
"yRight": 0,
},
"layers": Array [
Object {
"accessors": Array [
"metric_formula_accessor0",
],
"layerId": "layer_0",
"layerType": "data",
"seriesType": "bar",
"xAccessor": "metric_formula_accessor0_x",
},
],
"legend": Object {
"isVisible": true,
"position": "left",
},
"preferredSeriesType": "line",
"tickLabelsVisibilitySettings": Object {
"x": true,
"yLeft": true,
"yRight": true,
},
"valueLabels": "hide",
},
},
"title": "test",
"visualizationType": "lnsXY",
}
`);
});

View file

@ -0,0 +1,240 @@
/*
* 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 type {
FormBasedPersistedState,
FormulaPublicApi,
XYState,
XYReferenceLineLayerConfig,
XYDataLayerConfig,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { XYByValueAnnotationLayerConfig } from '@kbn/lens-plugin/public/visualizations/xy/types';
import type { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-common';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
import {
addLayerColumn,
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
} from '../utils';
import {
BuildDependencies,
LensAnnotationLayer,
LensAttributes,
LensReferenceLineLayer,
LensSeriesLayer,
LensXYConfig,
} from '../types';
const ACCESSOR = 'metric_formula_accessor';
function buildVisualizationState(config: LensXYConfig): XYState {
return {
legend: {
isVisible: config.legend?.show || true,
position: config.legend?.position || 'left',
},
preferredSeriesType: 'line',
valueLabels: 'hide',
fittingFunction: 'None',
axisTitlesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
labelsOrientation: {
x: 0,
yLeft: 0,
yRight: 0,
},
gridlinesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
layers: config.layers.map((layer, i) => {
switch (layer.type) {
case 'annotation':
return {
layerId: `layer_${i}`,
layerType: 'annotations',
annotations: layer.events.map((e, eventNr) => {
if ('datetime' in e) {
return {
type: 'manual',
id: `annotation_${eventNr}`,
icon: e.icon || 'triangle',
color: e.color || 'blue',
label: e.name,
key: {
type: 'point_in_time',
timestamp: e.datetime,
},
};
} else {
return {
id: `event${eventNr}`,
type: 'query',
icon: e.icon || 'triangle',
color: e.color || 'blue',
label: e.name,
key: {
type: 'point_in_time',
},
filter: {
type: 'kibana_query',
query: e.filter,
language: 'kuery',
},
...(e.field ? { timeField: e.field } : {}),
} as QueryPointEventAnnotationConfig;
}
}),
ignoreGlobalFilters: true,
} as XYByValueAnnotationLayerConfig;
case 'reference':
return {
layerId: `layer_${i}`,
layerType: 'referenceLine',
accessors: [`${ACCESSOR}${i}`],
yConfig: [
{
forAccessor: `${ACCESSOR}${i}`,
axisMode: 'left',
},
],
} as XYReferenceLineLayerConfig;
case 'series':
return {
layerId: `layer_${i}`,
layerType: 'data',
xAccessor: `${ACCESSOR}${i}_x`,
...(layer.breakdown
? {
splitAccessor: `${ACCESSOR}${i}_y}`,
}
: {}),
accessors: [`${ACCESSOR}${i}`],
seriesType: layer.seriesType || 'line',
} as XYDataLayerConfig;
}
}),
};
}
function getValueColumns(layer: LensSeriesLayer, i: number) {
if (layer.breakdown && typeof layer.breakdown !== 'string') {
throw new Error('breakdown must be a field name when not using index source');
}
if (typeof layer.xAxis !== 'string') {
throw new Error('xAxis must be a field name when not using index source');
}
return [
...(layer.breakdown
? [getValueColumn(`${ACCESSOR}${i}_breakdown`, layer.breakdown as string)]
: []),
getValueColumn(`${ACCESSOR}${i}_x`, layer.xAxis as string),
getValueColumn(`${ACCESSOR}${i}`, layer.value, 'number'),
];
}
function buildFormulaLayer(
layer: LensSeriesLayer | LensAnnotationLayer | LensReferenceLineLayer,
i: number,
dataView: DataView,
formulaAPI: FormulaPublicApi
): FormBasedPersistedState['layers'][0] {
if (layer.type === 'series') {
const resultLayer = {
...getFormulaColumn(
`${ACCESSOR}${i}`,
{
value: layer.value,
},
dataView,
formulaAPI
),
};
if (layer.xAxis) {
const columnName = `${ACCESSOR}${i}_x`;
const breakdownColumn = getBreakdownColumn({
options: layer.xAxis,
dataView,
});
addLayerColumn(resultLayer, columnName, breakdownColumn, true);
}
if (layer.breakdown) {
const columnName = `${ACCESSOR}${i}_y`;
const breakdownColumn = getBreakdownColumn({
options: layer.breakdown,
dataView,
});
addLayerColumn(resultLayer, columnName, breakdownColumn, true);
}
return resultLayer;
} else if (layer.type === 'annotation') {
// nothing ?
} else if (layer.type === 'reference') {
return {
...getFormulaColumn(
`${ACCESSOR}${i}`,
{
value: layer.value,
},
dataView,
formulaAPI
),
};
}
return {
columns: {},
columnOrder: [],
};
}
export async function buildXY(
config: LensXYConfig,
{ dataViewsAPI, formulaAPI }: BuildDependencies
): Promise<LensAttributes> {
const dataviews: Record<string, DataView> = {};
const _buildFormulaLayer = (cfg: any, i: number, dataView: DataView) =>
buildFormulaLayer(cfg, i, dataView, formulaAPI);
const datasourceStates = await buildDatasourceStates(
config,
dataviews,
_buildFormulaLayer,
getValueColumns,
dataViewsAPI
);
const references = buildReferences(dataviews);
return {
title: config.title,
visualizationType: 'lnsXY',
references,
state: {
datasourceStates,
internalReferences: [],
filters: [],
query: { language: 'kuery', query: '' },
visualization: buildVisualizationState(config),
// Getting the spec from a data view is a heavy operation, that's why the result is cached.
adHocDataViews: getAdhocDataviews(dataviews),
},
};
}

View file

@ -0,0 +1,51 @@
/*
* 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 { getBreakdownColumn } from './breakdown';
import type { DataView } from '@kbn/data-views-plugin/common';
const dataView = {
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return { type: 'date' };
case 'category':
return { type: 'string' };
case 'price':
return { type: 'number' };
default:
return { type: 'string' };
}
},
},
};
test('uses terms when field is a string', () => {
const column = getBreakdownColumn({
options: 'category',
dataView: dataView as unknown as DataView,
});
expect(column.operationType).toEqual('terms');
});
test('uses date histogram when field is a date', () => {
const column = getBreakdownColumn({
options: '@timestamp',
dataView: dataView as unknown as DataView,
});
expect(column.operationType).toEqual('date_histogram');
});
test('uses intervals when field is a number', () => {
const column = getBreakdownColumn({
options: 'price',
dataView: dataView as unknown as DataView,
});
expect(column.operationType).toEqual('range');
});

View file

@ -0,0 +1,109 @@
/*
* 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 type { DataView } from '@kbn/data-views-plugin/public';
import type { GenericIndexPatternColumn } from '@kbn/lens-plugin/public';
import {
LensBreakdownConfig,
LensBreakdownDateHistogramConfig,
LensBreakdownFiltersConfig,
LensBreakdownIntervalsConfig,
LensBreakdownTopValuesConfig,
} from '../types';
import { getHistogramColumn } from './date_histogram';
import { getTopValuesColumn } from './top_values';
import { getIntervalsColumn } from './intervals';
import { getFiltersColumn } from './filters';
const DEFAULT_BREAKDOWN_SIZE = 5;
function getBreakdownType(field: string, dataview: DataView) {
if (!dataview.fields.getByName(field)) {
throw new Error(
`field ${field} does not exist on dataview ${dataview.id ? dataview.id : dataview.title}`
);
}
switch (dataview.fields.getByName(field)!.type) {
case 'string':
return 'topValues';
case 'number':
return 'intervals';
case 'date':
return 'dateHistogram';
default:
return 'topValues';
}
}
export const getBreakdownColumn = ({
options,
dataView,
}: {
options: LensBreakdownConfig;
dataView: DataView;
}): GenericIndexPatternColumn => {
const breakdownType =
typeof options === 'string' ? getBreakdownType(options, dataView) : options.type;
const field: string =
typeof options === 'string' ? options : 'field' in options ? options.field : '';
const config = typeof options !== 'string' ? options : {};
switch (breakdownType) {
case 'dateHistogram':
return getHistogramColumn({
options: {
sourceField: field,
params:
typeof options !== 'string'
? {
interval: (options as LensBreakdownDateHistogramConfig).minimumInterval || 'auto',
}
: {
interval: 'auto',
},
},
});
case 'topValues':
const topValuesOptions = config as LensBreakdownTopValuesConfig;
return getTopValuesColumn({
field,
options: {
size: topValuesOptions.size || DEFAULT_BREAKDOWN_SIZE,
},
});
case 'intervals':
const intervalOptions = config as LensBreakdownIntervalsConfig;
return getIntervalsColumn({
field,
options: {
type: 'range',
ranges: [
{
from: 0,
to: 1000,
label: '',
},
],
maxBars: intervalOptions.granularity || 'auto',
},
});
case 'filters':
const filterOptions = config as LensBreakdownFiltersConfig;
return getFiltersColumn({
options: {
filters: filterOptions.filters.map((f) => ({
label: f.label || '',
input: {
language: 'kuery',
query: f.filter,
},
})),
},
});
}
};

View file

@ -0,0 +1,33 @@
/*
* 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 type { DateHistogramIndexPatternColumn } from '@kbn/lens-plugin/public';
export type DateHistogramColumnParams = DateHistogramIndexPatternColumn['params'];
export const getHistogramColumn = ({
options,
}: {
options?: Partial<
Pick<DateHistogramIndexPatternColumn, 'sourceField'> & {
params: DateHistogramColumnParams;
}
>;
}): DateHistogramIndexPatternColumn => {
const { interval = 'auto', ...rest } = options?.params ?? {};
return {
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
scale: 'interval',
sourceField: '@timestamp',
...options,
params: { interval, ...rest },
};
};

View file

@ -0,0 +1,28 @@
/*
* 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 type { FiltersIndexPatternColumn } from '@kbn/lens-plugin/public';
export const getFiltersColumn = ({
options,
}: {
options?: FiltersIndexPatternColumn['params'];
}): FiltersIndexPatternColumn => {
const { filters = [], ...params } = options ?? {};
return {
label: `Filters`,
dataType: 'number',
operationType: 'filters',
scale: 'ordinal',
isBucketed: true,
params: {
filters,
...params,
},
};
};

View file

@ -0,0 +1,38 @@
/*
* 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 type { FormulaPublicApi, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
type LensFormula = Parameters<FormulaPublicApi['insertOrReplaceFormulaColumn']>[1];
export type FormulaValueConfig = Omit<LensFormula, 'formula'> & {
color?: string;
value: string;
};
export function getFormulaColumn(
id: string,
config: FormulaValueConfig,
dataView: DataView,
formulaAPI: FormulaPublicApi,
baseLayer?: PersistedIndexPatternLayer
): PersistedIndexPatternLayer {
const { value, ...rest } = config;
const formulaLayer = formulaAPI.insertOrReplaceFormulaColumn(
id,
{ formula: value, ...rest },
baseLayer || { columnOrder: [], columns: {} },
dataView
);
if (!formulaLayer) {
throw new Error('Error generating the data layer for the chart');
}
return formulaLayer;
}

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
export * from './date_histogram';
export * from './formula';
export * from './static';
export * from './top_values';
export * from './filters';
export * from './intervals';
export * from './breakdown';
export * from './value';

View file

@ -0,0 +1,31 @@
/*
* 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 type { RangeIndexPatternColumn } from '@kbn/lens-plugin/public';
export const getIntervalsColumn = ({
field,
options,
}: {
field: string;
options: RangeIndexPatternColumn['params'];
}): RangeIndexPatternColumn => {
const { ranges = [], ...params } = options ?? {};
return {
label: `Intervals of ${field}`,
dataType: 'number',
operationType: 'range',
scale: 'ordinal',
sourceField: field,
isBucketed: true,
params: {
ranges,
...params,
},
};
};

View file

@ -0,0 +1,43 @@
/*
* 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 type { PersistedIndexPatternLayer, FormulaPublicApi } from '@kbn/lens-plugin/public';
import type { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types';
export type LensFormula = Parameters<FormulaPublicApi['insertOrReplaceFormulaColumn']>[1];
export type StaticValueConfig = Omit<LensFormula, 'formula'> & {
color?: string;
value: string;
};
export function getStaticColumn(
id: string,
baseLayer: PersistedIndexPatternLayer,
config: StaticValueConfig
): PersistedIndexPatternLayer {
const { label, ...params } = config;
return {
linkToLayers: [],
columnOrder: [...baseLayer.columnOrder, id],
columns: {
[id]: {
label: label ?? 'Reference',
dataType: 'number',
operationType: 'static_value',
isStaticValue: true,
isBucketed: false,
scale: 'ratio',
params,
references: [],
customLabel: true,
} as ReferenceBasedIndexPatternColumn,
},
sampling: 1,
incompleteColumns: {},
};
}

View file

@ -0,0 +1,48 @@
/*
* 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 type { TermsIndexPatternColumn } from '@kbn/lens-plugin/public';
import { TopValuesColumnParams } from '../../attribute_builder/utils';
const DEFAULT_BREAKDOWN_SIZE = 10;
export const getTopValuesColumn = ({
field,
options,
}: {
field: string;
options?: Partial<TopValuesColumnParams>;
}): TermsIndexPatternColumn => {
const { size = DEFAULT_BREAKDOWN_SIZE, ...params } = options ?? {};
return {
label: `Top ${size} values of ${field}`,
dataType: 'string',
operationType: 'terms',
scale: 'ordinal',
sourceField: field,
isBucketed: true,
params: {
size,
orderBy: {
type: 'alphabetical',
fallback: false,
},
orderDirection: 'asc',
otherBucket: false,
missingBucket: false,
parentFormat: {
id: 'terms',
},
include: [],
exclude: [],
includeIsRegex: false,
excludeIsRegex: false,
...params,
},
};
};

View file

@ -0,0 +1,22 @@
/*
* 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 type { TextBasedLayerColumn } from '@kbn/lens-plugin/public/datasources/text_based/types';
import type { DatatableColumnType } from '@kbn/expressions-plugin/common';
export function getValueColumn(
id: string,
fieldName?: string,
type?: DatatableColumnType
): TextBasedLayerColumn {
return {
columnId: id,
fieldName: fieldName || id,
...(type ? { meta: { type } } : {}),
};
}

View file

@ -0,0 +1,76 @@
/*
* 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 type { FormulaPublicApi, LensEmbeddableInput } from '@kbn/lens-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { v4 as uuidv4 } from 'uuid';
import { LensAttributes, LensConfig, LensConfigOptions } from './types';
import {
buildGauge,
buildHeatmap,
buildMetric,
buildRegionMap,
buildTagCloud,
buildTable,
buildXY,
buildPartitionChart,
} from './charts';
export class LensConfigBuilder {
private charts = {
metric: buildMetric,
tagcloud: buildTagCloud,
treemap: buildPartitionChart,
pie: buildPartitionChart,
donut: buildPartitionChart,
gauge: buildGauge,
heatmap: buildHeatmap,
mosaic: buildPartitionChart,
regionmap: buildRegionMap,
xy: buildXY,
table: buildTable,
};
private formulaAPI: FormulaPublicApi;
private dataViewsAPI: DataViewsPublicPluginStart;
constructor(formulaAPI: FormulaPublicApi, dataViewsAPI: DataViewsPublicPluginStart) {
this.formulaAPI = formulaAPI;
this.dataViewsAPI = dataViewsAPI;
}
async build(
config: LensConfig,
options: LensConfigOptions = {}
): Promise<LensAttributes | LensEmbeddableInput | undefined> {
const { chartType } = config;
const chartConfig = await this.charts[chartType](config as any, {
formulaAPI: this.formulaAPI,
dataViewsAPI: this.dataViewsAPI,
});
const chartState = {
...chartConfig,
state: {
...chartConfig.state,
filters: options.filters || [],
query: options.query || { language: 'kuery', query: '' },
},
};
if (options.embeddable) {
return {
id: uuidv4(),
attributes: chartState,
timeRange: options.timeRange,
references: chartState.references,
} as LensEmbeddableInput;
}
return chartState;
}
}

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export * from './config_builder';
export * from './types';

View file

@ -0,0 +1,274 @@
/*
* 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 type { FormulaPublicApi, TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { Filter, Query } from '@kbn/es-query';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { Datatable } from '@kbn/expressions-plugin/common';
export type LensAttributes = TypedLensByValueInput['attributes'];
export const DEFAULT_LAYER_ID = 'layer_0';
type Identity<T> = T extends object
? {
[P in keyof T]: T[P];
}
: T;
export type ChartType =
| 'xy'
| 'pie'
| 'heatmap'
| 'metric'
| 'gauge'
| 'donut'
| 'mosaic'
| 'regionmap'
| 'table'
| 'tagcloud'
| 'treemap';
export interface TimeRange {
from: string;
to: string;
type: 'relative' | 'absolute';
}
export type LensLayerQuery = string;
export interface LensDataviewDataset {
index: string;
timeFieldName?: string;
}
export type LensDatatableDataset = Datatable;
export interface LensESQLDataset {
esql: string;
}
export type LensDataset = LensDataviewDataset | LensDatatableDataset | LensESQLDataset;
export interface LensBaseConfig {
title: string;
/** default data view id or index pattern to use, it can be overriden on each query */
dataset?: LensDataset;
}
export interface LensBaseLayer {
label?: string;
filter?: string;
format?: 'bytes' | 'currency' | 'duration' | 'number' | 'percent' | 'string';
randomSampling?: number;
useGlobalFilter?: boolean;
seriesColor?: string;
dataset?: LensDataset;
value: LensLayerQuery;
}
export type LensConfig =
| LensMetricConfig
| LensGaugeConfig
| LensPieConfig
| LensHeatmapConfig
| LensMosaicConfig
| LensRegionMapConfig
| LensTableConfig
| LensTagCloudConfig
| LensTreeMapConfig
| LensXYConfig;
export interface LensConfigOptions {
/** if true the output will be embeddable input, else lens attributes */
embeddable?: boolean;
/** optional time range override */
timeRange?: TimeRange;
filters?: Filter[];
query?: Query;
}
export interface LensLegendConfig {
show?: boolean;
position?: 'top' | 'left' | 'bottom' | 'right';
}
export interface LensBreakdownDateHistogramConfig {
type: 'dateHistogram';
field: string;
minimumInterval?: string;
}
export interface LensBreakdownFiltersConfig {
type: 'filters';
filters: Array<{
label?: string;
filter: string;
}>;
}
export interface LensBreakdownIntervalsConfig {
type: 'intervals';
field: string;
granularity?: number;
}
export interface LensBreakdownTopValuesConfig {
type: 'topValues';
field: string;
size?: number;
}
export type LensBreakdownConfig =
| string
| Identity<
(
| LensBreakdownTopValuesConfig
| LensBreakdownIntervalsConfig
| LensBreakdownFiltersConfig
| LensBreakdownDateHistogramConfig
) & { colorPalette?: string }
>;
export interface LensMetricConfigBase {
chartType: 'metric';
querySecondaryMetric?: LensLayerQuery;
queryMaxValue?: LensLayerQuery;
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown?: LensBreakdownConfig;
trendLine?: boolean;
}
export type LensMetricConfig = Identity<LensBaseConfig & LensBaseLayer & LensMetricConfigBase>;
export interface LensGaugeConfigBase {
chartType: 'gauge';
queryMinValue?: LensLayerQuery;
queryMaxValue?: LensLayerQuery;
queryGoalValue?: LensLayerQuery;
shape?: 'arc' | 'circle' | 'horizontalBullet' | 'verticalBullet';
}
export type LensGaugeConfig = Identity<LensBaseConfig & LensBaseLayer & LensGaugeConfigBase>;
export interface LensPieConfigBase {
chartType: 'pie' | 'donut';
breakdown: LensBreakdownConfig[];
legend?: Identity<LensLegendConfig>;
}
export type LensPieConfig = Identity<LensBaseConfig & LensBaseLayer & LensPieConfigBase>;
export interface LensTreeMapConfigBase {
chartType: 'treemap';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig[];
}
export type LensTreeMapConfig = Identity<LensBaseConfig & LensBaseLayer & LensTreeMapConfigBase>;
export interface LensTagCloudConfigBase {
chartType: 'tagcloud';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig;
}
export type LensTagCloudConfig = Identity<LensBaseConfig & LensBaseLayer & LensTagCloudConfigBase>;
export interface LensRegionMapConfigBase {
chartType: 'regionmap';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig;
}
export type LensRegionMapConfig = Identity<
LensBaseConfig & LensBaseLayer & LensRegionMapConfigBase
>;
export interface LensMosaicConfigBase {
chartType: 'mosaic';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig;
/** field name to apply breakdown based on field type or full breakdown configuration */
xAxis: LensBreakdownConfig;
}
export type LensMosaicConfig = Identity<LensBaseConfig & LensBaseLayer & LensMosaicConfigBase>;
export interface LensTableConfigBase {
chartType: 'table';
/** field name to breakdown based on field type or full breakdown configuration */
splitBy?: LensBreakdownConfig[];
/** field name to breakdown based on field type or full breakdown configuration */
breakdown?: LensBreakdownConfig[];
}
export type LensTableConfig = Identity<LensBaseConfig & LensBaseLayer & LensTableConfigBase>;
export interface LensHeatmapConfigBase {
chartType: 'heatmap';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig;
xAxis: LensBreakdownConfig;
legend?: Identity<LensLegendConfig>;
}
export type LensHeatmapConfig = Identity<LensBaseConfig & LensBaseLayer & LensHeatmapConfigBase>;
export interface LensReferenceLineLayerBase {
type: 'reference';
lineThickness?: number;
color?: string;
fill?: 'none' | 'above' | 'below';
value?: number;
}
export type LensReferenceLineLayer = LensReferenceLineLayerBase & LensBaseLayer;
export interface LensAnnotationLayerBaseProps {
name: string;
color?: string;
icon?: string;
}
export type LensAnnotationLayer = Identity<
LensBaseLayer & {
type: 'annotation';
events: Array<
| Identity<
LensAnnotationLayerBaseProps & {
datetime: string;
}
>
| Identity<
LensAnnotationLayerBaseProps & {
field: string;
filter: string;
}
>
>;
}
>;
export type LensSeriesLayer = Identity<
LensBaseLayer & {
type: 'series';
breakdown?: LensBreakdownConfig;
xAxis: LensBreakdownConfig;
seriesType: 'line' | 'bar' | 'area';
}
>;
export interface LensXYConfigBase {
chartType: 'xy';
layers: Array<LensSeriesLayer | LensAnnotationLayer | LensReferenceLineLayer>;
legend?: Identity<LensLegendConfig>;
}
export interface BuildDependencies {
dataViewsAPI: DataViewsPublicPluginStart;
formulaAPI: FormulaPublicApi;
}
export type LensXYConfig = Identity<LensBaseConfig & LensXYConfigBase>;

View file

@ -0,0 +1,220 @@
/*
* 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 {
getAdhocDataviews,
buildDatasourceStates,
buildReferences,
getDatasetIndex,
addLayerColumn,
isFormulaDataset,
} from './utils';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
GenericIndexPatternColumn,
PersistedIndexPatternLayer,
} from '@kbn/lens-plugin/public';
const dataView = {
id: 'test-dataview',
fields: {
getByName: (name: string) => {
switch (name) {
case '@timestamp':
return 'datetime';
case 'category':
return 'string';
case 'price':
return 'number';
default:
return 'string';
}
},
},
toSpec: () => ({}),
};
describe('isFormulaDataset', () => {
test('isFormulaDataset returns true when dataset is based on index and timefield', () => {
const result = isFormulaDataset({
index: 'test',
timeFieldName: 'test',
});
expect(result).toEqual(true);
});
test('isFormulaDataset returns false when dataset is not based on index and timefield', () => {
const result = isFormulaDataset({
esql: 'test',
});
expect(result).toEqual(false);
const result2 = isFormulaDataset({
type: 'datatable',
columns: [],
rows: [],
});
expect(result2).toEqual(false);
});
});
test('build references correctly builds references', () => {
const results = buildReferences({
layer1: dataView as unknown as DataView,
layer2: dataView as unknown as DataView,
});
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"id": "test-dataview",
"name": "indexpattern-datasource-layer-layer1",
"type": "index-pattern",
},
Object {
"id": "test-dataview",
"name": "indexpattern-datasource-layer-layer2",
"type": "index-pattern",
},
]
`);
});
test('getAdhocDataviews', () => {
const results = getAdhocDataviews({
layer1: dataView as unknown as DataView,
layer2: dataView as unknown as DataView,
});
expect(results).toMatchInlineSnapshot(`
Object {
"test-dataview": Object {},
}
`);
});
describe('getDatasetIndex', () => {
test('returns index if provided', () => {
const result = getDatasetIndex({
index: 'test',
timeFieldName: '@timestamp',
});
expect(result).toMatchInlineSnapshot(`
Object {
"index": "test",
"timeFieldName": "@timestamp",
}
`);
});
test('extracts index from esql query', () => {
const result = getDatasetIndex({
esql: 'from test_index | limit 10',
});
expect(result).toMatchInlineSnapshot(`
Object {
"index": "test_index",
"timeFieldName": "@timestamp",
}
`);
});
test('returns undefined if no query or iundex provided', () => {
const result = getDatasetIndex({
type: 'datatable',
columns: [],
rows: [],
});
expect(result).toMatchInlineSnapshot(`undefined`);
});
});
describe('addLayerColumn', () => {
test('adds column to the end', () => {
const layer = {
columns: [],
columnOrder: [],
} as unknown as PersistedIndexPatternLayer;
addLayerColumn(layer, 'first', {
test: 'test',
} as unknown as GenericIndexPatternColumn);
addLayerColumn(layer, 'second', {
test: 'test',
} as unknown as GenericIndexPatternColumn);
addLayerColumn(
layer,
'before_first',
{
test: 'test',
} as unknown as GenericIndexPatternColumn,
true
);
expect(layer).toMatchInlineSnapshot(`
Object {
"columnOrder": Array [
"before_first",
"first",
"second",
],
"columns": Object {
"before_first": Object {
"test": "test",
},
"first": Object {
"test": "test",
},
"second": Object {
"test": "test",
},
},
}
`);
});
});
describe('buildDatasourceStates', () => {
test('correctly builds esql layer', async () => {
const results = await buildDatasourceStates(
{
title: 'test',
layers: [
{
dataset: {
esql: 'from test | limit 10',
},
label: 'test',
value: 'test',
},
],
},
{},
() => undefined,
() => [],
{
get: async () => ({ id: 'test' }),
} as any
);
expect(results).toMatchInlineSnapshot(`
Object {
"formBased": Object {
"layers": Object {},
},
"textBased": Object {
"layers": Object {
"layer_0": Object {
"allColumns": Array [],
"columns": Array [],
"index": "test",
"query": Object {
"esql": "from test | limit 10",
},
},
},
},
}
`);
});
});

View file

@ -0,0 +1,270 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common/src/server_types';
import type {
DataViewSpec,
DataView,
DataViewsPublicPluginStart,
} from '@kbn/data-views-plugin/public';
import type {
GenericIndexPatternColumn,
PersistedIndexPatternLayer,
} from '@kbn/lens-plugin/public';
import type {
TextBasedLayerColumn,
TextBasedPersistedState,
} from '@kbn/lens-plugin/public/datasources/text_based/types';
import { AggregateQuery, getIndexPatternFromESQLQuery } from '@kbn/es-query';
import {
LensAnnotationLayer,
LensAttributes,
LensBaseConfig,
LensBaseLayer,
LensDataset,
LensDatatableDataset,
LensESQLDataset,
} from './types';
export const getDefaultReferences = (
index: string,
dataLayerId: string
): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: index,
name: `indexpattern-datasource-layer-${dataLayerId}`,
},
];
};
export function buildReferences(dataviews: Record<string, DataView>) {
const references = [];
for (const layerid in dataviews) {
if (dataviews[layerid]) {
references.push(...getDefaultReferences(dataviews[layerid].id!, layerid));
}
}
return references.flat();
}
const getAdhocDataView = (dataView: DataView): Record<string, DataViewSpec> => {
return {
[dataView.id ?? uuidv4()]: {
...dataView.toSpec(),
},
};
};
export const getAdhocDataviews = (dataviews: Record<string, DataView>) => {
let adHocDataViews = {};
[...new Set(Object.values(dataviews))].forEach((d) => {
adHocDataViews = {
...adHocDataViews,
...getAdhocDataView(d),
};
});
return adHocDataViews;
};
export function isFormulaDataset(dataset?: LensDataset) {
if (dataset && 'index' in dataset) {
return true;
}
return false;
}
/**
* it loads dataview by id or creates an ad-hoc dataview if index pattern is provided
* @param index
* @param dataViewsAPI
* @param timeField
*/
export async function getDataView(
index: string,
dataViewsAPI: DataViewsPublicPluginStart,
timeField?: string
) {
let dataView: DataView;
try {
dataView = await dataViewsAPI.get(index, false);
} catch {
dataView = await dataViewsAPI.create({
title: index,
timeFieldName: timeField || '@timestamp',
});
}
return dataView;
}
export function getDatasetIndex(dataset?: LensDataset) {
if (!dataset) return undefined;
let index: string;
let timeFieldName: string = '@timestamp';
if ('index' in dataset) {
index = dataset.index;
timeFieldName = dataset.timeFieldName || '@timestamp';
} else if ('esql' in dataset) {
index = getIndexPatternFromESQLQuery(dataset.esql); // parseIndexFromQuery(config.dataset.query);
} else {
return undefined;
}
return { index, timeFieldName };
}
function buildDatasourceStatesLayer(
layer: LensBaseLayer,
i: number,
dataset: LensDataset,
dataView: DataView | undefined,
buildFormulaLayers: (
config: unknown,
i: number,
dataView: DataView
) => PersistedIndexPatternLayer | undefined,
getValueColumns: (config: unknown, i: number) => TextBasedLayerColumn[] // ValueBasedLayerColumn[]
): [
'textBased' | 'formBased',
PersistedIndexPatternLayer | TextBasedPersistedState['layers'][0] | undefined
] {
function buildValueLayer(config: LensBaseLayer): TextBasedPersistedState['layers'][0] {
const table = dataset as LensDatatableDataset;
const newLayer = {
table,
columns: getValueColumns(layer, i),
allColumns: table.columns.map(
(column) =>
({
fieldName: column.name,
columnId: column.id,
meta: column.meta,
} as TextBasedLayerColumn)
),
index: '',
query: undefined,
};
return newLayer;
}
function buildESQLLayer(config: LensBaseLayer): TextBasedPersistedState['layers'][0] {
const columns = getValueColumns(layer, i);
const newLayer = {
index: dataView!.id!,
query: { esql: (dataset as LensESQLDataset).esql } as AggregateQuery,
columns,
allColumns: columns,
};
return newLayer;
}
if ('esql' in dataset) {
return ['textBased', buildESQLLayer(layer)];
} else if ('type' in dataset) {
return ['textBased', buildValueLayer(layer)];
}
return ['formBased', buildFormulaLayers(layer, i, dataView!)];
}
export const buildDatasourceStates = async (
config: (LensBaseConfig & { layers: LensBaseLayer[] }) | (LensBaseLayer & LensBaseConfig),
dataviews: Record<string, DataView>,
buildFormulaLayers: (
config: unknown,
i: number,
dataView: DataView
) => PersistedIndexPatternLayer | undefined,
getValueColumns: (config: any, i: number) => TextBasedLayerColumn[],
dataViewsAPI: DataViewsPublicPluginStart
) => {
const layers: LensAttributes['state']['datasourceStates'] = {
textBased: { layers: {} },
formBased: { layers: {} },
};
const mainDataset = config.dataset;
const configLayers = 'layers' in config ? config.layers : [config];
for (let i = 0; i < configLayers.length; i++) {
const layer = configLayers[i];
const layerId = `layer_${i}`;
const dataset = layer.dataset || mainDataset;
if (!dataset && 'type' in layer && (layer as LensAnnotationLayer).type !== 'annotation') {
throw Error('dataset must be defined');
}
const index = getDatasetIndex(dataset);
const dataView = index
? await getDataView(index.index, dataViewsAPI, index.timeFieldName)
: undefined;
if (dataView) {
dataviews[layerId] = dataView;
}
if (dataset) {
const [type, layerConfig] = buildDatasourceStatesLayer(
layer,
i,
dataset,
dataView,
buildFormulaLayers,
getValueColumns
);
if (layerConfig) {
layers[type]!.layers[layerId] = layerConfig;
}
}
}
return layers;
};
export const addLayerColumn = (
layer: PersistedIndexPatternLayer,
columnName: string,
config: GenericIndexPatternColumn,
first = false
) => {
layer.columns = {
...layer.columns,
[columnName]: config,
};
if (first) {
layer.columnOrder.unshift(columnName);
} else {
layer.columnOrder.push(columnName);
}
};
export const addLayerFormulaColumns = (
layer: PersistedIndexPatternLayer,
columns: PersistedIndexPatternLayer,
postfix = ''
) => {
const altObj = Object.fromEntries(
Object.entries(columns.columns).map(([key, value]) =>
// Modify key here
[`${key}${postfix}`, value]
)
);
layer.columns = {
...layer.columns,
...altObj,
};
layer.columnOrder.push(...columns.columnOrder.map((c) => `${c}${postfix}`));
};

View file

@ -1,5 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/lens-embeddable-utils",
"owner": "@elastic/obs-ux-infra_services-team"
"owner": ["@elastic/obs-ux-infra_services-team", "@elastic/kibana-visualizations"]
}

View file

@ -11,6 +11,11 @@
"@kbn/data-plugin",
"@kbn/data-views-plugin",
"@kbn/lens-plugin",
"@kbn/maps-plugin",
"@kbn/event-annotation-common",
"@kbn/es-query",
"@kbn/expressions-plugin",
"@kbn/visualizations-plugin",
"@kbn/core-saved-objects-common",
]
}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { DatatableColumn } from '@kbn/expressions-plugin/public';
import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import type { VisualizeEditorContext } from '../../types';
@ -24,6 +24,7 @@ export interface TextBasedField {
export interface TextBasedLayer {
index: string;
query: AggregateQuery | undefined;
table?: Datatable;
columns: TextBasedLayerColumn[];
allColumns: TextBasedLayerColumn[];
timeField?: string;

View file

@ -46,6 +46,7 @@ export type { DatatableVisualizationState } from './visualizations/datatable/vis
export type { HeatmapVisualizationState } from './visualizations/heatmap/types';
export type { GaugeVisualizationState } from './visualizations/gauge/constants';
export type { MetricVisualizationState } from './visualizations/metric/types';
export type { TagcloudState } from './visualizations/tagcloud/types';
export type {
FormBasedPersistedState,
PersistedIndexPatternLayer,