mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
b20a3642a6
commit
8dcf1ad2ed
13 changed files with 204 additions and 32 deletions
|
@ -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',
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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})`;
|
||||
}
|
||||
|
|
|
@ -92,4 +92,8 @@ export const SUPPORTED_METRICS: { [key: string]: AggOptions } = {
|
|||
name: 'clamp',
|
||||
isFullReference: true,
|
||||
},
|
||||
static: {
|
||||
name: 'static_value',
|
||||
isFullReference: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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')
|
||||
) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue