[TSVB2Lens] Supports static values (#126903)

* [TSVB2Lens] Support static values

* Fixes TSVB aggregations

* Adds unit tests

* Add more unit tests

* Cleanup
This commit is contained in:
Stratoula Kalafateli 2022-03-09 14:29:35 +02:00 committed by GitHub
parent b20a3642a6
commit 8dcf1ad2ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 204 additions and 32 deletions

View file

@ -17,7 +17,7 @@ describe('getSeries', () => {
field: 'day_of_week_i',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'average',
@ -44,7 +44,7 @@ describe('getSeries', () => {
},
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'formula',
@ -71,7 +71,7 @@ describe('getSeries', () => {
field: '123456',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'formula',
@ -97,7 +97,7 @@ describe('getSeries', () => {
field: '123456',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'formula',
@ -122,7 +122,7 @@ describe('getSeries', () => {
field: '123456',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'cumulative_sum',
@ -147,7 +147,7 @@ describe('getSeries', () => {
field: '123456',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'formula',
@ -174,7 +174,7 @@ describe('getSeries', () => {
unit: '1m',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'differences',
@ -202,7 +202,7 @@ describe('getSeries', () => {
window: 6,
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'moving_average',
@ -246,7 +246,7 @@ describe('getSeries', () => {
window: 6,
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'formula',
@ -293,7 +293,7 @@ describe('getSeries', () => {
],
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'percentile',
@ -335,7 +335,7 @@ describe('getSeries', () => {
order_by: 'timestamp',
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'last_value',
@ -357,10 +357,43 @@ describe('getSeries', () => {
size: 2,
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toBeNull();
});
test('should return null for a static aggregation with 1 layer', () => {
const metric = [
{
id: '12345',
type: 'static',
value: '10',
},
] as Metric[];
const config = getSeries(metric, 1);
expect(config).toBeNull();
});
test('should return the correct config for a static aggregation with 2 layers', () => {
const metric = [
{
id: '12345',
type: 'static',
value: '10',
},
] as Metric[];
const config = getSeries(metric, 2);
expect(config).toStrictEqual([
{
agg: 'static_value',
fieldName: 'document',
isFullReference: true,
params: {
value: '10',
},
},
]);
});
test('should return the correct formula for the math aggregation with percentiles as variables', () => {
const metric = [
{
@ -415,7 +448,7 @@ describe('getSeries', () => {
],
},
] as Metric[];
const config = getSeries(metric);
const config = getSeries(metric, 1);
expect(config).toStrictEqual([
{
agg: 'formula',

View file

@ -21,7 +21,10 @@ import {
getTimeScale,
} from './metrics_helpers';
export const getSeries = (metrics: Metric[]): VisualizeEditorLayersContext['metrics'] | null => {
export const getSeries = (
metrics: Metric[],
totalSeriesNum: number
): VisualizeEditorLayersContext['metrics'] | null => {
const metricIdx = metrics.length - 1;
const aggregation = metrics[metricIdx].type;
const fieldName = metrics[metricIdx].field;
@ -50,12 +53,10 @@ export const getSeries = (metrics: Metric[]): VisualizeEditorLayersContext['metr
const variables = metrics[mathMetricIdx].variables;
const layerMetricsArray = metrics;
if (!finalScript || !variables) return null;
const metricsWithoutMath = layerMetricsArray.filter((metric) => metric.type !== 'math');
// create the script
for (let layerMetricIdx = 0; layerMetricIdx < layerMetricsArray.length; layerMetricIdx++) {
if (layerMetricsArray[layerMetricIdx].type === 'math') {
continue;
}
for (let layerMetricIdx = 0; layerMetricIdx < metricsWithoutMath.length; layerMetricIdx++) {
const currentMetric = metrics[layerMetricIdx];
// We can only support top_hit with size 1
if (
@ -102,7 +103,7 @@ export const getSeries = (metrics: Metric[]): VisualizeEditorLayersContext['metr
// percentile value is derived from the field Id. It has the format xxx-xxx-xxx-xxx[percentile]
const [fieldId, meta] = metrics[metricIdx]?.field?.split('[') ?? [];
const subFunctionMetric = metrics.find((metric) => metric.id === fieldId);
if (!subFunctionMetric) {
if (!subFunctionMetric || subFunctionMetric.type === 'static') {
return null;
}
const pipelineAgg = getPipelineAgg(subFunctionMetric);
@ -184,6 +185,24 @@ export const getSeries = (metrics: Metric[]): VisualizeEditorLayersContext['metr
];
break;
}
case 'static': {
// Lens support reference lines only when at least one layer data exists
if (totalSeriesNum === 1) {
return null;
}
const staticValue = metrics[metricIdx].value;
metricsArray = [
{
agg: aggregationMap.name,
isFullReference: aggregationMap.isFullReference,
fieldName: 'document',
params: {
...(staticValue && { value: staticValue }),
},
},
];
break;
}
default: {
const timeScale = getTimeScale(metrics[metricIdx]);
metricsArray = [

View file

@ -37,6 +37,11 @@ export const triggerTSVBtoLensConfiguration = async (
return null;
}
const layersConfiguration: { [key: string]: VisualizeEditorLayersContext } = {};
// get the active series number
let seriesNum = 0;
model.series.forEach((series) => {
if (!series.hidden) seriesNum++;
});
// handle multiple layers/series
for (let layerIdx = 0; layerIdx < model.series.length; layerIdx++) {
@ -64,7 +69,7 @@ export const triggerTSVBtoLensConfiguration = async (
}
// handle multiple metrics
let metricsArray = getSeries(layer.metrics);
let metricsArray = getSeries(layer.metrics, seriesNum);
if (!metricsArray) {
return null;
}

View file

@ -91,7 +91,7 @@ export const getParentPipelineSeries = (
// percentile value is derived from the field Id. It has the format xxx-xxx-xxx-xxx[percentile]
const [fieldId, meta] = currentMetric?.field?.split('[') ?? [];
const subFunctionMetric = metrics.find((metric) => metric.id === fieldId);
if (!subFunctionMetric) {
if (!subFunctionMetric || subFunctionMetric.type === 'static') {
return null;
}
const pipelineAgg = getPipelineAgg(subFunctionMetric);
@ -184,7 +184,7 @@ export const getSiblingPipelineSeriesFormula = (
metrics: Metric[]
) => {
const subFunctionMetric = metrics.find((metric) => metric.id === currentMetric.field);
if (!subFunctionMetric) {
if (!subFunctionMetric || subFunctionMetric.type === 'static') {
return null;
}
const pipelineAggMap = SUPPORTED_METRICS[subFunctionMetric.type];
@ -311,6 +311,9 @@ export const getFormulaEquivalent = (
case 'filter_ratio': {
return getFilterRatioFormula(currentMetric);
}
case 'static': {
return `${currentMetric.value}`;
}
default: {
return `${aggregation}(${currentMetric.field})`;
}

View file

@ -92,4 +92,8 @@ export const SUPPORTED_METRICS: { [key: string]: AggOptions } = {
name: 'clamp',
isFullReference: true,
},
static: {
name: 'static_value',
isFullReference: true,
},
};

View file

@ -148,7 +148,8 @@ export function getSuggestions({
},
currentVisualizationState,
subVisualizationId,
palette
palette,
visualizeTriggerFieldContext && 'isVisualizeAction' in visualizeTriggerFieldContext
);
});
})
@ -205,7 +206,8 @@ function getVisualizationSuggestions(
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
currentVisualizationState: unknown,
subVisualizationId?: string,
mainPalette?: PaletteOutput
mainPalette?: PaletteOutput,
isFromContext?: boolean
) {
return visualization
.getSuggestions({
@ -214,6 +216,7 @@ function getVisualizationSuggestions(
keptLayerIds: datasourceSuggestion.keptLayerIds,
subVisualizationId,
mainPalette,
isFromContext,
})
.map(({ state, ...visualizationSuggestion }) => ({
...visualizationSuggestion,

View file

@ -1835,6 +1835,61 @@ describe('IndexPattern Data Source suggestions', () => {
})
);
});
it('should apply a static layer if it is provided', () => {
const updatedContext = [
{
...context[0],
metrics: [
{
agg: 'static_value',
isFullReference: true,
fieldName: 'document',
params: {
value: '10',
},
color: '#68BC00',
},
],
},
];
const suggestions = getDatasourceSuggestionsForVisualizeCharts(
stateWithoutLayer(),
updatedContext
);
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
columnOrder: ['id2'],
columns: {
id2: expect.objectContaining({
operationType: 'static_value',
isStaticValue: true,
params: expect.objectContaining({
value: '10',
}),
}),
},
}),
},
}),
table: {
changeType: 'initial',
label: undefined,
isMultiRow: false,
columns: [
expect.objectContaining({
columnId: 'id2',
}),
],
layerId: 'id1',
},
})
);
});
});
describe('#getDatasourceSuggestionsForVisualizeField', () => {

View file

@ -203,6 +203,10 @@ function createNewTimeseriesLayerWithMetricAggregationFromVizEditor(
layer.format,
layer.label
);
// static values layers do not need a date histogram column
if (Object.values(computedLayer.columns)[0].isStaticValue) {
return computedLayer;
}
return insertNewColumn({
op: 'date_histogram',

View file

@ -228,15 +228,12 @@ export function insertNewColumn({
const possibleOperation = operationDefinition.getPossibleOperation();
const isBucketed = Boolean(possibleOperation?.isBucketed);
const addOperationFn = isBucketed ? addBucket : addMetric;
const buildColumnFn = columnParams
? operationDefinition.buildColumn({ ...baseOptions, layer }, columnParams)
: operationDefinition.buildColumn({ ...baseOptions, layer });
return updateDefaultLabels(
addOperationFn(
layer,
operationDefinition.buildColumn({ ...baseOptions, layer }),
columnId,
visualizationGroups,
targetGroup
),
addOperationFn(layer, buildColumnFn, columnId, visualizationGroups, targetGroup),
indexPattern
);
}

View file

@ -634,6 +634,7 @@ export interface SuggestionRequest<T = unknown> {
*/
state?: T;
mainPalette?: PaletteOutput;
isFromContext?: boolean;
/**
* The visualization needs to know which table is being suggested
*/

View file

@ -435,6 +435,49 @@ describe('xy_visualization', () => {
type: 'palette',
});
});
it('sets the context configuration correctly for reference lines', () => {
const newContext = {
...context,
metrics: [
{
agg: 'static_value',
fieldName: 'document',
isFullReference: true,
color: '#68BC00',
params: {
value: '10',
},
},
],
};
const state = xyVisualization?.updateLayersConfigurationFromContext?.({
prevState: {
...exampleState(),
layers: [
{
layerId: 'first',
layerType: layerTypes.DATA,
seriesType: 'line',
xAccessor: undefined,
accessors: ['a'],
},
],
},
layerId: 'first',
context: newContext,
});
expect(state?.layers[0]).toHaveProperty('seriesType', 'area');
expect(state?.layers[0]).toHaveProperty('layerType', 'referenceLine');
expect(state?.layers[0].yConfig).toStrictEqual([
{
axisMode: 'right',
color: '#68BC00',
forAccessor: 'a',
fill: 'below',
},
]);
});
});
describe('#getVisualizationSuggestionFromContext', () => {

View file

@ -15,6 +15,7 @@ import { FieldFormatsStart } from 'src/plugins/field_formats/public';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import type { FillStyle } from '../../common/expressions/xy_chart';
import { getSuggestions } from './xy_suggestions';
import { XyToolbar } from './xy_config_panel';
import { DimensionEditor } from './xy_config_panel/dimension_editor';
@ -290,12 +291,14 @@ export const getXyVisualization = ({
if (!foundLayer) {
return prevState;
}
const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value');
const axisMode = axisPosition as YAxisMode;
const yConfig = metrics.map((metric, idx) => {
return {
color: metric.color,
forAccessor: metric.accessor ?? foundLayer.accessors[idx],
...(axisMode && { axisMode }),
...(isReferenceLine && { fill: chartType === 'area' ? 'below' : ('none' as FillStyle) }),
};
});
const newLayer = {
@ -303,6 +306,7 @@ export const getXyVisualization = ({
...(chartType && { seriesType: chartType as SeriesType }),
...(palette && { palette }),
yConfig,
layerType: isReferenceLine ? layerTypes.REFERENCELINE : layerTypes.DATA,
};
const newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l));

View file

@ -45,6 +45,7 @@ export function getSuggestions({
keptLayerIds,
subVisualizationId,
mainPalette,
isFromContext,
}: SuggestionRequest<State>): Array<VisualizationSuggestion<State>> {
const incompleteTable =
!table.isMultiRow ||
@ -72,7 +73,7 @@ export function getSuggestions({
if (
(incompleteTable && state && !subVisualizationId) ||
table.columns.some((col) => col.operation.isStaticValue) ||
table.columns.some((col) => col.operation.isStaticValue && !isFromContext) ||
// do not use suggestions with non-numeric metrics
table.columns.some((col) => !col.operation.isBucketed && col.operation.dataType !== 'number')
) {