mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens][TSVB] Ad-hoc dataViews for index pattern string mode in TSVB. (#143500)
## Summary Completes part of https://github.com/elastic/kibana/issues/138236. Added support of ad-hoc dataViews while converting TSVB visualizations, when index pattern string mode is turned on. Co-authored-by: Uladzislau Lasitsa <vlad.lasitsa@gmail.com> Co-authored-by: Joe Reuter <johannes.reuter@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
452b81f0e7
commit
3d7b01e28b
26 changed files with 1019 additions and 712 deletions
|
@ -13,7 +13,7 @@ import type { Panel, IndexPatternValue, FetchedIndexPattern } from './types';
|
|||
|
||||
export const isStringTypeIndexPattern = (
|
||||
indexPatternValue: IndexPatternValue
|
||||
): indexPatternValue is string => typeof indexPatternValue === 'string';
|
||||
): indexPatternValue is string => typeof indexPatternValue === 'string' && indexPatternValue !== '';
|
||||
|
||||
export const isDataViewTypeIndexPattern = (
|
||||
indexPatternValue: IndexPatternValue
|
||||
|
|
|
@ -6,13 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFieldText, EuiFieldTextProps } from '@elastic/eui';
|
||||
import { EuiFieldText, EuiFieldTextProps, EuiButtonIcon } from '@elastic/eui';
|
||||
import { SwitchModePopover } from './switch_mode_popover';
|
||||
|
||||
import type { SelectIndexComponentProps } from './types';
|
||||
|
||||
const updateIndexText = i18n.translate('visTypeTimeseries.indexPatternSelect.updateIndex', {
|
||||
defaultMessage: 'Update visualization with entered data view',
|
||||
});
|
||||
|
||||
export const FieldTextSelect = ({
|
||||
fetchedIndex,
|
||||
onIndexChange,
|
||||
|
@ -35,15 +38,30 @@ export const FieldTextSelect = ({
|
|||
}
|
||||
}, [indexPatternString, inputValue]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if ((inputValue ?? '') !== (indexPatternString ?? '')) {
|
||||
onIndexChange(inputValue);
|
||||
}
|
||||
},
|
||||
150,
|
||||
[inputValue, onIndexChange]
|
||||
);
|
||||
const updateIndex = useCallback(() => {
|
||||
if ((inputValue ?? '') !== (indexPatternString ?? '')) {
|
||||
onIndexChange(inputValue);
|
||||
}
|
||||
}, [onIndexChange, inputValue, indexPatternString]);
|
||||
|
||||
const appends = [
|
||||
<EuiButtonIcon
|
||||
aria-label={updateIndexText}
|
||||
iconType="play"
|
||||
onClick={updateIndex}
|
||||
disabled={inputValue === indexPatternString}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (allowSwitchMode) {
|
||||
appends.push(
|
||||
<SwitchModePopover
|
||||
onModeChange={onModeChange}
|
||||
fetchedIndex={fetchedIndex}
|
||||
useKibanaIndices={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFieldText
|
||||
|
@ -52,15 +70,7 @@ export const FieldTextSelect = ({
|
|||
value={inputValue ?? ''}
|
||||
placeholder={placeholder}
|
||||
data-test-subj={dataTestSubj}
|
||||
{...(allowSwitchMode && {
|
||||
append: (
|
||||
<SwitchModePopover
|
||||
onModeChange={onModeChange}
|
||||
fetchedIndex={fetchedIndex}
|
||||
useKibanaIndices={false}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
append={appends}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ const mockIsValidMetrics = jest.fn();
|
|||
const mockGetDatasourceValue = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(stubLogstashDataView));
|
||||
const mockGetDataSourceInfo = jest.fn();
|
||||
const mockExtractOrGenerateDatasourceInfo = jest.fn();
|
||||
const mockGetSeriesAgg = jest.fn();
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
|
@ -50,7 +50,7 @@ jest.mock('../lib/metrics', () => {
|
|||
});
|
||||
|
||||
jest.mock('../lib/datasource', () => ({
|
||||
getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()),
|
||||
extractOrGenerateDatasourceInfo: jest.fn(() => mockExtractOrGenerateDatasourceInfo()),
|
||||
}));
|
||||
|
||||
describe('convertToLens', () => {
|
||||
|
@ -77,7 +77,7 @@ describe('convertToLens', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockIsValidMetrics.mockReturnValue(true);
|
||||
mockGetDataSourceInfo.mockReturnValue({
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPatternId: 'test-index-pattern',
|
||||
timeField: 'timeField',
|
||||
indexPattern: { id: 'test-index-pattern' },
|
||||
|
@ -126,7 +126,7 @@ describe('convertToLens', () => {
|
|||
},
|
||||
} as Vis<Panel>);
|
||||
expect(result).toBeNull();
|
||||
expect(mockGetDataSourceInfo).toBeCalledTimes(0);
|
||||
expect(mockExtractOrGenerateDatasourceInfo).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should return null if only series agg is specified', async () => {
|
||||
|
@ -177,4 +177,31 @@ describe('convertToLens', () => {
|
|||
expect(result).toBeDefined();
|
||||
expect(result?.type).toBe('lnsMetric');
|
||||
});
|
||||
|
||||
test('should drop adhoc dataviews if action is required', async () => {
|
||||
mockGetMetricsColumns.mockReturnValue([metricColumn]);
|
||||
mockGetSeriesAgg.mockReturnValue({ metrics: [metric] });
|
||||
mockGetConfigurationForGauge.mockReturnValue({});
|
||||
|
||||
const result = await convertToLens(
|
||||
{
|
||||
params: createPanel({
|
||||
series: [
|
||||
createSeries({
|
||||
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
|
||||
hidden: false,
|
||||
}),
|
||||
createSeries({
|
||||
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
|
||||
hidden: false,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
} as Vis<Panel>,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.type).toBe('lnsMetric');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { PANEL_TYPES, TSVB_METRIC_TYPES } from '../../../common/enums';
|
||||
import { Metric } from '../../../common/types';
|
||||
import { getDataViewsStart } from '../../services';
|
||||
import { getDataSourceInfo } from '../lib/datasource';
|
||||
import { extractOrGenerateDatasourceInfo } from '../lib/datasource';
|
||||
import { getMetricsColumns, getBucketsColumns } from '../lib/series';
|
||||
import { getConfigurationForGauge as getConfiguration } from '../lib/configurations/metric';
|
||||
import {
|
||||
|
@ -45,98 +45,107 @@ const getMaxFormula = (metric: Metric, column?: Column) => {
|
|||
}))`;
|
||||
};
|
||||
|
||||
const invalidModelError = () => new Error('Invalid model');
|
||||
|
||||
export const convertToLens: ConvertTsvbToLensVisualization = async (
|
||||
{ params: model },
|
||||
timeRange
|
||||
) => {
|
||||
const dataViews = getDataViewsStart();
|
||||
try {
|
||||
const series = model.series[0];
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const series = model.series[0];
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.GAUGE, series.time_range_mode)) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (series.metrics[series.metrics.length - 1].type === TSVB_METRIC_TYPES.STATIC) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
|
||||
if (!datasourceInfo) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, model.series.length, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (metricsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, false);
|
||||
|
||||
if (bucketsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const [bucket] = bucketsColumns;
|
||||
|
||||
const extendedLayer: ExtendedLayer = {
|
||||
indexPatternId,
|
||||
layerId: uuid(),
|
||||
columns: [...metricsColumns, ...(bucket ? [bucket] : [])],
|
||||
columnOrder: [],
|
||||
};
|
||||
|
||||
const primarySeries = model.series[0];
|
||||
const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries);
|
||||
|
||||
if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const primaryColumn = findMetricColumn(
|
||||
primaryMetricWithCollapseFn.metric,
|
||||
extendedLayer.columns
|
||||
);
|
||||
if (!primaryColumn) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
let gaugeMaxColumn: StaticValueColumn | FormulaColumn | null = createFormulaColumnWithoutMeta(
|
||||
getMaxFormula(primaryMetricWithCollapseFn.metric, primaryColumn)
|
||||
);
|
||||
if (model.gauge_max !== undefined && model.gauge_max !== '') {
|
||||
gaugeMaxColumn = createStaticValueColumn(model.gauge_max);
|
||||
}
|
||||
|
||||
const layer = {
|
||||
...extendedLayer,
|
||||
columns: [...extendedLayer.columns, gaugeMaxColumn],
|
||||
};
|
||||
const configuration = getConfiguration(model, layer, bucket, gaugeMaxColumn ?? undefined);
|
||||
if (!configuration) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const layers = Object.values(excludeMetaFromLayers({ 0: layer }));
|
||||
|
||||
return {
|
||||
type: 'lnsMetric',
|
||||
layers,
|
||||
configuration,
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.GAUGE, series.time_range_mode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (series.metrics[series.metrics.length - 1].type === TSVB_METRIC_TYPES.STATIC) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
|
||||
if (!datasourceInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, model.series.length, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (metricsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, false);
|
||||
|
||||
if (bucketsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [bucket] = bucketsColumns;
|
||||
|
||||
const extendedLayer: ExtendedLayer = {
|
||||
indexPatternId,
|
||||
layerId: uuid(),
|
||||
columns: [...metricsColumns, ...(bucket ? [bucket] : [])],
|
||||
columnOrder: [],
|
||||
};
|
||||
|
||||
const primarySeries = model.series[0];
|
||||
const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries);
|
||||
|
||||
if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryColumn = findMetricColumn(primaryMetricWithCollapseFn.metric, extendedLayer.columns);
|
||||
if (!primaryColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let gaugeMaxColumn: StaticValueColumn | FormulaColumn | null = createFormulaColumnWithoutMeta(
|
||||
getMaxFormula(primaryMetricWithCollapseFn.metric, primaryColumn)
|
||||
);
|
||||
if (model.gauge_max !== undefined && model.gauge_max !== '') {
|
||||
gaugeMaxColumn = createStaticValueColumn(model.gauge_max);
|
||||
}
|
||||
|
||||
const layer = {
|
||||
...extendedLayer,
|
||||
columns: [...extendedLayer.columns, gaugeMaxColumn],
|
||||
};
|
||||
const configuration = getConfiguration(model, layer, bucket, gaugeMaxColumn ?? undefined);
|
||||
if (!configuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layers = Object.values(excludeMetaFromLayers({ 0: layer }));
|
||||
return {
|
||||
type: 'lnsMetric',
|
||||
layers,
|
||||
configuration,
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -48,15 +48,4 @@ describe('convertTSVBtoLensConfiguration', () => {
|
|||
} as Vis<Panel>);
|
||||
expect(triggerOptions).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null for a string index pattern', async () => {
|
||||
const stringIndexPatternModel = {
|
||||
...model,
|
||||
use_kibana_indexes: false,
|
||||
};
|
||||
const triggerOptions = await convertTSVBtoLensConfiguration({
|
||||
params: stringIndexPatternModel,
|
||||
} as Vis<Panel>);
|
||||
expect(triggerOptions).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,10 +45,6 @@ const getConvertFnByType = (type: PANEL_TYPES) => {
|
|||
* In case of null, the menu item is disabled and the user can't navigate to Lens.
|
||||
*/
|
||||
export const convertTSVBtoLensConfiguration = async (vis: Vis<Panel>, timeRange?: TimeRange) => {
|
||||
// Disables the option for not supported charts, for the string mode and for series with annotations
|
||||
if (!vis.params.use_kibana_indexes) {
|
||||
return null;
|
||||
}
|
||||
// Disables if model is invalid
|
||||
if (vis.params.isModelInvalid) {
|
||||
return null;
|
||||
|
|
|
@ -20,10 +20,16 @@ import { createPanel, createSeries } from '../../__mocks__';
|
|||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
const mockExtractOrGenerateDatasourceInfo = jest.fn();
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => 'test-id',
|
||||
}));
|
||||
|
||||
jest.mock('../../datasource', () => ({
|
||||
extractOrGenerateDatasourceInfo: jest.fn(() => mockExtractOrGenerateDatasourceInfo()),
|
||||
}));
|
||||
|
||||
const mockedIndices = [
|
||||
{
|
||||
id: 'test',
|
||||
|
@ -31,6 +37,12 @@ const mockedIndices = [
|
|||
timeFieldName: 'test_field',
|
||||
getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }),
|
||||
},
|
||||
{
|
||||
id: 'test2',
|
||||
title: 'test2',
|
||||
timeFieldName: 'test_field',
|
||||
getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }),
|
||||
},
|
||||
] as unknown as DataView[];
|
||||
|
||||
const indexPatternsService = {
|
||||
|
@ -356,6 +368,14 @@ describe('getLayers', () => {
|
|||
],
|
||||
series: [createSeries({ metrics: staticValueMetric })],
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPattern: mockedIndices[0],
|
||||
indexPatternId: mockedIndices[0].id,
|
||||
timeField: mockedIndices[0].timeFieldName,
|
||||
});
|
||||
});
|
||||
|
||||
test.each<
|
||||
[
|
||||
|
@ -568,9 +588,31 @@ describe('getLayers', () => {
|
|||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'multiple annotations with different data views create separate layers',
|
||||
[dataSourceLayersWithStatic, panelWithMultiAnnotations, indexPatternsService, false],
|
||||
])('should return %s', async (_, input, expected) => {
|
||||
const layers = await getLayers(...input);
|
||||
expect(layers).toEqual(expected.map(expect.objectContaining));
|
||||
});
|
||||
|
||||
test('should return multiple annotations with different data views create separate layers', async () => {
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValueOnce({
|
||||
indexPattern: mockedIndices[0],
|
||||
indexPatternId: mockedIndices[0].id,
|
||||
timeField: mockedIndices[0].timeFieldName,
|
||||
});
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValueOnce({
|
||||
indexPattern: mockedIndices[1],
|
||||
indexPatternId: mockedIndices[1].id,
|
||||
timeField: mockedIndices[1].timeFieldName,
|
||||
});
|
||||
|
||||
const layers = await getLayers(
|
||||
dataSourceLayersWithStatic,
|
||||
panelWithMultiAnnotations,
|
||||
indexPatternsService,
|
||||
false
|
||||
);
|
||||
|
||||
expect(layers).toEqual(
|
||||
[
|
||||
{
|
||||
layerType: 'referenceLine',
|
||||
|
@ -634,7 +676,7 @@ describe('getLayers', () => {
|
|||
type: 'query',
|
||||
},
|
||||
],
|
||||
indexPatternId: 'test',
|
||||
indexPatternId: 'test2',
|
||||
},
|
||||
{
|
||||
layerId: 'test-id',
|
||||
|
@ -659,18 +701,28 @@ describe('getLayers', () => {
|
|||
type: 'query',
|
||||
},
|
||||
],
|
||||
indexPatternId: 'test2',
|
||||
indexPatternId: 'test',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'annotation layer gets correct dataView when none is defined',
|
||||
[
|
||||
dataSourceLayersWithStatic,
|
||||
panelWithSingleAnnotationDefaultDataView,
|
||||
indexPatternsService,
|
||||
false,
|
||||
],
|
||||
].map(expect.objectContaining)
|
||||
);
|
||||
expect(mockExtractOrGenerateDatasourceInfo).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should return annotation layer gets correct dataView when none is defined', async () => {
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPattern: { ...mockedIndices[0], id: 'default' },
|
||||
indexPatternId: 'default',
|
||||
timeField: mockedIndices[0].timeFieldName,
|
||||
});
|
||||
|
||||
const layers = await getLayers(
|
||||
dataSourceLayersWithStatic,
|
||||
panelWithSingleAnnotationDefaultDataView,
|
||||
indexPatternsService,
|
||||
false
|
||||
);
|
||||
|
||||
expect(layers).toEqual(
|
||||
[
|
||||
{
|
||||
layerType: 'referenceLine',
|
||||
|
@ -712,10 +764,8 @@ describe('getLayers', () => {
|
|||
],
|
||||
indexPatternId: 'default',
|
||||
},
|
||||
],
|
||||
],
|
||||
])('should return %s', async (_, input, expected) => {
|
||||
const layers = await getLayers(...input);
|
||||
expect(layers).toEqual(expected.map(expect.objectContaining));
|
||||
].map(expect.objectContaining)
|
||||
);
|
||||
expect(mockExtractOrGenerateDatasourceInfo).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,6 @@ import { euiLightVars } from '@kbn/ui-theme';
|
|||
import { groupBy } from 'lodash';
|
||||
import { DataViewsPublicPluginStart, DataView } from '@kbn/data-plugin/public/data_views';
|
||||
import { getDefaultQueryLanguage } from '../../../../application/components/lib/get_default_query_language';
|
||||
import { fetchIndexPattern } from '../../../../../common/index_patterns_utils';
|
||||
import { ICON_TYPES_MAP } from '../../../../application/visualizations/constants';
|
||||
import { SUPPORTED_METRICS } from '../../metrics';
|
||||
import type { Annotation, Metric, Panel, Series } from '../../../../../common/types';
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
AnyColumnWithReferences,
|
||||
} from '../../convert';
|
||||
import { getChartType } from './chart_type';
|
||||
import { extractOrGenerateDatasourceInfo } from '../../datasource';
|
||||
|
||||
export const isColumnWithReference = (column: Column): column is AnyColumnWithReferences =>
|
||||
Boolean((column as AnyColumnWithReferences).references);
|
||||
|
@ -144,22 +144,32 @@ export const getLayers = async (
|
|||
}
|
||||
|
||||
const annotationsByIndexPatternAndIgnoreFlag = groupBy(model.annotations, (a) => {
|
||||
const id = typeof a.index_pattern === 'object' && 'id' in a.index_pattern && a.index_pattern.id;
|
||||
return `${id}-${Boolean(a.ignore_global_filters)}`;
|
||||
const id =
|
||||
typeof a.index_pattern === 'object' && 'id' in a.index_pattern
|
||||
? a.index_pattern.id
|
||||
: a.index_pattern;
|
||||
return `${id}-${a.time_field ?? ''}-${Boolean(a.ignore_global_filters)}`;
|
||||
});
|
||||
|
||||
try {
|
||||
const annotationsLayers: Array<XYAnnotationsLayerConfig | undefined> = await Promise.all(
|
||||
Object.values(annotationsByIndexPatternAndIgnoreFlag).map(async (annotations) => {
|
||||
const [firstAnnotation] = annotations;
|
||||
const indexPatternId =
|
||||
typeof firstAnnotation.index_pattern === 'string'
|
||||
? firstAnnotation.index_pattern
|
||||
: firstAnnotation.index_pattern?.id;
|
||||
const convertedAnnotations: EventAnnotationConfig[] = [];
|
||||
const { indexPattern } =
|
||||
(await fetchIndexPattern(indexPatternId && { id: indexPatternId }, dataViews)) || {};
|
||||
|
||||
const result = await extractOrGenerateDatasourceInfo(
|
||||
firstAnnotation.index_pattern,
|
||||
firstAnnotation.time_field,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Invalid annotation datasource');
|
||||
}
|
||||
const { indexPattern } = result;
|
||||
if (indexPattern) {
|
||||
annotations.forEach((a: Annotation) => {
|
||||
const lensAnnotation = convertAnnotation(a, indexPattern);
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import { extractOrGenerateDatasourceInfo } from './datasource_info';
|
||||
|
||||
const dataViewsMap: Record<string, DataView> = {
|
||||
test1: { id: 'test1', title: 'test1', timeFieldName: 'timeField1' } as DataView,
|
||||
test2: {
|
||||
id: 'test2',
|
||||
title: 'test2',
|
||||
timeFieldName: 'timeField2',
|
||||
} as DataView,
|
||||
test3: {
|
||||
id: 'test3',
|
||||
title: 'test3',
|
||||
timeFieldName: 'timeField3',
|
||||
name: 'index-pattern-3',
|
||||
} as DataView,
|
||||
};
|
||||
|
||||
const mockCreateDataView = jest.fn();
|
||||
|
||||
jest.mock('../../../../common/index_patterns_utils', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/index_patterns_utils');
|
||||
return {
|
||||
isStringTypeIndexPattern: originalModule.isStringTypeIndexPattern,
|
||||
};
|
||||
});
|
||||
|
||||
const getDataview = async (id: string): Promise<DataView | undefined> => dataViewsMap[id];
|
||||
|
||||
describe('extractOrGenerateDatasourceInfo', () => {
|
||||
let dataViews: DataViewsPublicPluginStart;
|
||||
beforeAll(() => {
|
||||
dataViews = {
|
||||
getDefault: jest.fn(async () => {
|
||||
return { id: '12345', title: 'default', timeFieldName: '@timestamp' };
|
||||
}),
|
||||
get: getDataview,
|
||||
create: mockCreateDataView,
|
||||
} as unknown as DataViewsPublicPluginStart;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreateDataView.mockReturnValue(getDataview('test3'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return ad-hoc dataview if model_indexpattern is string and no corresponding dataview found by string', async () => {
|
||||
const timeFieldName = 'timeField-3';
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
'test',
|
||||
timeFieldName,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField, indexPattern } = datasourceInfo!;
|
||||
expect(indexPatternId).toBe(dataViewsMap.test3.id);
|
||||
expect(timeField).toBe(dataViewsMap.test3.timeFieldName);
|
||||
expect(indexPattern).toBe(dataViewsMap.test3);
|
||||
});
|
||||
|
||||
test('should return dataview if model_indexpattern is string and corresponding dataview is found by string', async () => {
|
||||
const timeFieldName = 'timeField-3';
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
dataViewsMap.test3.name,
|
||||
timeFieldName,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField, indexPattern } = datasourceInfo!;
|
||||
expect(indexPatternId).toBe(dataViewsMap.test3.id);
|
||||
expect(timeField).toBe(dataViewsMap.test3.timeFieldName);
|
||||
expect(indexPattern).toBe(dataViewsMap.test3);
|
||||
});
|
||||
|
||||
test('should return the correct dataview if model_indexpattern is object', async () => {
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
{ id: 'dataview-1-id' },
|
||||
'timeField-1',
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField } = datasourceInfo!;
|
||||
|
||||
expect(indexPatternId).toBe('dataview-1-id');
|
||||
expect(timeField).toBe('timeField-1');
|
||||
});
|
||||
|
||||
test('should fetch the correct data if overwritten dataview is provided', async () => {
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
{ id: 'dataview-1-id' },
|
||||
'timeField-1',
|
||||
true,
|
||||
{ id: 'test2' },
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField } = datasourceInfo!;
|
||||
|
||||
expect(indexPatternId).toBe('test2');
|
||||
expect(timeField).toBe(dataViewsMap.test2.timeFieldName);
|
||||
});
|
||||
|
||||
test('should return the correct dataview if overwritten dataview is string', async () => {
|
||||
mockCreateDataView.mockReturnValue(dataViewsMap.test2);
|
||||
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
{ id: 'dataview-1-id' },
|
||||
'timeField-1',
|
||||
true,
|
||||
'test2',
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField } = datasourceInfo!;
|
||||
|
||||
expect(indexPatternId).toBe('test2');
|
||||
expect(timeField).toBe('timeField2');
|
||||
});
|
||||
|
||||
test('should return null if dataview is string and invalid', async () => {
|
||||
mockCreateDataView.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
'dataview-1-i',
|
||||
'timeField-1',
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
|
||||
expect(datasourceInfo).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null if overritten dataview is string and invalid', async () => {
|
||||
mockCreateDataView.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
{ id: 'dataview-1-id' },
|
||||
'timeField-1',
|
||||
true,
|
||||
'test',
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
|
||||
expect(datasourceInfo).toBeNull();
|
||||
});
|
||||
});
|
|
@ -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 { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { isStringTypeIndexPattern } from '../../../../common/index_patterns_utils';
|
||||
import type { IndexPatternValue } from '../../../../common/types';
|
||||
|
||||
const getOverwrittenIndexPattern = async (
|
||||
overwrittenIndexPattern: IndexPatternValue,
|
||||
overwrittenTimeField: string | undefined,
|
||||
dataViews: DataViewsPublicPluginStart
|
||||
) => {
|
||||
if (isStringTypeIndexPattern(overwrittenIndexPattern)) {
|
||||
const indexPattern = await dataViews.create(
|
||||
{
|
||||
id: `tsvb_ad_hoc_${overwrittenIndexPattern}${
|
||||
overwrittenTimeField ? '/' + overwrittenTimeField : ''
|
||||
}`,
|
||||
title: overwrittenIndexPattern,
|
||||
timeFieldName: overwrittenTimeField,
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
const indexPatternId = indexPattern.id ?? '';
|
||||
const timeField = indexPattern.timeFieldName;
|
||||
return { indexPattern, indexPatternId, timeField };
|
||||
} else if (overwrittenIndexPattern) {
|
||||
const indexPattern = await dataViews.get(overwrittenIndexPattern.id);
|
||||
if (indexPattern) {
|
||||
const indexPatternId = indexPattern.id ?? '';
|
||||
const timeField = overwrittenTimeField ?? indexPattern.timeFieldName;
|
||||
return { indexPattern, indexPatternId, timeField };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getSelectedIndexPattern = async (
|
||||
selectedIndexPattern: IndexPatternValue,
|
||||
selectedTimeField: string | undefined,
|
||||
dataViews: DataViewsPublicPluginStart
|
||||
) => {
|
||||
if (isStringTypeIndexPattern(selectedIndexPattern)) {
|
||||
if (!selectedTimeField) {
|
||||
throw new Error('Time field is empty');
|
||||
}
|
||||
const indexPattern = await dataViews.create(
|
||||
{
|
||||
id: `tsvb_ad_hoc_${selectedIndexPattern}${
|
||||
selectedTimeField ? '/' + selectedTimeField : ''
|
||||
}`,
|
||||
title: selectedIndexPattern,
|
||||
timeFieldName: selectedTimeField,
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
const indexPatternId = indexPattern.id ?? '';
|
||||
return { indexPattern, indexPatternId, timeField: indexPattern.timeFieldName };
|
||||
}
|
||||
const indexPattern = await dataViews.getDefault();
|
||||
const indexPatternId = indexPattern?.id ?? '';
|
||||
const timeField = indexPattern?.timeFieldName;
|
||||
return { indexPattern, indexPatternId, timeField };
|
||||
};
|
||||
|
||||
export const extractOrGenerateDatasourceInfo = async (
|
||||
currentIndexPattern: IndexPatternValue,
|
||||
currentTimeField: string | undefined,
|
||||
isOverwritten: boolean,
|
||||
overwrittenIndexPattern: IndexPatternValue | undefined,
|
||||
overwrittenTimeField: string | undefined,
|
||||
dataViews: DataViewsPublicPluginStart
|
||||
) => {
|
||||
try {
|
||||
let indexPatternId =
|
||||
currentIndexPattern && !isStringTypeIndexPattern(currentIndexPattern)
|
||||
? currentIndexPattern.id
|
||||
: '';
|
||||
|
||||
let timeField = currentTimeField;
|
||||
let indexPattern: DataView | null | undefined;
|
||||
// handle override index pattern
|
||||
if (isOverwritten) {
|
||||
const result = await getOverwrittenIndexPattern(
|
||||
overwrittenIndexPattern,
|
||||
overwrittenTimeField,
|
||||
dataViews
|
||||
);
|
||||
if (result) {
|
||||
[indexPattern, indexPatternId, timeField] = [
|
||||
result.indexPattern,
|
||||
result.indexPatternId,
|
||||
result.timeField,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!indexPatternId) {
|
||||
const result = await getSelectedIndexPattern(
|
||||
currentIndexPattern,
|
||||
currentTimeField,
|
||||
dataViews
|
||||
);
|
||||
[indexPattern, indexPatternId, timeField] = [
|
||||
result.indexPattern,
|
||||
result.indexPatternId,
|
||||
result.timeField,
|
||||
];
|
||||
} else {
|
||||
indexPattern = await dataViews.get(indexPatternId);
|
||||
if (!timeField) {
|
||||
timeField = indexPattern.timeFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
return { indexPatternId, timeField, indexPattern };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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 { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import { getDataSourceInfo } from './get_datasource_info';
|
||||
|
||||
const dataViewsMap: Record<string, DataView> = {
|
||||
test1: { id: 'test1', title: 'test1', timeFieldName: 'timeField1' } as DataView,
|
||||
test2: {
|
||||
id: 'test2',
|
||||
title: 'test2',
|
||||
timeFieldName: 'timeField2',
|
||||
} as DataView,
|
||||
test3: { id: 'test3', title: 'test3', timeFieldName: 'timeField3' } as DataView,
|
||||
};
|
||||
|
||||
const getDataview = async (id: string): Promise<DataView | undefined> => dataViewsMap[id];
|
||||
|
||||
describe('getDataSourceInfo', () => {
|
||||
let dataViews: DataViewsPublicPluginStart;
|
||||
beforeAll(() => {
|
||||
dataViews = {
|
||||
getDefault: jest.fn(async () => {
|
||||
return { id: '12345', title: 'default', timeFieldName: '@timestamp' };
|
||||
}),
|
||||
get: getDataview,
|
||||
} as unknown as DataViewsPublicPluginStart;
|
||||
});
|
||||
|
||||
test('should return the default dataview if model_indexpattern is string', async () => {
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
'test',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField } = datasourceInfo!;
|
||||
expect(indexPatternId).toBe('12345');
|
||||
expect(timeField).toBe('@timestamp');
|
||||
});
|
||||
|
||||
test('should return the correct dataview if model_indexpattern is object', async () => {
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
{ id: 'dataview-1-id' },
|
||||
'timeField-1',
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField } = datasourceInfo!;
|
||||
|
||||
expect(indexPatternId).toBe('dataview-1-id');
|
||||
expect(timeField).toBe('timeField-1');
|
||||
});
|
||||
|
||||
test('should fetch the correct data if overwritten dataview is provided', async () => {
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
{ id: 'dataview-1-id' },
|
||||
'timeField-1',
|
||||
true,
|
||||
{ id: 'test2' },
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
const { indexPatternId, timeField } = datasourceInfo!;
|
||||
|
||||
expect(indexPatternId).toBe('test2');
|
||||
expect(timeField).toBe('timeField2');
|
||||
});
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* 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 { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
fetchIndexPattern,
|
||||
isStringTypeIndexPattern,
|
||||
} from '../../../../common/index_patterns_utils';
|
||||
import type { IndexPatternValue } from '../../../../common/types';
|
||||
|
||||
export const getDataSourceInfo = async (
|
||||
modelIndexPattern: IndexPatternValue,
|
||||
modelTimeField: string | undefined,
|
||||
isOverwritten: boolean,
|
||||
overwrittenIndexPattern: IndexPatternValue | undefined,
|
||||
seriesTimeField: string | undefined,
|
||||
dataViews: DataViewsPublicPluginStart
|
||||
) => {
|
||||
try {
|
||||
let indexPatternId =
|
||||
modelIndexPattern && !isStringTypeIndexPattern(modelIndexPattern) ? modelIndexPattern.id : '';
|
||||
|
||||
let timeField = modelTimeField;
|
||||
let indexPattern: DataView | null | undefined;
|
||||
// handle override index pattern
|
||||
if (isOverwritten) {
|
||||
const fetchedIndexPattern = await fetchIndexPattern(overwrittenIndexPattern, dataViews);
|
||||
indexPattern = fetchedIndexPattern.indexPattern;
|
||||
|
||||
if (indexPattern) {
|
||||
indexPatternId = indexPattern.id ?? '';
|
||||
timeField = seriesTimeField ?? indexPattern.timeFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!indexPatternId) {
|
||||
indexPattern = await dataViews.getDefault();
|
||||
indexPatternId = indexPattern?.id ?? '';
|
||||
timeField = indexPattern?.timeFieldName;
|
||||
} else {
|
||||
indexPattern = await dataViews.get(indexPatternId);
|
||||
if (!timeField) {
|
||||
timeField = indexPattern.timeFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
indexPatternId,
|
||||
timeField,
|
||||
indexPattern,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './get_datasource_info';
|
||||
export * from './datasource_info';
|
||||
|
|
|
@ -20,7 +20,7 @@ const mockIsValidMetrics = jest.fn();
|
|||
const mockGetDatasourceValue = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(stubLogstashDataView));
|
||||
const mockGetDataSourceInfo = jest.fn();
|
||||
const mockExtractOrGenerateDatasourceInfo = jest.fn();
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
getDataViewsStart: jest.fn(() => mockGetDatasourceValue),
|
||||
|
@ -41,7 +41,7 @@ jest.mock('../lib/metrics', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../lib/datasource', () => ({
|
||||
getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()),
|
||||
extractOrGenerateDatasourceInfo: jest.fn(() => mockExtractOrGenerateDatasourceInfo()),
|
||||
}));
|
||||
|
||||
describe('convertToLens', () => {
|
||||
|
@ -125,7 +125,7 @@ describe('convertToLens', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockIsValidMetrics.mockReturnValue(true);
|
||||
mockGetDataSourceInfo.mockReturnValue({
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPatternId: 'test-index-pattern',
|
||||
timeField: 'timeField',
|
||||
indexPattern: { id: 'test-index-pattern' },
|
||||
|
@ -168,6 +168,14 @@ describe('convertToLens', () => {
|
|||
expect(mockGetConfigurationForMetric).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should drop adhoc dataviews if action is required', async () => {
|
||||
const result = await convertToLens(vis, undefined, true);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.type).toBe('lnsMetric');
|
||||
expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length);
|
||||
expect(mockGetConfigurationForMetric).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should skip hidden series', async () => {
|
||||
const result = await convertToLens({
|
||||
params: createPanel({
|
||||
|
@ -185,7 +193,7 @@ describe('convertToLens', () => {
|
|||
});
|
||||
|
||||
test('should return null if multiple indexPatterns are provided', async () => {
|
||||
mockGetDataSourceInfo.mockReturnValueOnce({
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValueOnce({
|
||||
indexPatternId: 'test-index-pattern-1',
|
||||
timeField: 'timeField',
|
||||
indexPattern: { id: 'test-index-pattern-1' },
|
||||
|
|
|
@ -11,7 +11,7 @@ import { DataView, parseTimeShift } from '@kbn/data-plugin/common';
|
|||
import { getIndexPatternIds } from '@kbn/visualizations-plugin/common/convert_to_lens';
|
||||
import { PANEL_TYPES } from '../../../common/enums';
|
||||
import { getDataViewsStart } from '../../services';
|
||||
import { getDataSourceInfo } from '../lib/datasource';
|
||||
import { extractOrGenerateDatasourceInfo } from '../lib/datasource';
|
||||
import { getMetricsColumns, getBucketsColumns } from '../lib/series';
|
||||
import { getConfigurationForMetric as getConfiguration } from '../lib/configurations/metric';
|
||||
import { getReducedTimeRange, isValidMetrics } from '../lib/metrics';
|
||||
|
@ -22,113 +22,121 @@ import { excludeMetaFromLayers, getUniqueBuckets } from '../utils';
|
|||
const MAX_SERIES = 2;
|
||||
const MAX_BUCKETS = 2;
|
||||
|
||||
const invalidModelError = () => new Error('Invalid model');
|
||||
|
||||
export const convertToLens: ConvertTsvbToLensVisualization = async (
|
||||
{ params: model },
|
||||
timeRange
|
||||
) => {
|
||||
const dataViews = getDataViewsStart();
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
try {
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
|
||||
const indexPatternIds = new Set();
|
||||
// we should get max only 2 series
|
||||
const visibleSeries = model.series.filter(({ hidden }) => !hidden).slice(0, 2);
|
||||
let currentIndexPattern: DataView | null = null;
|
||||
for (const series of visibleSeries) {
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
const indexPatternIds = new Set();
|
||||
// we should get max only 2 series
|
||||
const visibleSeries = model.series.filter(({ hidden }) => !hidden).slice(0, 2);
|
||||
let currentIndexPattern: DataView | null = null;
|
||||
for (const series of visibleSeries) {
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
|
||||
if (!datasourceInfo) {
|
||||
return null;
|
||||
if (!datasourceInfo) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
|
||||
indexPatternIds.add(indexPatternId);
|
||||
currentIndexPattern = indexPattern;
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
indexPatternIds.add(indexPatternId);
|
||||
currentIndexPattern = indexPattern;
|
||||
}
|
||||
if (indexPatternIds.size > 1) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (indexPatternIds.size > 1) {
|
||||
const [indexPatternId] = indexPatternIds.values();
|
||||
|
||||
const buckets = [];
|
||||
const metrics = [];
|
||||
|
||||
// handle multiple layers/series
|
||||
for (const series of visibleSeries) {
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.METRIC, series.time_range_mode)) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, currentIndexPattern!, seriesNum, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (metricsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(
|
||||
model,
|
||||
series,
|
||||
metricsColumns,
|
||||
currentIndexPattern!,
|
||||
false
|
||||
);
|
||||
|
||||
if (bucketsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
buckets.push(...bucketsColumns);
|
||||
metrics.push(...metricsColumns);
|
||||
}
|
||||
|
||||
let uniqueBuckets = buckets;
|
||||
if (visibleSeries.length === MAX_SERIES && buckets.length) {
|
||||
if (buckets.length !== MAX_BUCKETS) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
uniqueBuckets = getUniqueBuckets(buckets as ColumnsWithoutMeta[]);
|
||||
if (uniqueBuckets.length !== 1) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
}
|
||||
|
||||
const [bucket] = uniqueBuckets;
|
||||
|
||||
const extendedLayer: ExtendedLayer = {
|
||||
indexPatternId: indexPatternId as string,
|
||||
layerId: uuid(),
|
||||
columns: [...metrics, ...(bucket ? [bucket] : [])],
|
||||
columnOrder: [],
|
||||
};
|
||||
|
||||
const configuration = getConfiguration(model, extendedLayer, bucket);
|
||||
if (!configuration) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer }));
|
||||
|
||||
return {
|
||||
type: 'lnsMetric',
|
||||
layers,
|
||||
configuration,
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [indexPatternId] = indexPatternIds.values();
|
||||
|
||||
const buckets = [];
|
||||
const metrics = [];
|
||||
|
||||
// handle multiple layers/series
|
||||
for (const series of visibleSeries) {
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.METRIC, series.time_range_mode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, currentIndexPattern!, seriesNum, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (metricsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(
|
||||
model,
|
||||
series,
|
||||
metricsColumns,
|
||||
currentIndexPattern!,
|
||||
false
|
||||
);
|
||||
|
||||
if (bucketsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
buckets.push(...bucketsColumns);
|
||||
metrics.push(...metricsColumns);
|
||||
}
|
||||
|
||||
let uniqueBuckets = buckets;
|
||||
if (visibleSeries.length === MAX_SERIES && buckets.length) {
|
||||
if (buckets.length !== MAX_BUCKETS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
uniqueBuckets = getUniqueBuckets(buckets as ColumnsWithoutMeta[]);
|
||||
if (uniqueBuckets.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const [bucket] = uniqueBuckets;
|
||||
|
||||
const extendedLayer: ExtendedLayer = {
|
||||
indexPatternId: indexPatternId as string,
|
||||
layerId: uuid(),
|
||||
columns: [...metrics, ...(bucket ? [bucket] : [])],
|
||||
columnOrder: [],
|
||||
};
|
||||
|
||||
const configuration = getConfiguration(model, extendedLayer, bucket);
|
||||
if (!configuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer }));
|
||||
return {
|
||||
type: 'lnsMetric',
|
||||
layers,
|
||||
configuration,
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -23,7 +23,7 @@ const mockIsValidMetrics = jest.fn();
|
|||
const mockGetDatasourceValue = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(stubLogstashDataView));
|
||||
const mockGetDataSourceInfo = jest.fn();
|
||||
const mockExtractOrGenerateDatasourceInfo = jest.fn();
|
||||
const mockGetColumnState = jest.fn();
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
|
@ -49,7 +49,7 @@ jest.mock('../lib/metrics', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../lib/datasource', () => ({
|
||||
getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()),
|
||||
extractOrGenerateDatasourceInfo: jest.fn(() => mockExtractOrGenerateDatasourceInfo()),
|
||||
}));
|
||||
|
||||
describe('convertToLens', () => {
|
||||
|
@ -73,7 +73,7 @@ describe('convertToLens', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockIsValidMetrics.mockReturnValue(true);
|
||||
mockGetDataSourceInfo.mockReturnValue({
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPatternId: 'test-index-pattern',
|
||||
timeField: 'timeField',
|
||||
indexPattern: { id: 'test-index-pattern' },
|
||||
|
|
|
@ -12,7 +12,7 @@ import { getIndexPatternIds, Layer } from '@kbn/visualizations-plugin/common/con
|
|||
import { PANEL_TYPES } from '../../../common/enums';
|
||||
import { getDataViewsStart } from '../../services';
|
||||
import { getColumnState } from '../lib/configurations/table';
|
||||
import { getDataSourceInfo } from '../lib/datasource';
|
||||
import { extractOrGenerateDatasourceInfo } from '../lib/datasource';
|
||||
import { getMetricsColumns, getBucketsColumns } from '../lib/series';
|
||||
import { getReducedTimeRange, isValidMetrics } from '../lib/metrics';
|
||||
import { ConvertTsvbToLensVisualization } from '../types';
|
||||
|
@ -28,156 +28,163 @@ const excludeMetaFromLayers = (layers: Record<string, ExtendedLayer>): Record<st
|
|||
return newLayers;
|
||||
};
|
||||
|
||||
const invalidModelError = () => new Error('Invalid model');
|
||||
|
||||
export const convertToLens: ConvertTsvbToLensVisualization = async (
|
||||
{ params: model, uiState },
|
||||
timeRange
|
||||
) => {
|
||||
const columnStates = [];
|
||||
const dataViews = getDataViewsStart();
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
const sortConfig = uiState.get('table')?.sort ?? {};
|
||||
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
try {
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
const sortConfig = uiState.get('table')?.sort ?? {};
|
||||
|
||||
if (!datasourceInfo) {
|
||||
return null;
|
||||
}
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
dataViews
|
||||
);
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
|
||||
const commonBucketsColumns = getBucketsColumns(
|
||||
undefined,
|
||||
{
|
||||
split_mode: 'terms',
|
||||
terms_field: model.pivot_id,
|
||||
terms_size: model.pivot_rows ? model.pivot_rows.toString() : undefined,
|
||||
},
|
||||
[],
|
||||
indexPattern!,
|
||||
false,
|
||||
model.pivot_label,
|
||||
false
|
||||
);
|
||||
|
||||
if (!commonBucketsColumns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortConfiguration = {
|
||||
columnId: commonBucketsColumns[0].columnId,
|
||||
direction: sortConfig.order,
|
||||
};
|
||||
|
||||
columnStates.push(getColumnState(commonBucketsColumns[0].columnId));
|
||||
|
||||
let bucketsColumns: Column[] | null = [];
|
||||
|
||||
if (
|
||||
!model.series.every(
|
||||
(s) =>
|
||||
((!s.aggregate_by && !model.series[0].aggregate_by) ||
|
||||
s.aggregate_by === model.series[0].aggregate_by) &&
|
||||
((!s.aggregate_function && !model.series[0].aggregate_function) ||
|
||||
s.aggregate_function === model.series[0].aggregate_function)
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (model.series[0].aggregate_by) {
|
||||
if (
|
||||
!model.series[0].aggregate_function ||
|
||||
!['sum', 'mean', 'min', 'max'].includes(model.series[0].aggregate_function)
|
||||
) {
|
||||
return null;
|
||||
if (!datasourceInfo) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
bucketsColumns = getBucketsColumns(
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
|
||||
const commonBucketsColumns = getBucketsColumns(
|
||||
undefined,
|
||||
{
|
||||
split_mode: 'terms',
|
||||
terms_field: model.series[0].aggregate_by,
|
||||
terms_field: model.pivot_id,
|
||||
terms_size: model.pivot_rows ? model.pivot_rows.toString() : undefined,
|
||||
},
|
||||
[],
|
||||
indexPattern!,
|
||||
false,
|
||||
model.pivot_label,
|
||||
false
|
||||
);
|
||||
if (bucketsColumns === null) {
|
||||
return null;
|
||||
|
||||
if (!commonBucketsColumns) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
columnStates.push(
|
||||
getColumnState(
|
||||
bucketsColumns[0].columnId,
|
||||
model.series[0].aggregate_function === 'mean' ? 'avg' : model.series[0].aggregate_function
|
||||
const sortConfiguration = {
|
||||
columnId: commonBucketsColumns[0].columnId,
|
||||
direction: sortConfig.order,
|
||||
};
|
||||
|
||||
columnStates.push(getColumnState(commonBucketsColumns[0].columnId));
|
||||
|
||||
let bucketsColumns: Column[] | null = [];
|
||||
|
||||
if (
|
||||
!model.series.every(
|
||||
(s) =>
|
||||
((!s.aggregate_by && !model.series[0].aggregate_by) ||
|
||||
s.aggregate_by === model.series[0].aggregate_by) &&
|
||||
((!s.aggregate_function && !model.series[0].aggregate_function) ||
|
||||
s.aggregate_function === model.series[0].aggregate_function)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const metrics = [];
|
||||
|
||||
// handle multiple layers/series
|
||||
for (const [_, series] of model.series.entries()) {
|
||||
if (series.hidden) {
|
||||
continue;
|
||||
) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
return null;
|
||||
if (model.series[0].aggregate_by) {
|
||||
if (
|
||||
!model.series[0].aggregate_function ||
|
||||
!['sum', 'mean', 'min', 'max'].includes(model.series[0].aggregate_function)
|
||||
) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
bucketsColumns = getBucketsColumns(
|
||||
undefined,
|
||||
{
|
||||
split_mode: 'terms',
|
||||
terms_field: model.series[0].aggregate_by,
|
||||
},
|
||||
[],
|
||||
indexPattern!,
|
||||
false
|
||||
);
|
||||
if (bucketsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
columnStates.push(
|
||||
getColumnState(
|
||||
bucketsColumns[0].columnId,
|
||||
model.series[0].aggregate_function === 'mean' ? 'avg' : model.series[0].aggregate_function
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.TABLE, series.time_range_mode)) {
|
||||
return null;
|
||||
const metrics = [];
|
||||
|
||||
// handle multiple layers/series
|
||||
for (const [_, series] of model.series.entries()) {
|
||||
if (series.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.TABLE, series.time_range_mode)) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (!metricsColumns) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
columnStates.push(getColumnState(metricsColumns[0].columnId, undefined, series));
|
||||
|
||||
if (sortConfig.column === series.id) {
|
||||
sortConfiguration.columnId = metricsColumns[0].columnId;
|
||||
}
|
||||
|
||||
metrics.push(...metricsColumns);
|
||||
}
|
||||
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (!metricsColumns) {
|
||||
return null;
|
||||
if (!metrics.length || metrics.every((metric) => metric.operationType === 'static_value')) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
columnStates.push(getColumnState(metricsColumns[0].columnId, undefined, series));
|
||||
const extendedLayer: ExtendedLayer = {
|
||||
indexPatternId: indexPatternId as string,
|
||||
layerId: uuid(),
|
||||
columns: [...metrics, ...commonBucketsColumns, ...bucketsColumns],
|
||||
columnOrder: [],
|
||||
};
|
||||
|
||||
if (sortConfig.column === series.id) {
|
||||
sortConfiguration.columnId = metricsColumns[0].columnId;
|
||||
}
|
||||
const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer }));
|
||||
|
||||
metrics.push(...metricsColumns);
|
||||
}
|
||||
|
||||
if (!metrics.length || metrics.every((metric) => metric.operationType === 'static_value')) {
|
||||
return {
|
||||
type: 'lnsDatatable',
|
||||
layers,
|
||||
configuration: {
|
||||
columns: columnStates,
|
||||
layerId: extendedLayer.layerId,
|
||||
layerType: 'data',
|
||||
sorting: sortConfiguration,
|
||||
},
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const extendedLayer: ExtendedLayer = {
|
||||
indexPatternId: indexPatternId as string,
|
||||
layerId: uuid(),
|
||||
columns: [...metrics, ...commonBucketsColumns, ...bucketsColumns],
|
||||
columnOrder: [],
|
||||
};
|
||||
|
||||
const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer }));
|
||||
|
||||
return {
|
||||
type: 'lnsDatatable',
|
||||
layers,
|
||||
configuration: {
|
||||
columns: columnStates,
|
||||
layerId: extendedLayer.layerId,
|
||||
layerType: 'data',
|
||||
sorting: sortConfiguration,
|
||||
},
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ const mockIsValidMetrics = jest.fn();
|
|||
const mockGetDatasourceValue = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(stubLogstashDataView));
|
||||
const mockGetDataSourceInfo = jest.fn();
|
||||
const mockExtractOrGenerateDatasourceInfo = jest.fn();
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
getDataViewsStart: jest.fn(() => mockGetDatasourceValue),
|
||||
|
@ -47,7 +47,7 @@ jest.mock('../lib/metrics', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../lib/datasource', () => ({
|
||||
getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()),
|
||||
extractOrGenerateDatasourceInfo: jest.fn(() => mockExtractOrGenerateDatasourceInfo()),
|
||||
}));
|
||||
|
||||
describe('convertToLens', () => {
|
||||
|
@ -68,7 +68,7 @@ describe('convertToLens', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockIsValidMetrics.mockReturnValue(true);
|
||||
mockGetDataSourceInfo.mockReturnValue({
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPatternId: 'test-index-pattern',
|
||||
timeField: 'timeField',
|
||||
indexPattern: { id: 'test-index-pattern' },
|
||||
|
@ -91,10 +91,10 @@ describe('convertToLens', () => {
|
|||
});
|
||||
|
||||
test('should return null for empty time field', async () => {
|
||||
mockGetDataSourceInfo.mockReturnValue({ timeField: null });
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({ timeField: null });
|
||||
const result = await convertToLens(vis);
|
||||
expect(result).toBeNull();
|
||||
expect(mockGetDataSourceInfo).toBeCalledTimes(1);
|
||||
expect(mockExtractOrGenerateDatasourceInfo).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should return null for invalid date histogram', async () => {
|
||||
|
@ -139,6 +139,14 @@ describe('convertToLens', () => {
|
|||
expect(mockGetConfigurationForTimeseries).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should drop adhoc dataviews if action is required', async () => {
|
||||
const result = await convertToLens(vis, undefined, true);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.type).toBe('lnsXY');
|
||||
expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length);
|
||||
expect(mockGetConfigurationForTimeseries).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should skip hidden series', async () => {
|
||||
const result = await convertToLens({
|
||||
params: createPanel({
|
||||
|
|
|
@ -16,7 +16,7 @@ import uuid from 'uuid';
|
|||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { PANEL_TYPES } from '../../../common/enums';
|
||||
import { getDataViewsStart } from '../../services';
|
||||
import { getDataSourceInfo } from '../lib/datasource';
|
||||
import { extractOrGenerateDatasourceInfo } from '../lib/datasource';
|
||||
import { getMetricsColumns, getBucketsColumns } from '../lib/series';
|
||||
import {
|
||||
getConfigurationForTimeseries as getConfiguration,
|
||||
|
@ -40,100 +40,107 @@ const excludeMetaFromLayers = (layers: Record<string, ExtendedLayer>): Record<st
|
|||
return newLayers;
|
||||
};
|
||||
|
||||
const invalidModelError = () => new Error('Invalid model');
|
||||
|
||||
export const convertToLens: ConvertTsvbToLensVisualization = async ({ params: model }) => {
|
||||
const dataViews: DataViewsPublicPluginStart = getDataViewsStart();
|
||||
const extendedLayers: Record<number, ExtendedLayer> = {};
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
try {
|
||||
const extendedLayers: Record<number, ExtendedLayer> = {};
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
|
||||
// handle multiple layers/series
|
||||
for (const [layerIdx, series] of model.series.entries()) {
|
||||
if (series.hidden) {
|
||||
continue;
|
||||
// handle multiple layers/series
|
||||
for (const [layerIdx, series] of model.series.entries()) {
|
||||
if (series.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.TIMESERIES)) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
if (!datasourceInfo) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern, timeField } = datasourceInfo;
|
||||
|
||||
if (!timeField) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const dateHistogramColumn = convertToDateHistogramColumn(model, series, indexPattern!, {
|
||||
fieldName: timeField,
|
||||
isSplit: false,
|
||||
});
|
||||
if (dateHistogramColumn === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
|
||||
isStaticValueColumnSupported: true,
|
||||
});
|
||||
if (metricsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, true);
|
||||
if (bucketsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const isReferenceLine =
|
||||
metricsColumns.length === 1 && metricsColumns[0].operationType === 'static_value';
|
||||
|
||||
// only static value without split is supported
|
||||
if (isReferenceLine && bucketsColumns.length) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const layerId = uuid();
|
||||
extendedLayers[layerIdx] = {
|
||||
indexPatternId,
|
||||
layerId,
|
||||
columns: isReferenceLine
|
||||
? [...metricsColumns]
|
||||
: [...metricsColumns, dateHistogramColumn, ...bucketsColumns],
|
||||
columnOrder: [],
|
||||
};
|
||||
}
|
||||
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
return null;
|
||||
const configLayers = await getLayers(extendedLayers, model, dataViews);
|
||||
if (configLayers === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.TIMESERIES)) {
|
||||
return null;
|
||||
}
|
||||
const configuration = getConfiguration(model, configLayers);
|
||||
const layers = Object.values(excludeMetaFromLayers(extendedLayers));
|
||||
const annotationIndexPatterns = configuration.layers.reduce<string[]>((acc, layer) => {
|
||||
if (isAnnotationsLayer(layer)) {
|
||||
return [...acc, layer.indexPatternId];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
if (!datasourceInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern, timeField } = datasourceInfo;
|
||||
if (!timeField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateHistogramColumn = convertToDateHistogramColumn(model, series, indexPattern!, {
|
||||
fieldName: timeField,
|
||||
isSplit: false,
|
||||
});
|
||||
if (dateHistogramColumn === null) {
|
||||
return null;
|
||||
}
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
|
||||
isStaticValueColumnSupported: true,
|
||||
});
|
||||
if (metricsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, true);
|
||||
if (bucketsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isReferenceLine =
|
||||
metricsColumns.length === 1 && metricsColumns[0].operationType === 'static_value';
|
||||
|
||||
// only static value without split is supported
|
||||
if (isReferenceLine && bucketsColumns.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layerId = uuid();
|
||||
extendedLayers[layerIdx] = {
|
||||
indexPatternId,
|
||||
layerId,
|
||||
columns: isReferenceLine
|
||||
? [...metricsColumns]
|
||||
: [...metricsColumns, dateHistogramColumn, ...bucketsColumns],
|
||||
columnOrder: [],
|
||||
return {
|
||||
type: 'lnsXY',
|
||||
layers,
|
||||
configuration,
|
||||
indexPatternIds: [...getIndexPatternIds(layers), ...annotationIndexPatterns],
|
||||
};
|
||||
}
|
||||
|
||||
const configLayers = await getLayers(extendedLayers, model, dataViews);
|
||||
if (configLayers === null) {
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configuration = getConfiguration(model, configLayers);
|
||||
const layers = Object.values(excludeMetaFromLayers(extendedLayers));
|
||||
const annotationIndexPatterns = configuration.layers.reduce<string[]>((acc, layer) => {
|
||||
if (isAnnotationsLayer(layer)) {
|
||||
return [...acc, layer.indexPatternId];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
type: 'lnsXY',
|
||||
layers,
|
||||
configuration,
|
||||
indexPatternIds: [...getIndexPatternIds(layers), ...annotationIndexPatterns],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ const mockIsValidMetrics = jest.fn();
|
|||
const mockGetDatasourceValue = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(stubLogstashDataView));
|
||||
const mockGetDataSourceInfo = jest.fn();
|
||||
const mockExtractOrGenerateDatasourceInfo = jest.fn();
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
getDataViewsStart: jest.fn(() => mockGetDatasourceValue),
|
||||
|
@ -46,7 +46,7 @@ jest.mock('../lib/metrics', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../lib/datasource', () => ({
|
||||
getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()),
|
||||
extractOrGenerateDatasourceInfo: jest.fn(() => mockExtractOrGenerateDatasourceInfo()),
|
||||
}));
|
||||
|
||||
describe('convertToLens', () => {
|
||||
|
@ -67,7 +67,7 @@ describe('convertToLens', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockIsValidMetrics.mockReturnValue(true);
|
||||
mockGetDataSourceInfo.mockReturnValue({
|
||||
mockExtractOrGenerateDatasourceInfo.mockReturnValue({
|
||||
indexPatternId: 'test-index-pattern',
|
||||
timeField: 'timeField',
|
||||
indexPattern: { id: 'test-index-pattern' },
|
||||
|
@ -110,6 +110,14 @@ describe('convertToLens', () => {
|
|||
expect(mockGetConfigurationForTopN).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should drop adhoc dataviews if action is required', async () => {
|
||||
const result = await convertToLens(vis, undefined, true);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.type).toBe('lnsXY');
|
||||
expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length);
|
||||
expect(mockGetConfigurationForTopN).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should skip hidden series', async () => {
|
||||
const result = await convertToLens({
|
||||
params: createPanel({
|
||||
|
|
|
@ -11,7 +11,7 @@ import { parseTimeShift } from '@kbn/data-plugin/common';
|
|||
import { getIndexPatternIds, Layer } from '@kbn/visualizations-plugin/common/convert_to_lens';
|
||||
import { PANEL_TYPES } from '../../../common/enums';
|
||||
import { getDataViewsStart } from '../../services';
|
||||
import { getDataSourceInfo } from '../lib/datasource';
|
||||
import { extractOrGenerateDatasourceInfo } from '../lib/datasource';
|
||||
import { getMetricsColumns, getBucketsColumns } from '../lib/series';
|
||||
import { getConfigurationForTopN as getConfiguration, getLayers } from '../lib/configurations/xy';
|
||||
import { getReducedTimeRange, isValidMetrics } from '../lib/metrics';
|
||||
|
@ -28,77 +28,84 @@ const excludeMetaFromLayers = (layers: Record<string, ExtendedLayer>): Record<st
|
|||
return newLayers;
|
||||
};
|
||||
|
||||
const invalidModelError = () => new Error('Invalid model');
|
||||
|
||||
export const convertToLens: ConvertTsvbToLensVisualization = async (
|
||||
{ params: model },
|
||||
timeRange
|
||||
) => {
|
||||
const dataViews = getDataViewsStart();
|
||||
const extendedLayers: Record<number, ExtendedLayer> = {};
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
try {
|
||||
const extendedLayers: Record<number, ExtendedLayer> = {};
|
||||
const seriesNum = model.series.filter((series) => !series.hidden).length;
|
||||
|
||||
// handle multiple layers/series
|
||||
for (const [layerIdx, series] of model.series.entries()) {
|
||||
if (series.hidden) {
|
||||
continue;
|
||||
// handle multiple layers/series
|
||||
for (const [layerIdx, series] of model.series.entries()) {
|
||||
if (series.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.TOP_N, series.time_range_mode)) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const datasourceInfo = await extractOrGenerateDatasourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
|
||||
if (!datasourceInfo) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (!metricsColumns) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, false);
|
||||
if (bucketsColumns === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
const layerId = uuid();
|
||||
extendedLayers[layerIdx] = {
|
||||
indexPatternId,
|
||||
layerId,
|
||||
columns: [...metricsColumns, ...bucketsColumns],
|
||||
columnOrder: [],
|
||||
};
|
||||
}
|
||||
|
||||
// not valid time shift
|
||||
if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') {
|
||||
return null;
|
||||
const configLayers = await getLayers(extendedLayers, model, dataViews, true);
|
||||
if (configLayers === null) {
|
||||
throw invalidModelError();
|
||||
}
|
||||
|
||||
if (!isValidMetrics(series.metrics, PANEL_TYPES.TOP_N, series.time_range_mode)) {
|
||||
return null;
|
||||
}
|
||||
const layers = Object.values(excludeMetaFromLayers(extendedLayers));
|
||||
|
||||
const datasourceInfo = await getDataSourceInfo(
|
||||
model.index_pattern,
|
||||
model.time_field,
|
||||
Boolean(series.override_index_pattern),
|
||||
series.series_index_pattern,
|
||||
series.series_time_field,
|
||||
dataViews
|
||||
);
|
||||
|
||||
if (!datasourceInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { indexPatternId, indexPattern } = datasourceInfo;
|
||||
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
|
||||
|
||||
// handle multiple metrics
|
||||
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
|
||||
reducedTimeRange,
|
||||
});
|
||||
if (!metricsColumns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, false);
|
||||
if (bucketsColumns === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layerId = uuid();
|
||||
extendedLayers[layerIdx] = {
|
||||
indexPatternId,
|
||||
layerId,
|
||||
columns: [...metricsColumns, ...bucketsColumns],
|
||||
columnOrder: [],
|
||||
return {
|
||||
type: 'lnsXY',
|
||||
layers,
|
||||
configuration: getConfiguration(model, configLayers),
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
}
|
||||
|
||||
const configLayers = await getLayers(extendedLayers, model, dataViews, true);
|
||||
if (configLayers === null) {
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layers = Object.values(excludeMetaFromLayers(extendedLayers));
|
||||
return {
|
||||
type: 'lnsXY',
|
||||
layers,
|
||||
configuration: getConfiguration(model, configLayers),
|
||||
indexPatternIds: getIndexPatternIds(layers),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -18,7 +18,8 @@ import type { Panel } from '../../common/types';
|
|||
|
||||
export type ConvertTsvbToLensVisualization = (
|
||||
vis: Vis<Panel>,
|
||||
timeRange?: TimeRange
|
||||
timeRange?: TimeRange,
|
||||
clearAdHocDataViews?: boolean
|
||||
) => Promise<NavigateToLensContext<
|
||||
XYConfiguration | MetricVisConfiguration | TableVisConfiguration
|
||||
> | null>;
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-
|
|||
import { keyBy } from 'lodash';
|
||||
import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types';
|
||||
import { documentField } from '../datasources/form_based/document_field';
|
||||
import { sortDataViewRefs } from '../utils';
|
||||
|
||||
type ErrorHandler = (err: Error) => void;
|
||||
type MinimalDataViewsContract = Pick<DataViewsContract, 'get' | 'getIdsWithTitle' | 'create'>;
|
||||
|
@ -111,34 +112,10 @@ export function convertDataViewIntoLensIndexPattern(
|
|||
}
|
||||
|
||||
export async function loadIndexPatternRefs(
|
||||
dataViews: MinimalDataViewsContract,
|
||||
adHocDataViews?: Record<string, DataViewSpec>,
|
||||
contextDataViewSpec?: DataViewSpec
|
||||
dataViews: MinimalDataViewsContract
|
||||
): Promise<IndexPatternRef[]> {
|
||||
const indexPatterns = await dataViews.getIdsWithTitle();
|
||||
const missedIndexPatterns = Object.values(adHocDataViews || {});
|
||||
|
||||
// add data view from context
|
||||
if (contextDataViewSpec) {
|
||||
const existingDataView = indexPatterns.find(
|
||||
(indexPattern) => indexPattern.id === contextDataViewSpec.id
|
||||
);
|
||||
if (!existingDataView) {
|
||||
missedIndexPatterns.push(contextDataViewSpec);
|
||||
}
|
||||
}
|
||||
|
||||
return indexPatterns
|
||||
.concat(
|
||||
missedIndexPatterns.map((dataViewSpec) => ({
|
||||
id: dataViewSpec.id!,
|
||||
name: dataViewSpec.name,
|
||||
title: dataViewSpec.title!,
|
||||
}))
|
||||
)
|
||||
.sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
const indexPatternsRefs = await dataViews.getIdsWithTitle();
|
||||
return sortDataViewRefs(indexPatternsRefs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -109,9 +109,7 @@ function getUsedIndexPatterns({
|
|||
const indexPatternIds = [];
|
||||
if (initialContext) {
|
||||
if ('isVisualizeAction' in initialContext) {
|
||||
for (const { indexPatternId } of initialContext.layers) {
|
||||
indexPatternIds.push(indexPatternId);
|
||||
}
|
||||
indexPatternIds.push(...initialContext.indexPatternIds);
|
||||
} else {
|
||||
indexPatternIds.push(initialContext.dataViewSpec.id!);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
import { buildExpression } from './expression_helpers';
|
||||
import { showMemoizedErrorNotification } from '../../lens_ui_errors';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { getActiveDatasourceIdFromDoc } from '../../utils';
|
||||
import { getActiveDatasourceIdFromDoc, sortDataViewRefs } from '../../utils';
|
||||
import type { ErrorMessage } from '../types';
|
||||
import {
|
||||
getMissingCurrentDatasource,
|
||||
|
@ -81,6 +81,20 @@ const getLastUsedIndexPatternId = (
|
|||
return indexPattern && indexPatternRefs.find((i) => i.id === indexPattern)?.id;
|
||||
};
|
||||
|
||||
const getRefsForAdHocDataViewsFromContext = (
|
||||
indexPatternRefs: IndexPatternRef[],
|
||||
usedIndexPatternsIds: string[],
|
||||
indexPatterns: Record<string, IndexPattern>
|
||||
) => {
|
||||
const indexPatternIds = indexPatternRefs.map(({ id }) => id);
|
||||
const adHocDataViewsIds = usedIndexPatternsIds.filter((id) => !indexPatternIds.includes(id));
|
||||
|
||||
const adHocDataViewsList = Object.values(indexPatterns).filter(({ id }) =>
|
||||
adHocDataViewsIds.includes(id)
|
||||
);
|
||||
return adHocDataViewsList.map(({ id, title, name }) => ({ id, title, name }));
|
||||
};
|
||||
|
||||
export async function initializeDataViews(
|
||||
{
|
||||
dataViews,
|
||||
|
@ -110,10 +124,10 @@ export async function initializeDataViews(
|
|||
})
|
||||
);
|
||||
const { isFullEditor } = options ?? {};
|
||||
const contextDataViewSpec = (initialContext as VisualizeFieldContext)?.dataViewSpec;
|
||||
|
||||
// make it explicit or TS will infer never[] and break few lines down
|
||||
const indexPatternRefs: IndexPatternRef[] = await (isFullEditor
|
||||
? loadIndexPatternRefs(dataViews, adHocDataViews, contextDataViewSpec)
|
||||
? loadIndexPatternRefs(dataViews)
|
||||
: []);
|
||||
|
||||
// if no state is available, use the fallbackId
|
||||
|
@ -127,7 +141,7 @@ export async function initializeDataViews(
|
|||
|
||||
const adHocDataviewsIds: string[] = Object.keys(adHocDataViews || {});
|
||||
|
||||
const usedIndexPatterns = getIndexPatterns(
|
||||
const usedIndexPatternsIds = getIndexPatterns(
|
||||
references,
|
||||
initialContext,
|
||||
initialId,
|
||||
|
@ -137,17 +151,25 @@ export async function initializeDataViews(
|
|||
// load them
|
||||
const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id));
|
||||
|
||||
const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedIndexPatterns);
|
||||
const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedIndexPatternsIds);
|
||||
|
||||
const indexPatterns = await loadIndexPatterns({
|
||||
dataViews,
|
||||
patterns: usedIndexPatterns,
|
||||
patterns: usedIndexPatternsIds,
|
||||
notUsedPatterns,
|
||||
cache: {},
|
||||
adHocDataViews,
|
||||
});
|
||||
|
||||
return { indexPatternRefs, indexPatterns };
|
||||
const adHocDataViewsRefs = getRefsForAdHocDataViewsFromContext(
|
||||
indexPatternRefs,
|
||||
usedIndexPatternsIds,
|
||||
indexPatterns
|
||||
);
|
||||
return {
|
||||
indexPatternRefs: sortDataViewRefs([...indexPatternRefs, ...adHocDataViewsRefs]),
|
||||
indexPatterns,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -284,6 +284,11 @@ export const isOperationFromTheSameGroup = (op1?: DraggingIdentifier, op2?: Drag
|
|||
);
|
||||
};
|
||||
|
||||
export const sortDataViewRefs = (dataViewRefs: IndexPatternRef[]) =>
|
||||
dataViewRefs.sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
|
||||
export const getSearchWarningMessages = (
|
||||
adapter: RequestAdapter,
|
||||
datasource: Datasource,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue