mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
1022ccdf78
commit
11451b48b8
37 changed files with 3787 additions and 3 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
240
packages/kbn-lens-embeddable-utils/config_builder/charts/xy.ts
Normal file
240
packages/kbn-lens-embeddable-utils/config_builder/charts/xy.ts
Normal 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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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');
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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 },
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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: {},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 } } : {}),
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
10
packages/kbn-lens-embeddable-utils/config_builder/index.ts
Normal file
10
packages/kbn-lens-embeddable-utils/config_builder/index.ts
Normal 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';
|
274
packages/kbn-lens-embeddable-utils/config_builder/types.ts
Normal file
274
packages/kbn-lens-embeddable-utils/config_builder/types.ts
Normal 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>;
|
220
packages/kbn-lens-embeddable-utils/config_builder/utils.test.ts
Normal file
220
packages/kbn-lens-embeddable-utils/config_builder/utils.test.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
270
packages/kbn-lens-embeddable-utils/config_builder/utils.ts
Normal file
270
packages/kbn-lens-embeddable-utils/config_builder/utils.ts
Normal 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}`));
|
||||
};
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue