[Lens][TSVB] Navigate to Lens TSVB Table. (#143946)

* Added convert to lens support for tsvb table

* Added unit tests

* Added functional tests

* Some refactoring of table metric option config

* Fixed imports

* Some small refactoring

* Fix flaky test

* Fixed test

* Some small fixes

* Fixed test
This commit is contained in:
Uladzislau Lasitsa 2022-10-26 17:30:01 +03:00 committed by GitHub
parent 21806cabab
commit c31c38c3d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1019 additions and 148 deletions

View file

@ -65,6 +65,10 @@ export class SeriesEditor extends Component {
}
};
handleSeriesChange = (doc) => {
handleChange(this.props, doc);
};
render() {
const { limit, model, name, fields, colorPicker } = this.props;
const list = model[name].filter((val, index) => index < (limit || Infinity));
@ -89,7 +93,7 @@ export class SeriesEditor extends Component {
disableDelete={model[name].length < 2}
fields={fields}
onAdd={() => handleAdd(this.props, newSeriesFn)}
onChange={(doc) => handleChange(this.props, doc)}
onChange={this.handleSeriesChange}
onClone={() => this.handleClone(row)}
onDelete={() => handleDelete(this.props, row)}
model={row}

View file

@ -35,7 +35,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { getDefaultQueryLanguage } from '../../lib/get_default_query_language';
import { checkIfNumericMetric } from '../../lib/check_if_numeric_metric';
import { QueryBarWrapper } from '../../query_bar_wrapper';
import { DATA_FORMATTERS } from '../../../../../common/enums';
import { DATA_FORMATTERS, BUCKET_TYPES } from '../../../../../common/enums';
import { isConfigurationFeatureEnabled } from '../../../../../common/check_ui_restrictions';
import { filterCannotBeAppliedErrorMessage } from '../../../../../common/errors';
import { tsvbEditorRowStyles } from '../../../styles/common.styles';
@ -50,13 +50,20 @@ class TableSeriesConfigUi extends Component {
}
}
handleAggregateByChange = (selectedOptions) => {
this.props.onChange({
aggregate_by: selectedOptions?.[0],
});
};
handleSelectChange = createSelectHandler(this.props.onChange);
handleTextChange = createTextHandler(this.props.onChange);
changeModelFormatter = (formatter) => this.props.onChange({ formatter });
render() {
const defaults = { offset_time: '', value_template: '{{value}}' };
const model = { ...defaults, ...this.props.model };
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const htmlId = htmlIdGenerator();
const functionOptions = [
@ -160,7 +167,7 @@ class TableSeriesConfigUi extends Component {
fullWidth
>
<EuiFieldText
onChange={handleTextChange('value_template')}
onChange={this.handleTextChange('value_template')}
value={model.value_template}
disabled={model.formatter === DATA_FORMATTERS.DEFAULT}
fullWidth
@ -214,7 +221,7 @@ class TableSeriesConfigUi extends Component {
<EuiHorizontalRule margin="s" />
<EuiFlexGroup responsive={false} wrap={true}>
<EuiFlexItem grow={true}>
<EuiFlexItem grow={true} data-test-subj="tsvbAggregateBySelect">
<FieldSelect
label={
<FormattedMessage id="visTypeTimeseries.table.fieldLabel" defaultMessage="Field" />
@ -222,11 +229,7 @@ class TableSeriesConfigUi extends Component {
fields={this.props.fields}
indexPattern={this.props.panel.index_pattern}
value={model.aggregate_by}
onChange={(value) =>
this.props.onChange({
aggregate_by: value?.[0],
})
}
onChange={this.handleAggregateByChange}
fullWidth
restrict={[
KBN_FIELD_TYPES.NUMBER,
@ -236,7 +239,7 @@ class TableSeriesConfigUi extends Component {
KBN_FIELD_TYPES.STRING,
]}
uiRestrictions={this.props.uiRestrictions}
type={'terms'}
type={BUCKET_TYPES.TERMS}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
@ -251,9 +254,10 @@ class TableSeriesConfigUi extends Component {
fullWidth
>
<EuiComboBox
data-test-subj="tsvbAggregateFunctionCombobox"
options={functionOptions}
selectedOptions={selectedAggFuncOption ? [selectedAggFuncOption] : []}
onChange={handleSelectChange('aggregate_function')}
onChange={this.handleSelectChange('aggregate_function')}
singleSelection={{ asPlainText: true }}
fullWidth
/>

View file

@ -6,10 +6,11 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { TSVB_METRIC_TYPES } from '../../../common/enums';
import { Metric } from '../../../common/types';
import { Panel, Metric } from '../../../common/types';
import { convertToLens } from '.';
import { createPanel, createSeries } from '../lib/__mocks__';
import { AvgColumn } from '../lib/convert';
@ -58,6 +59,10 @@ describe('convertToLens', () => {
series: [createSeries({ metrics: [metric] })],
});
const vis = {
params: model,
} as Vis<Panel>;
const metricColumn: AvgColumn = {
columnId: 'col-id',
dataType: 'number',
@ -89,51 +94,55 @@ describe('convertToLens', () => {
test('should return null for invalid metrics', async () => {
mockIsValidMetrics.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockIsValidMetrics).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported metrics', async () => {
mockGetMetricsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetMetricsColumns).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported buckets', async () => {
mockGetBucketsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
test('should return null if metric is staticValue', async () => {
const result = await convertToLens({
...model,
series: [
{
...model.series[0],
metrics: [...model.series[0].metrics, { type: TSVB_METRIC_TYPES.STATIC } as Metric],
},
],
});
params: {
...model,
series: [
{
...model.series[0],
metrics: [...model.series[0].metrics, { type: TSVB_METRIC_TYPES.STATIC } as Metric],
},
],
},
} as Vis<Panel>);
expect(result).toBeNull();
expect(mockGetDataSourceInfo).toBeCalledTimes(0);
});
test('should return null if only series agg is specified', async () => {
const result = await convertToLens({
...model,
series: [
{
...model.series[0],
metrics: [
{ type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min', id: 'some-id' } as Metric,
],
},
],
});
params: {
...model,
series: [
{
...model.series[0],
metrics: [
{ type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min', id: 'some-id' } as Metric,
],
},
],
},
} as Vis<Panel>);
expect(result).toBeNull();
});
@ -142,7 +151,7 @@ describe('convertToLens', () => {
mockGetSeriesAgg.mockReturnValue({ metrics: [metric] });
mockGetConfigurationForGauge.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
});
@ -151,8 +160,8 @@ describe('convertToLens', () => {
mockGetSeriesAgg.mockReturnValue({ metrics: [metric] });
mockGetConfigurationForGauge.mockReturnValue({});
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
@ -163,8 +172,8 @@ describe('convertToLens', () => {
hidden: false,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsMetric');
});

View file

@ -45,7 +45,10 @@ const getMaxFormula = (metric: Metric, column?: Column) => {
}))`;
};
export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => {
export const convertToLens: ConvertTsvbToLensVisualization = async (
{ params: model },
timeRange
) => {
const dataViews = getDataViewsStart();
const series = model.series[0];

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import type { Panel } from '../../common/types';
import { convertTSVBtoLensConfiguration } from '.';
@ -42,7 +43,9 @@ describe('convertTSVBtoLensConfiguration', () => {
...model,
type: 'markdown',
} as Panel;
const triggerOptions = await convertTSVBtoLensConfiguration(metricModel);
const triggerOptions = await convertTSVBtoLensConfiguration({
params: metricModel,
} as Vis<Panel>);
expect(triggerOptions).toBeNull();
});
@ -51,7 +54,9 @@ describe('convertTSVBtoLensConfiguration', () => {
...model,
use_kibana_indexes: false,
};
const triggerOptions = await convertTSVBtoLensConfiguration(stringIndexPatternModel);
const triggerOptions = await convertTSVBtoLensConfiguration({
params: stringIndexPatternModel,
} as Vis<Panel>);
expect(triggerOptions).toBeNull();
});
});

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import { TimeRange } from '@kbn/data-plugin/common';
import type { Panel } from '../../common/types';
import { PANEL_TYPES } from '../../common/enums';
@ -29,6 +30,10 @@ const getConvertFnByType = (type: PANEL_TYPES) => {
const { convertToLens } = await import('./gauge');
return convertToLens;
},
[PANEL_TYPES.TABLE]: async () => {
const { convertToLens } = await import('./table');
return convertToLens;
},
};
return convertionFns[type]?.();
@ -39,17 +44,17 @@ const getConvertFnByType = (type: PANEL_TYPES) => {
* Returns the Lens model, only if it is supported. If not, it returns null.
* In case of null, the menu item is disabled and the user can't navigate to Lens.
*/
export const convertTSVBtoLensConfiguration = async (model: Panel, timeRange?: TimeRange) => {
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 (!model.use_kibana_indexes) {
if (!vis.params.use_kibana_indexes) {
return null;
}
// Disables if model is invalid
if (model.isModelInvalid) {
if (vis.params.isModelInvalid) {
return null;
}
const convertFn = await getConvertFnByType(model.type);
const convertFn = await getConvertFnByType(vis.params.type);
return (await convertFn?.(model, timeRange)) ?? null;
return (await convertFn?.(vis, timeRange)) ?? null;
};

View file

@ -14,7 +14,7 @@ import { getConfigurationForMetric, getConfigurationForGauge } from '.';
const mockGetPalette = jest.fn();
jest.mock('./palette', () => ({
jest.mock('../palette', () => ({
getPalette: jest.fn(() => mockGetPalette()),
}));

View file

@ -10,7 +10,7 @@ import color from 'color';
import { MetricVisConfiguration } from '@kbn/visualizations-plugin/common';
import { Panel } from '../../../../../common/types';
import { Column, Layer } from '../../convert';
import { getPalette } from './palette';
import { getPalette } from '../palette';
import { findMetricColumn, getMetricWithCollapseFn } from '../../../utils';
export const getConfigurationForMetric = (

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { getPalette } from './palette';
import { getPalette } from '.';
describe('getPalette', () => {
const baseColor = '#fff';

View file

@ -8,7 +8,7 @@
import color from 'color';
import { ColorStop, CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
import { uniqBy } from 'lodash';
import { Panel } from '../../../../../common/types';
import { Panel, Series } from '../../../../../common/types';
const Operators = {
GTE: 'gte',
@ -24,9 +24,11 @@ type ColorStopsWithMinMax = Pick<
type MetricColorRules = Exclude<Panel['background_color_rules'], undefined>;
type GaugeColorRules = Exclude<Panel['gauge_color_rules'], undefined>;
type SeriesColorRules = Exclude<Series['color_rules'], undefined>;
type MetricColorRule = MetricColorRules[number];
type GaugeColorRule = GaugeColorRules[number];
type SeriesColorRule = SeriesColorRules[number];
type ValidMetricColorRule = Omit<MetricColorRule, 'background_color' | 'color'> &
(
@ -44,31 +46,47 @@ type ValidGaugeColorRule = Omit<GaugeColorRule, 'gauge'> & {
gauge: Exclude<GaugeColorRule['gauge'], undefined>;
};
type ValidSeriesColorRule = Omit<SeriesColorRule, 'text'> & {
text: Exclude<SeriesColorRule['text'], undefined>;
};
const isValidColorRule = (
rule: MetricColorRule | GaugeColorRule
): rule is ValidMetricColorRule | ValidGaugeColorRule => {
): rule is ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule => {
const { background_color: bColor, color: textColor } = rule as MetricColorRule;
const { gauge } = rule as GaugeColorRule;
const { text } = rule as SeriesColorRule;
return rule.operator && (bColor ?? textColor ?? gauge) && rule.value !== undefined ? true : false;
return Boolean(
rule.operator && (bColor ?? textColor ?? gauge ?? text) && rule.value !== undefined
);
};
const isMetricColorRule = (
rule: ValidMetricColorRule | ValidGaugeColorRule
rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule
): rule is ValidMetricColorRule => {
const metricRule = rule as ValidMetricColorRule;
return metricRule.background_color ?? metricRule.color ? true : false;
};
const getColor = (rule: ValidMetricColorRule | ValidGaugeColorRule) => {
const isGaugeColorRule = (
rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule
): rule is ValidGaugeColorRule => {
const metricRule = rule as ValidGaugeColorRule;
return Boolean(metricRule.gauge);
};
const getColor = (rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule) => {
if (isMetricColorRule(rule)) {
return rule.background_color ?? rule.color;
} else if (isGaugeColorRule(rule)) {
return rule.gauge;
}
return rule.gauge;
return rule.text;
};
const getColorStopsWithMinMaxForAllGteOrWithLte = (
rules: Array<ValidMetricColorRule | ValidGaugeColorRule>,
rules: Array<ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule>,
tailOperator: string,
baseColor?: string
): ColorStopsWithMinMax => {
@ -125,7 +143,7 @@ const getColorStopsWithMinMaxForAllGteOrWithLte = (
};
const getColorStopsWithMinMaxForLtWithLte = (
rules: Array<ValidMetricColorRule | ValidGaugeColorRule>
rules: Array<ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule>
): ColorStopsWithMinMax => {
const lastRule = rules[rules.length - 1];
const colorStops = rules.reduce<ColorStop[]>((colors, rule, index, rulesArr) => {
@ -166,7 +184,7 @@ const getColorStopsWithMinMaxForLtWithLte = (
};
const getColorStopWithMinMaxForLte = (
rule: ValidMetricColorRule | ValidGaugeColorRule
rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule
): ColorStopsWithMinMax => {
const colorStop = {
color: color(getColor(rule)).hex(),
@ -183,7 +201,7 @@ const getColorStopWithMinMaxForLte = (
};
const getColorStopWithMinMaxForGte = (
rule: ValidMetricColorRule | ValidGaugeColorRule,
rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule,
baseColor?: string
): ColorStopsWithMinMax => {
const colorStop = {
@ -224,12 +242,14 @@ const getCustomPalette = (
};
export const getPalette = (
rules: MetricColorRules | GaugeColorRules,
rules: MetricColorRules | GaugeColorRules | SeriesColorRules,
baseColor?: string
): PaletteOutput<CustomPaletteParams> | null | undefined => {
const validRules = (rules as Array<MetricColorRule | GaugeColorRule>).filter<
ValidMetricColorRule | ValidGaugeColorRule
>((rule): rule is ValidMetricColorRule | ValidGaugeColorRule => isValidColorRule(rule));
const validRules = (rules as Array<MetricColorRule | GaugeColorRule | SeriesColorRule>).filter<
ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule
>((rule): rule is ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule =>
isValidColorRule(rule)
);
validRules.sort((rule1, rule2) => {
return rule1.value! - rule2.value!;

View file

@ -0,0 +1,56 @@
/*
* 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 { createSeries } from '../../__mocks__';
import { getColumnState } from '.';
const mockGetPalette = jest.fn();
jest.mock('../palette', () => ({
getPalette: jest.fn(() => mockGetPalette()),
}));
describe('getColumnState', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetPalette.mockReturnValue({ id: 'custom' });
});
test('should return column state without palette if series is not provided', () => {
const config = getColumnState('test');
expect(config).toEqual({
columnId: 'test',
alignment: 'left',
colorMode: 'none',
});
expect(mockGetPalette).toBeCalledTimes(0);
});
test('should return column state with palette if series is provided', () => {
const config = getColumnState('test', undefined, createSeries());
expect(config).toEqual({
columnId: 'test',
alignment: 'left',
colorMode: 'text',
palette: { id: 'custom' },
});
expect(mockGetPalette).toBeCalledTimes(1);
});
test('should return column state with collapseFn if collapseFn is provided', () => {
const config = getColumnState('test', 'max', createSeries());
expect(config).toEqual({
columnId: 'test',
alignment: 'left',
colorMode: 'text',
palette: { id: 'custom' },
collapseFn: 'max',
});
expect(mockGetPalette).toBeCalledTimes(1);
});
});

View file

@ -0,0 +1,21 @@
/*
* 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 { Series } from '../../../../../common/types';
import { getPalette } from '../palette';
export const getColumnState = (columnId: string, collapseFn?: string, series?: Series) => {
const palette = series ? getPalette(series.color_rules ?? []) : undefined;
return {
columnId,
alignment: 'left' as const,
colorMode: palette ? 'text' : 'none',
...(palette ? { palette } : {}),
...(collapseFn ? { collapseFn } : {}),
};
};

View file

@ -32,7 +32,7 @@ interface ExtraColumnFields {
const isSupportedFormat = (format: string) => ['bytes', 'number', 'percent'].includes(format);
export const getFormat = (series: Series): FormatParams => {
export const getFormat = (series: Pick<Series, 'formatter' | 'value_template'>): FormatParams => {
let suffix;
if (!series.formatter || series.formatter === 'default') {

View file

@ -9,28 +9,36 @@
import type { DataView } from '@kbn/data-views-plugin/common';
import uuid from 'uuid';
import { DateHistogramParams, DataType } from '@kbn/visualizations-plugin/common/convert_to_lens';
import { DateHistogramColumn } from './types';
import type { Panel, Series } from '../../../../common/types';
import { DateHistogramColumn, DateHistogramSeries } from './types';
import type { Panel } from '../../../../common/types';
const getInterval = (interval?: string) => {
return interval && !interval?.includes('=') ? interval : 'auto';
};
export const convertToDateHistogramParams = (model: Panel, series: Series): DateHistogramParams => {
export const convertToDateHistogramParams = (
model: Panel | undefined,
series: DateHistogramSeries,
includeEmptyRows: boolean = true
): DateHistogramParams => {
return {
interval: getInterval(series.override_index_pattern ? series.series_interval : model.interval),
interval: getInterval(series.override_index_pattern ? series.series_interval : model?.interval),
dropPartials: series.override_index_pattern
? series.series_drop_last_bucket > 0
: model.drop_last_bucket > 0,
includeEmptyRows: true,
: (model?.drop_last_bucket ?? 0) > 0,
includeEmptyRows,
};
};
export const convertToDateHistogramColumn = (
model: Panel,
series: Series,
model: Panel | undefined,
series: DateHistogramSeries,
dataView: DataView,
{ fieldName, isSplit }: { fieldName: string; isSplit: boolean }
{
fieldName,
isSplit,
includeEmptyRows = true,
}: { fieldName: string; isSplit: boolean; includeEmptyRows?: boolean }
): DateHistogramColumn | null => {
const dateField = dataView.getFieldByName(fieldName);
@ -38,7 +46,7 @@ export const convertToDateHistogramColumn = (
return null;
}
const params = convertToDateHistogramParams(model, series);
const params = convertToDateHistogramParams(model, series, includeEmptyRows);
return {
columnId: uuid(),

View file

@ -8,10 +8,9 @@
import uuid from 'uuid';
import { FiltersParams } from '@kbn/visualizations-plugin/common/convert_to_lens';
import { FiltersColumn } from './types';
import type { Series } from '../../../../common/types';
import { FiltersColumn, FiltersSeries } from './types';
export const convertToFiltersParams = (series: Series): FiltersParams => {
export const convertToFiltersParams = (series: FiltersSeries): FiltersParams => {
const splitFilters = [];
if (series.split_mode === 'filter' && series.filter) {
splitFilters.push({ filter: series.filter });
@ -35,7 +34,10 @@ export const convertToFiltersParams = (series: Series): FiltersParams => {
};
};
export const convertToFiltersColumn = (series: Series, isSplit: boolean): FiltersColumn | null => {
export const convertToFiltersColumn = (
series: FiltersSeries,
isSplit: boolean
): FiltersColumn | null => {
const params = convertToFiltersParams(series);
if (!params.filters.length) {
return null;

View file

@ -9,16 +9,15 @@
import type { DataView } from '@kbn/data-views-plugin/common';
import { DataType, TermsParams } from '@kbn/visualizations-plugin/common';
import uuid from 'uuid';
import { Series } from '../../../../common/types';
import { excludeMetaFromColumn, getFormat, isColumnWithMeta } from './column';
import { Column, TermsColumn } from './types';
import { Column, TermsColumn, TermsSeries } from './types';
interface OrderByWithAgg {
orderAgg?: TermsParams['orderAgg'];
orderBy: TermsParams['orderBy'];
}
const getOrderByWithAgg = (series: Series, columns: Column[]): OrderByWithAgg | null => {
const getOrderByWithAgg = (series: TermsSeries, columns: Column[]): OrderByWithAgg | null => {
if (series.terms_order_by === '_key') {
return { orderBy: { type: 'alphabetical' } };
}
@ -56,7 +55,7 @@ const getOrderByWithAgg = (series: Series, columns: Column[]): OrderByWithAgg |
};
export const convertToTermsParams = (
series: Series,
series: TermsSeries,
columns: Column[],
secondaryFields: string[]
): TermsParams | null => {
@ -84,10 +83,11 @@ export const convertToTermsParams = (
export const convertToTermsColumn = (
termFields: [string, ...string[]],
series: Series,
series: TermsSeries,
columns: Column[],
dataView: DataView,
isSplit: boolean = false
isSplit: boolean = false,
label?: string
): TermsColumn | null => {
const [baseField, ...secondaryFields] = termFields;
const field = dataView.getFieldByName(baseField);
@ -108,6 +108,7 @@ export const convertToTermsColumn = (
sourceField: field.name,
isBucketed: true,
isSplit,
label,
params: { ...params, ...getFormat(series) },
};
};

View file

@ -117,4 +117,24 @@ export interface CommonColumnConverterArgs {
dataView: DataView;
}
export type TermsSeries = Pick<
Series,
| 'split_mode'
| 'terms_direction'
| 'terms_order_by'
| 'terms_size'
| 'terms_include'
| 'terms_exclude'
| 'terms_field'
| 'formatter'
| 'value_template'
>;
export type FiltersSeries = Pick<Series, 'split_mode' | 'filter' | 'split_filters'>;
export type DateHistogramSeries = Pick<
Series,
'split_mode' | 'override_index_pattern' | 'series_interval' | 'series_drop_last_bucket'
>;
export { FiltersColumn, TermsColumn, DateHistogramColumn };

View file

@ -68,6 +68,7 @@ const supportedPanelTypes: readonly PANEL_TYPES[] = [
PANEL_TYPES.TOP_N,
PANEL_TYPES.METRIC,
PANEL_TYPES.GAUGE,
PANEL_TYPES.TABLE,
];
const supportedTimeRangeModes: readonly TIME_RANGE_DATA_MODES[] = [

View file

@ -7,18 +7,21 @@
*/
import type { DataView } from '@kbn/data-views-plugin/common';
import { Series, Panel } from '../../../../common/types';
import { Panel } from '../../../../common/types';
import { getFieldsForTerms } from '../../../../common/fields_utils';
import {
Column,
convertToFiltersColumn,
convertToDateHistogramColumn,
convertToTermsColumn,
TermsSeries,
FiltersSeries,
DateHistogramSeries,
} from '../convert';
import { getValidColumns } from './columns';
export const isSplitWithDateHistogram = (
series: Series,
series: TermsSeries,
splitFields: string[],
dataView: DataView
) => {
@ -39,27 +42,49 @@ export const isSplitWithDateHistogram = (
return false;
};
const isFiltersSeries = (
series: DateHistogramSeries | TermsSeries | FiltersSeries
): series is FiltersSeries => {
return series.split_mode === 'filters' || series.split_mode === 'filter';
};
const isTermsSeries = (
series: DateHistogramSeries | TermsSeries | FiltersSeries
): series is TermsSeries => {
return series.split_mode === 'terms';
};
const isDateHistogramSeries = (
series: DateHistogramSeries | TermsSeries | FiltersSeries,
isDateHistogram: boolean
): series is DateHistogramSeries => {
return isDateHistogram && series.split_mode === 'terms';
};
export const getBucketsColumns = (
model: Panel,
series: Series,
model: Panel | undefined,
series: DateHistogramSeries | TermsSeries | FiltersSeries,
columns: Column[],
dataView: DataView,
isSplit: boolean = false
isSplit: boolean = false,
label?: string,
includeEmptyRowsForDateHistogram: boolean = true
) => {
if (series.split_mode === 'filters' || series.split_mode === 'filter') {
if (isFiltersSeries(series)) {
const filterColumn = convertToFiltersColumn(series, true);
return getValidColumns([filterColumn]);
}
if (series.split_mode === 'terms') {
if (isTermsSeries(series)) {
const splitFields = getFieldsForTerms(series.terms_field);
const isDateHistogram = isSplitWithDateHistogram(series, splitFields, dataView);
if (isDateHistogram === null) {
return null;
}
if (isDateHistogram) {
if (isDateHistogramSeries(series, isDateHistogram)) {
const dateHistogramColumn = convertToDateHistogramColumn(model, series, dataView, {
fieldName: splitFields[0],
isSplit: true,
includeEmptyRows: includeEmptyRowsForDateHistogram,
});
return getValidColumns(dateHistogramColumn);
}
@ -73,7 +98,8 @@ export const getBucketsColumns = (
series,
columns,
dataView,
isSplit
isSplit,
label
);
return getValidColumns(termsColumn);
}

View file

@ -6,10 +6,12 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { convertToLens } from '.';
import { createPanel, createSeries } from '../lib/__mocks__';
import { Panel } from '../../../common/types';
const mockGetMetricsColumns = jest.fn();
const mockGetBucketsColumns = jest.fn();
@ -54,6 +56,10 @@ describe('convertToLens', () => {
],
});
const vis = {
params: model,
} as Vis<Panel>;
const bucket = {
isBucketed: true,
isSplit: true,
@ -135,27 +141,27 @@ describe('convertToLens', () => {
test('should return null for invalid metrics', async () => {
mockIsValidMetrics.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockIsValidMetrics).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported metrics', async () => {
mockGetMetricsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetMetricsColumns).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported buckets', async () => {
mockGetBucketsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
test('should return state for valid model', async () => {
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsMetric');
expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length);
@ -163,16 +169,16 @@ describe('convertToLens', () => {
});
test('should skip hidden series', async () => {
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
hidden: true,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsMetric');
expect(mockIsValidMetrics).toBeCalledTimes(0);
@ -185,8 +191,8 @@ describe('convertToLens', () => {
indexPattern: { id: 'test-index-pattern-1' },
});
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
@ -197,8 +203,8 @@ describe('convertToLens', () => {
hidden: false,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeNull();
});
@ -207,8 +213,8 @@ describe('convertToLens', () => {
mockGetBucketsColumns.mockReturnValueOnce([]);
mockGetMetricsColumns.mockReturnValueOnce([metric]);
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
@ -219,8 +225,8 @@ describe('convertToLens', () => {
hidden: false,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeNull();
});
@ -229,8 +235,8 @@ describe('convertToLens', () => {
mockGetBucketsColumns.mockReturnValueOnce([bucket2]);
mockGetMetricsColumns.mockReturnValueOnce([metric]);
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
@ -241,8 +247,8 @@ describe('convertToLens', () => {
hidden: false,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeNull();
});
@ -251,8 +257,8 @@ describe('convertToLens', () => {
mockGetBucketsColumns.mockReturnValueOnce([bucket]);
mockGetMetricsColumns.mockReturnValueOnce([metric]);
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
@ -263,8 +269,8 @@ describe('convertToLens', () => {
hidden: false,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsMetric');
expect(mockGetConfigurationForMetric).toBeCalledTimes(1);

View file

@ -22,7 +22,10 @@ import { excludeMetaFromLayers, getUniqueBuckets } from '../utils';
const MAX_SERIES = 2;
const MAX_BUCKETS = 2;
export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => {
export const convertToLens: ConvertTsvbToLensVisualization = async (
{ params: model },
timeRange
) => {
const dataViews = getDataViewsStart();
const seriesNum = model.series.filter((series) => !series.hidden).length;

View file

@ -0,0 +1,235 @@
/*
* 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 { TableVisConfiguration } from '@kbn/visualizations-plugin/common';
import { Vis } from '@kbn/visualizations-plugin/public';
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { convertToLens } from '.';
import { createPanel, createSeries } from '../lib/__mocks__';
import { Panel } from '../../../common/types';
import { TSVB_METRIC_TYPES } from '../../../common/enums';
const mockConvertToDateHistogramColumn = jest.fn();
const mockGetMetricsColumns = jest.fn();
const mockGetBucketsColumns = jest.fn();
const mockGetConfigurationForTimeseries = jest.fn();
const mockIsValidMetrics = jest.fn();
const mockGetDatasourceValue = jest
.fn()
.mockImplementation(() => Promise.resolve(stubLogstashDataView));
const mockGetDataSourceInfo = jest.fn();
const mockGetColumnState = jest.fn();
jest.mock('../../services', () => ({
getDataViewsStart: jest.fn(() => mockGetDatasourceValue),
}));
jest.mock('../lib/convert', () => ({
excludeMetaFromColumn: jest.fn().mockReturnValue({}),
}));
jest.mock('../lib/series', () => ({
getMetricsColumns: jest.fn(() => mockGetMetricsColumns()),
getBucketsColumns: jest.fn(() => mockGetBucketsColumns()),
}));
jest.mock('../lib/configurations/table', () => ({
getColumnState: jest.fn(() => mockGetColumnState()),
}));
jest.mock('../lib/metrics', () => ({
isValidMetrics: jest.fn(() => mockIsValidMetrics()),
getReducedTimeRange: jest.fn().mockReturnValue('10'),
}));
jest.mock('../lib/datasource', () => ({
getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()),
}));
describe('convertToLens', () => {
const model = createPanel({
series: [
createSeries({
metrics: [
{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' },
{ id: 'some-id-1', type: METRIC_TYPES.COUNT },
],
}),
],
});
const vis = {
params: model,
uiState: {
get: () => ({}),
},
} as Vis<Panel>;
beforeEach(() => {
mockIsValidMetrics.mockReturnValue(true);
mockGetDataSourceInfo.mockReturnValue({
indexPatternId: 'test-index-pattern',
timeField: 'timeField',
indexPattern: { id: 'test-index-pattern' },
});
mockConvertToDateHistogramColumn.mockReturnValue({});
mockGetMetricsColumns.mockReturnValue([{}]);
mockGetBucketsColumns.mockReturnValue([{}]);
mockGetConfigurationForTimeseries.mockReturnValue({ layers: [] });
});
afterEach(() => {
jest.clearAllMocks();
});
test('should return null for invalid metrics', async () => {
mockIsValidMetrics.mockReturnValue(null);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockIsValidMetrics).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported metrics', async () => {
mockGetMetricsColumns.mockReturnValue(null);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetMetricsColumns).toBeCalledTimes(1);
});
test('should return null if several series have different “Field” + “Aggregate function”', async () => {
const result = await convertToLens({
params: createPanel({
series: [createSeries({ aggregate_by: 'new' }), createSeries({ aggregate_by: 'test' })],
}),
uiState: {
get: () => ({}),
},
} as Vis<Panel>);
expect(result).toBeNull();
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
test('should return null if “Aggregate function” is not supported', async () => {
const result = await convertToLens({
params: createPanel({
series: [createSeries({ aggregate_by: 'new', aggregate_function: 'cumulative_sum' })],
}),
uiState: {
get: () => ({}),
},
} as Vis<Panel>);
expect(result).toBeNull();
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
test('should return null if model have not visible metrics', async () => {
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
hidden: true,
}),
],
}),
uiState: {
get: () => ({}),
},
} as Vis<Panel>);
expect(result).toBeNull();
});
test('should return null if only static value is visible metric', async () => {
mockGetMetricsColumns.mockReturnValue([
{ columnId: 'metric-column-1', operationType: 'static_value' },
]);
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: TSVB_METRIC_TYPES.STATIC }],
hidden: true,
}),
],
}),
uiState: {
get: () => ({}),
},
} as Vis<Panel>);
expect(result).toBeNull();
});
test('should return state for valid model', async () => {
const result = await convertToLens(vis);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsDatatable');
expect(mockGetBucketsColumns).toBeCalledTimes(1);
// every series + group by
expect(mockGetColumnState).toBeCalledTimes(model.series.length + 1);
});
test('should return state for valid model with “Field” + “Aggregate function”', async () => {
const result = await convertToLens({
params: createPanel({
series: [createSeries({ aggregate_by: 'new', aggregate_function: 'sum' })],
}),
uiState: {
get: () => ({}),
},
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsDatatable');
expect(mockGetBucketsColumns).toBeCalledTimes(2);
// every series + group by + (“Field” + “Aggregate function”)
expect(mockGetColumnState).toBeCalledTimes(model.series.length + 2);
});
test('should return correct sorting config', async () => {
mockGetMetricsColumns.mockReturnValue([{ columnId: 'metric-column-1' }]);
const result = await convertToLens({
params: createPanel({
series: [createSeries({ id: 'test' })],
}),
uiState: {
get: () => ({ sort: { order: 'decs', column: 'test' } }),
},
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsDatatable');
expect((result?.configuration as TableVisConfiguration).sorting).toEqual({
direction: 'decs',
columnId: 'metric-column-1',
});
expect(mockGetBucketsColumns).toBeCalledTimes(1);
// every series + group by
expect(mockGetColumnState).toBeCalledTimes(model.series.length + 1);
});
test('should skip hidden series', async () => {
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
hidden: true,
}),
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
}),
],
}),
uiState: {
get: () => ({}),
},
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsDatatable');
expect(mockIsValidMetrics).toBeCalledTimes(1);
});
});

View file

@ -0,0 +1,183 @@
/*
* 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 uuid from 'uuid';
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 { getColumnState } from '../lib/configurations/table';
import { getDataSourceInfo } from '../lib/datasource';
import { getMetricsColumns, getBucketsColumns } from '../lib/series';
import { getReducedTimeRange, isValidMetrics } from '../lib/metrics';
import { ConvertTsvbToLensVisualization } from '../types';
import { Layer as ExtendedLayer, excludeMetaFromColumn, Column } from '../lib/convert';
const excludeMetaFromLayers = (layers: Record<string, ExtendedLayer>): Record<string, Layer> => {
const newLayers: Record<string, Layer> = {};
Object.entries(layers).forEach(([layerId, layer]) => {
const columns = layer.columns.map(excludeMetaFromColumn);
newLayers[layerId] = { ...layer, columns };
});
return newLayers;
};
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
);
if (!datasourceInfo) {
return null;
}
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;
}
bucketsColumns = getBucketsColumns(
undefined,
{
split_mode: 'terms',
terms_field: model.series[0].aggregate_by,
},
[],
indexPattern!,
false
);
if (bucketsColumns === null) {
return null;
}
columnStates.push(
getColumnState(
bucketsColumns[0].columnId,
model.series[0].aggregate_function === 'mean' ? 'avg' : model.series[0].aggregate_function
)
);
}
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') {
return null;
}
if (!isValidMetrics(series.metrics, PANEL_TYPES.TABLE, series.time_range_mode)) {
return null;
}
const reducedTimeRange = getReducedTimeRange(model, series, timeRange);
// handle multiple metrics
const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, {
reducedTimeRange,
});
if (!metricsColumns) {
return null;
}
columnStates.push(getColumnState(metricsColumns[0].columnId, undefined, series));
if (sortConfig.column === series.id) {
sortConfiguration.columnId = metricsColumns[0].columnId;
}
metrics.push(...metricsColumns);
}
if (!metrics.length || metrics.every((metric) => metric.operationType === 'static_value')) {
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),
};
};

View file

@ -6,10 +6,12 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { convertToLens } from '.';
import { createPanel, createSeries } from '../lib/__mocks__';
import { Panel } from '../../../common/types';
const mockConvertToDateHistogramColumn = jest.fn();
const mockGetMetricsColumns = jest.fn();
@ -60,6 +62,10 @@ describe('convertToLens', () => {
],
});
const vis = {
params: model,
} as Vis<Panel>;
beforeEach(() => {
mockIsValidMetrics.mockReturnValue(true);
mockGetDataSourceInfo.mockReturnValue({
@ -79,35 +85,35 @@ describe('convertToLens', () => {
test('should return null for invalid metrics', async () => {
mockIsValidMetrics.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockIsValidMetrics).toBeCalledTimes(1);
});
test('should return null for empty time field', async () => {
mockGetDataSourceInfo.mockReturnValue({ timeField: null });
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetDataSourceInfo).toBeCalledTimes(1);
});
test('should return null for invalid date histogram', async () => {
mockConvertToDateHistogramColumn.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockConvertToDateHistogramColumn).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported metrics', async () => {
mockGetMetricsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetMetricsColumns).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported buckets', async () => {
mockGetBucketsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
@ -119,14 +125,14 @@ describe('convertToLens', () => {
operationType: 'static_value',
},
]);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetMetricsColumns).toBeCalledTimes(1);
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
test('should return state for valid model', async () => {
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsXY');
expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length);
@ -134,16 +140,16 @@ describe('convertToLens', () => {
});
test('should skip hidden series', async () => {
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
hidden: true,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsXY');
expect(mockIsValidMetrics).toBeCalledTimes(0);

View file

@ -14,7 +14,6 @@ import {
} from '@kbn/visualizations-plugin/common/convert_to_lens';
import uuid from 'uuid';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { Panel } from '../../../common/types';
import { PANEL_TYPES } from '../../../common/enums';
import { getDataViewsStart } from '../../services';
import { getDataSourceInfo } from '../lib/datasource';
@ -41,7 +40,7 @@ const excludeMetaFromLayers = (layers: Record<string, ExtendedLayer>): Record<st
return newLayers;
};
export const convertToLens: ConvertTsvbToLensVisualization = async (model: Panel) => {
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;

View file

@ -6,10 +6,12 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import { METRIC_TYPES } from '@kbn/data-plugin/public';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { convertToLens } from '.';
import { createPanel, createSeries } from '../lib/__mocks__';
import { Panel } from '../../../common/types';
const mockGetMetricsColumns = jest.fn();
const mockGetBucketsColumns = jest.fn();
@ -59,6 +61,10 @@ describe('convertToLens', () => {
],
});
const vis = {
params: model,
} as Vis<Panel>;
beforeEach(() => {
mockIsValidMetrics.mockReturnValue(true);
mockGetDataSourceInfo.mockReturnValue({
@ -77,27 +83,27 @@ describe('convertToLens', () => {
test('should return null for invalid metrics', async () => {
mockIsValidMetrics.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockIsValidMetrics).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported metrics', async () => {
mockGetMetricsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetMetricsColumns).toBeCalledTimes(1);
});
test('should return null for invalid or unsupported buckets', async () => {
mockGetBucketsColumns.mockReturnValue(null);
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeNull();
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
test('should return state for valid model', async () => {
const result = await convertToLens(model);
const result = await convertToLens(vis);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsXY');
expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length);
@ -105,16 +111,16 @@ describe('convertToLens', () => {
});
test('should skip hidden series', async () => {
const result = await convertToLens(
createPanel({
const result = await convertToLens({
params: createPanel({
series: [
createSeries({
metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }],
hidden: true,
}),
],
})
);
}),
} as Vis<Panel>);
expect(result).toBeDefined();
expect(result?.type).toBe('lnsXY');
expect(mockIsValidMetrics).toBeCalledTimes(0);

View file

@ -28,7 +28,10 @@ const excludeMetaFromLayers = (layers: Record<string, ExtendedLayer>): Record<st
return newLayers;
};
export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => {
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;

View file

@ -6,18 +6,22 @@
* Side Public License, v 1.
*/
import { Vis } from '@kbn/visualizations-plugin/public';
import {
MetricVisConfiguration,
NavigateToLensContext,
XYConfiguration,
TableVisConfiguration,
} from '@kbn/visualizations-plugin/common';
import { TimeRange } from '@kbn/data-plugin/common';
import type { Panel } from '../../common/types';
export type ConvertTsvbToLensVisualization = (
model: Panel,
vis: Vis<Panel>,
timeRange?: TimeRange
) => Promise<NavigateToLensContext<XYConfiguration | MetricVisConfiguration> | null>;
) => Promise<NavigateToLensContext<
XYConfiguration | MetricVisConfiguration | TableVisConfiguration
> | null>;
export interface Filter {
kql?: string | { [key: string]: any } | undefined;

View file

@ -171,15 +171,13 @@ export const metricsVisDefinition: VisTypeDefinition<
return {
canNavigateToLens: Boolean(
vis?.params
? await convertTSVBtoLensConfiguration(vis.params as Panel, timeFilter?.getAbsoluteTime())
? await convertTSVBtoLensConfiguration(vis, timeFilter?.getAbsoluteTime())
: null
),
};
},
navigateToLens: async (vis, timeFilter) =>
vis?.params
? await convertTSVBtoLensConfiguration(vis?.params as Panel, timeFilter?.getAbsoluteTime())
: null,
vis?.params ? await convertTSVBtoLensConfiguration(vis, timeFilter?.getAbsoluteTime()) : null,
inspectorAdapters: () => ({
requests: new RequestAdapter(),

View file

@ -183,6 +183,7 @@ export interface ColumnState {
summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max';
alignment?: 'left' | 'right' | 'center';
collapseFn?: CollapseFunction;
palette?: PaletteOutput<CustomPaletteParams>;
}
export interface TableVisConfiguration {

View file

@ -114,6 +114,7 @@ const TopNav = ({
vis.type,
vis.params,
uiStateJSON?.vis,
uiStateJSON?.table,
vis.data.indexPattern,
]);

View file

@ -659,6 +659,28 @@ export class VisualBuilderPageObject extends FtrService {
await this.comboBox.setElement(fieldEl, field);
}
public async setFieldForAggregateBy(field: string): Promise<void> {
const aggregateBy = await this.testSubjects.find('tsvbAggregateBySelect');
await this.retry.try(async () => {
await this.comboBox.setElement(aggregateBy, field);
if (!(await this.comboBox.isOptionSelected(aggregateBy, field))) {
throw new Error(`aggregate by field - ${field} is not selected`);
}
});
}
public async setFunctionForAggregateFunction(func: string): Promise<void> {
const aggregateFunction = await this.testSubjects.find('tsvbAggregateFunctionCombobox');
await this.retry.try(async () => {
await this.comboBox.setElement(aggregateFunction, func);
if (!(await this.comboBox.isOptionSelected(aggregateFunction, func))) {
throw new Error(`aggregate function - ${func} is not selected`);
}
});
}
public async checkFieldForAggregationValidity(aggNth: number = 0): Promise<boolean> {
const fieldEl = await this.getFieldForAggregation(aggNth);

View file

@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./timeseries'));
loadTestFile(require.resolve('./dashboard'));
loadTestFile(require.resolve('./top_n'));
loadTestFile(require.resolve('./table'));
});
}

View file

@ -0,0 +1,218 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const { visualize, visualBuilder, lens, header } = getPageObjects([
'visualBuilder',
'visualize',
'header',
'lens',
]);
const testSubjects = getService('testSubjects');
const retry = getService('retry');
describe('Table', function describeIndexTests() {
before(async () => {
await visualize.initTests();
});
beforeEach(async () => {
await visualBuilder.resetPage();
await visualBuilder.clickTable();
await header.waitUntilLoadingHasFinished();
await visualBuilder.checkTableTabIsPresent();
await visualBuilder.selectGroupByField('machine.os.raw');
});
it('should not allow converting of not valid panel', async () => {
await visualBuilder.selectAggType('Max');
await header.waitUntilLoadingHasFinished();
expect(await visualize.hasNavigateToLensButton()).to.be(false);
});
it('should not allow converting of unsupported aggregations', async () => {
await visualBuilder.selectAggType('Sum of Squares');
await visualBuilder.setFieldForAggregation('machine.ram');
await header.waitUntilLoadingHasFinished();
expect(await visualize.hasNavigateToLensButton()).to.be(false);
});
it('should not allow converting sibling pipeline aggregations', async () => {
await visualBuilder.createNewAgg();
await visualBuilder.selectAggType('Overall Average', 1);
await visualBuilder.setFieldForAggregation('Count', 1);
await header.waitUntilLoadingHasFinished();
expect(await visualize.hasNavigateToLensButton()).to.be(false);
});
it('should not allow converting parent pipeline aggregations', async () => {
await visualBuilder.clickPanelOptions('table');
await visualBuilder.setMetricsDataTimerangeMode('Last value');
await visualBuilder.clickDataTab('table');
await visualBuilder.createNewAgg();
await visualBuilder.selectAggType('Cumulative Sum', 1);
await visualBuilder.setFieldForAggregation('Count', 1);
await header.waitUntilLoadingHasFinished();
expect(await visualize.hasNavigateToLensButton()).to.be(false);
});
it('should not allow converting not valid aggregation function', async () => {
await visualBuilder.clickSeriesOption();
await visualBuilder.setFieldForAggregateBy('clientip');
await visualBuilder.setFunctionForAggregateFunction('Cumulative Sum');
await header.waitUntilLoadingHasFinished();
expect(await visualize.hasNavigateToLensButton()).to.be(false);
});
it('should not allow converting series with different aggregation fucntion or aggregation by', async () => {
await visualBuilder.createNewAggSeries();
await visualBuilder.selectAggType('Static Value', 1);
await visualBuilder.setStaticValue(10);
await visualBuilder.clickSeriesOption();
await visualBuilder.setFieldForAggregateBy('bytes');
await visualBuilder.setFunctionForAggregateFunction('Sum');
await visualBuilder.clickSeriesOption(1);
await visualBuilder.setFieldForAggregateBy('bytes');
await visualBuilder.setFunctionForAggregateFunction('Min');
await header.waitUntilLoadingHasFinished();
expect(await visualize.hasNavigateToLensButton()).to.be(false);
});
it('should allow converting a count aggregation', async () => {
expect(await visualize.hasNavigateToLensButton()).to.be(true);
});
it('should convert last value mode to reduced time range', async () => {
await visualBuilder.clickPanelOptions('table');
await visualBuilder.setMetricsDataTimerangeMode('Last value');
await visualBuilder.setIntervalValue('1m');
await visualBuilder.clickDataTab('table');
await header.waitUntilLoadingHasFinished();
await visualize.navigateToLensFromAnotherVisulization();
await lens.waitForVisualization('lnsDataTable');
await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger');
await testSubjects.click('indexPattern-advanced-accordion');
const reducedTimeRange = await testSubjects.find('indexPattern-dimension-reducedTimeRange');
expect(await reducedTimeRange.getVisibleText()).to.be('1 minute (1m)');
await retry.try(async () => {
const layerCount = await lens.getLayerCount();
expect(layerCount).to.be(1);
const metricDimensionText = await lens.getDimensionTriggerText('lnsDatatable_metrics', 0);
expect(metricDimensionText).to.be('Count of records last 1m');
});
});
it('should convert static value to the metric dimension', async () => {
await visualBuilder.createNewAggSeries();
await visualBuilder.selectAggType('Static Value', 1);
await visualBuilder.setStaticValue(10);
await header.waitUntilLoadingHasFinished();
await visualize.navigateToLensFromAnotherVisulization();
await lens.waitForVisualization('lnsDataTable');
await retry.try(async () => {
const layerCount = await lens.getLayerCount();
expect(layerCount).to.be(1);
const metricDimensionText1 = await lens.getDimensionTriggerText('lnsDatatable_metrics', 0);
const metricDimensionText2 = await lens.getDimensionTriggerText('lnsDatatable_metrics', 1);
expect(metricDimensionText1).to.be('Count of records');
expect(metricDimensionText2).to.be('10');
});
});
it('should convert aggregate by to split row dimension', async () => {
await visualBuilder.clickSeriesOption();
await visualBuilder.setFieldForAggregateBy('clientip');
await visualBuilder.setFunctionForAggregateFunction('Sum');
await header.waitUntilLoadingHasFinished();
await visualize.navigateToLensFromAnotherVisulization();
await lens.waitForVisualization('lnsDataTable');
await retry.try(async () => {
const layerCount = await lens.getLayerCount();
expect(layerCount).to.be(1);
const splitRowsText1 = await lens.getDimensionTriggerText('lnsDatatable_rows', 0);
const splitRowsText2 = await lens.getDimensionTriggerText('lnsDatatable_rows', 1);
expect(splitRowsText1).to.be('Top 10 values of machine.os.raw');
expect(splitRowsText2).to.be('Top 10 values of clientip');
});
await lens.openDimensionEditor('lnsDatatable_rows > lns-dimensionTrigger', 0, 1);
const collapseBy = await testSubjects.find('indexPattern-collapse-by');
expect(await collapseBy.getAttribute('value')).to.be('sum');
});
it('should convert group by field with custom label', async () => {
await visualBuilder.setColumnLabelValue('test');
await header.waitUntilLoadingHasFinished();
await visualize.navigateToLensFromAnotherVisulization();
await lens.waitForVisualization('lnsDataTable');
await retry.try(async () => {
const layerCount = await lens.getLayerCount();
expect(layerCount).to.be(1);
const splitRowsText = await lens.getDimensionTriggerText('lnsDatatable_rows', 0);
expect(splitRowsText).to.be('test');
});
});
it('should convert color ranges', async () => {
await visualBuilder.clickSeriesOption();
await visualBuilder.setColorRuleOperator('>= greater than or equal');
await visualBuilder.setColorRuleValue(10);
await visualBuilder.setColorPickerValue('#54B399');
await visualBuilder.createColorRule(1);
await visualBuilder.setColorRuleOperator('>= greater than or equal');
await visualBuilder.setColorRuleValue(100, 1);
await visualBuilder.setColorPickerValue('#54A000', 1);
await header.waitUntilLoadingHasFinished();
await visualize.navigateToLensFromAnotherVisulization();
await lens.waitForVisualization('lnsDataTable');
await retry.try(async () => {
const closePalettePanels = await testSubjects.findAll(
'lns-indexPattern-PalettePanelContainerBack'
);
if (closePalettePanels.length) {
await lens.closePalettePanel();
await lens.closeDimensionEditor();
}
await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger');
await lens.openPalettePanel('lnsDatatable');
const colorStops = await lens.getPaletteColorStops();
expect(colorStops).to.eql([
{ stop: '10', color: 'rgba(84, 179, 153, 1)' },
{ stop: '100', color: 'rgba(84, 160, 0, 1)' },
{ stop: '', color: undefined },
]);
});
});
});
}