[Agg based pie][Lens]Navigate to lens Agg based pie (#140879)

* Added support for converting Agg-based pie to Lens.

* Fixed problem with removed index pattern.

* Made some changes for uniformity.

* Fixed label of go back to.

* Fixed legend display if uiState was changed.

* Fixed configuration.

* Removed as string[].

* Fixed code according to the nit.

* Allowed 3 segments and restricted converting of split_row/split_column.

* Fixed tests.

* Refactored functional tests.

* Added functional tests for pie.

Co-authored-by: Yaroslav Kuznietsov <kuznetsov.yaroslav.yk@gmail.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
Uladzislau Lasitsa 2022-09-29 09:32:55 +03:00 committed by GitHub
parent 45e80b2a47
commit ea046acd24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1010 additions and 275 deletions

View file

@ -26,6 +26,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"canNavigateToLens": false,
"params": Object {
"listenOnChange": true,
},
@ -160,6 +161,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"canNavigateToLens": false,
"params": Object {
"listenOnChange": true,
},

View file

@ -188,6 +188,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: args.isDonut ? ChartTypes.DONUT : ChartTypes.PIE,
canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens),
params: {
listenOnChange: true,
},

View file

@ -108,6 +108,7 @@ export interface RenderValue {
visType: ChartTypes;
visConfig: PartitionVisParams;
syncColors: boolean;
canNavigateToLens?: boolean;
}
export enum LabelPositions {

View file

@ -49,7 +49,11 @@ export const getPartitionVisRenderer: (
displayName: strings.getDisplayName(),
help: strings.getHelpDescription(),
reuseDomNode: true,
render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => {
render: async (
domNode,
{ visConfig, visData, visType, syncColors, canNavigateToLens },
handlers
) => {
const { core, plugins } = getStartDeps();
handlers.onDestroy(() => {
@ -62,9 +66,12 @@ export const getPartitionVisRenderer: (
const visualizationType = extractVisualizationType(executionContext);
if (containerType && visualizationType) {
plugins.usageCollection?.reportUiCounter(containerType, METRIC_TYPE.COUNT, [
const events = [
`render_${visualizationType}_${visType}`,
]);
canNavigateToLens ? `render_${visualizationType}_${visType}_convertable` : undefined,
].filter<string>((event): event is string => Boolean(event));
plugins.usageCollection?.reportUiCounter(containerType, METRIC_TYPE.COUNT, events);
}
handlers.done();
};

View file

@ -3,8 +3,8 @@
"version": "kibana",
"ui": true,
"server": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPartitionVis"],
"requiredBundles": ["visDefaultEditor"],
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPartitionVis", "dataViews"],
"requiredBundles": ["visDefaultEditor", "kibanaUtils"],
"extraPublicDirs": ["common/index"],
"owner": {
"name": "Vis Editors",

View file

@ -0,0 +1,104 @@
/*
* 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 { getConfiguration } from '.';
import { samplePieVis } from '../../sample_vis.test.mocks';
describe('getConfiguration', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should return correct configuration', () => {
samplePieVis.uiState.get.mockReturnValueOnce(undefined);
expect(
getConfiguration('test1', samplePieVis as any, {
metrics: ['metric-1'],
buckets: ['bucket-1'],
})
).toEqual({
layers: [
{
categoryDisplay: undefined,
emptySizeRatio: undefined,
layerId: 'test1',
layerType: 'data',
legendDisplay: 'show',
legendMaxLines: 1,
legendPosition: 'right',
legendSize: 'large',
metric: 'metric-1',
nestedLegend: true,
numberDisplay: 'percent',
percentDecimals: 2,
primaryGroups: ['bucket-1'],
secondaryGroups: [],
showValuesInLegend: true,
truncateLegend: true,
},
],
shape: 'donut',
palette: undefined,
});
});
test('should return legendDisplay = show if uiState contains truthy value', () => {
samplePieVis.uiState.get.mockReturnValueOnce(true);
expect(
getConfiguration(
'test1',
{ ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'hide' } } as any,
{
metrics: ['metric-1'],
buckets: ['bucket-1'],
}
)
).toEqual({
layers: [expect.objectContaining({ legendDisplay: 'show' })],
shape: 'donut',
palette: undefined,
});
});
test('should return legendDisplay = hide if uiState contains falsy value', () => {
samplePieVis.uiState.get.mockReturnValueOnce(false);
expect(
getConfiguration(
'test1',
{ ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'show' } } as any,
{
metrics: ['metric-1'],
buckets: ['bucket-1'],
}
)
).toEqual({
layers: [expect.objectContaining({ legendDisplay: 'hide' })],
shape: 'donut',
palette: undefined,
});
});
test('should return value of legendDisplay if uiState contains undefined value', () => {
samplePieVis.uiState.get.mockReturnValueOnce(undefined);
const legendDisplay = 'show';
expect(
getConfiguration(
'test1',
{ ...samplePieVis, params: { ...samplePieVis.params, legendDisplay } } as any,
{
metrics: ['metric-1'],
buckets: ['bucket-1'],
}
)
).toEqual({
layers: [expect.objectContaining({ legendDisplay })],
shape: 'donut',
palette: undefined,
});
});
});

View file

@ -0,0 +1,79 @@
/*
* 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 { LegendDisplay, PartitionVisParams } from '@kbn/expression-partition-vis-plugin/common';
import {
CategoryDisplayTypes,
NumberDisplayTypes,
PartitionVisConfiguration,
} from '@kbn/visualizations-plugin/common/convert_to_lens';
import { Vis } from '@kbn/visualizations-plugin/public';
const getLayers = (
layerId: string,
vis: Vis<PartitionVisParams>,
metrics: string[],
buckets: string[]
): PartitionVisConfiguration['layers'] => {
const legendOpen = vis.uiState.get('vis.legendOpen');
const legendDisplayFromUiState =
legendOpen !== undefined ? (legendOpen ? LegendDisplay.SHOW : LegendDisplay.HIDE) : undefined;
const showValuesInLegend =
vis.params.labels.values ??
vis.params.showValuesInLegend ??
vis.type.visConfig.defaults.showValuesInLegend;
return [
{
layerId,
layerType: 'data' as const,
primaryGroups: buckets,
secondaryGroups: [],
metric: metrics[0],
numberDisplay:
showValuesInLegend === false
? NumberDisplayTypes.HIDDEN
: vis.params.labels.valuesFormat ?? vis.type.visConfig.defaults.labels.valuesFormat,
categoryDisplay: vis.params.labels.show
? vis.params.labels.position ?? vis.type.visConfig.defaults.labels.position
: CategoryDisplayTypes.HIDE,
legendDisplay:
legendDisplayFromUiState ??
vis.params.legendDisplay ??
vis.type.visConfig.defaults.legendDisplay,
legendPosition: vis.params.legendPosition ?? vis.type.visConfig.defaults.legendPosition,
showValuesInLegend,
nestedLegend: vis.params.nestedLegend ?? vis.type.visConfig.defaults.nestedLegend,
percentDecimals:
vis.params.labels.percentDecimals ?? vis.type.visConfig.defaults.labels.percentDecimals,
emptySizeRatio: vis.params.emptySizeRatio ?? vis.type.visConfig.defaults.emptySizeRatio,
legendMaxLines: vis.params.maxLegendLines ?? vis.type.visConfig.defaults.maxLegendLines,
legendSize: vis.params.legendSize ?? vis.type.visConfig.defaults.legendSize,
truncateLegend: vis.params.truncateLegend ?? vis.type.visConfig.defaults.truncateLegend,
},
];
};
export const getConfiguration = (
layerId: string,
vis: Vis<PartitionVisParams>,
{
metrics,
buckets,
}: {
metrics: string[];
buckets: string[];
}
): PartitionVisConfiguration => {
return {
shape: vis.params.isDonut ? 'donut' : 'pie',
layers: getLayers(layerId, vis, metrics, buckets),
palette: vis.params.palette,
};
};

View file

@ -0,0 +1,77 @@
/*
* 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 { convertToLens } from '.';
import { samplePieVis } from '../sample_vis.test.mocks';
const mockGetColumnsFromVis = jest.fn();
const mockGetConfiguration = jest.fn().mockReturnValue({});
jest.mock('../services', () => ({
getDataViewsStart: jest.fn(() => ({ get: () => ({}), getDefault: () => ({}) })),
}));
jest.mock('@kbn/visualizations-plugin/public', () => ({
convertToLensModule: Promise.resolve({
getColumnsFromVis: jest.fn(() => mockGetColumnsFromVis()),
}),
getDataViewByIndexPatternId: jest.fn(() => ({ id: 'index-pattern' })),
}));
jest.mock('./configurations', () => ({
getConfiguration: jest.fn(() => mockGetConfiguration()),
}));
describe('convertToLens', () => {
afterEach(() => {
jest.clearAllMocks();
});
test('should return null if getColumnsFromVis returns null', async () => {
mockGetColumnsFromVis.mockReturnValue(null);
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if more than three split slice levels', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: ['1', '2', '3', '4'],
});
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should return null if no one split slices', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: [],
});
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(result).toBeNull();
});
test('should state for valid vis', async () => {
mockGetColumnsFromVis.mockReturnValue({
buckets: ['2'],
columns: [{ columnId: '2' }, { columnId: '1' }],
});
const result = await convertToLens(samplePieVis as any, {} as any);
expect(mockGetColumnsFromVis).toBeCalledTimes(1);
expect(mockGetConfiguration).toBeCalledTimes(1);
expect(result?.type).toEqual('lnsPie');
expect(result?.layers.length).toEqual(1);
expect(result?.layers[0]).toEqual(
expect.objectContaining({
columnOrder: [],
columns: [{ columnId: '2' }, { columnId: '1' }],
})
);
});
});

View file

@ -0,0 +1,79 @@
/*
* 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 { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common';
import {
convertToLensModule,
getDataViewByIndexPatternId,
} from '@kbn/visualizations-plugin/public';
import uuid from 'uuid';
import { getDataViewsStart } from '../services';
import { getConfiguration } from './configurations';
import { ConvertPieToLensVisualization } from './types';
export const isColumnWithMeta = (column: Column): column is ColumnWithMeta => {
if ((column as ColumnWithMeta).meta) {
return true;
}
return false;
};
export const excludeMetaFromColumn = (column: Column) => {
if (isColumnWithMeta(column)) {
const { meta, ...rest } = column;
return rest;
}
return column;
};
export const convertToLens: ConvertPieToLensVisualization = async (vis, timefilter) => {
if (!timefilter) {
return null;
}
const dataViews = getDataViewsStart();
const dataView = await getDataViewByIndexPatternId(vis.data.indexPattern?.id, dataViews);
if (!dataView) {
return null;
}
const { getColumnsFromVis } = await convertToLensModule;
const result = getColumnsFromVis(vis, timefilter, dataView, {
buckets: [],
splits: ['segment'],
unsupported: ['split_row', 'split_column'],
});
if (result === null) {
return null;
}
// doesn't support more than three split slice levels
// doesn't support pie without at least one split slice
if (result.buckets.length > 3 || !result.buckets.length) {
return null;
}
const layerId = uuid();
const indexPatternId = dataView.id!;
return {
type: 'lnsPie',
layers: [
{
indexPatternId,
layerId,
columns: result.columns.map(excludeMetaFromColumn),
columnOrder: [],
},
],
configuration: getConfiguration(layerId, vis, result),
indexPatternIds: [indexPatternId],
};
};

View file

@ -0,0 +1,20 @@
/*
* 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 { TimefilterContract } from '@kbn/data-plugin/public';
import { PartitionVisParams } from '@kbn/expression-partition-vis-plugin/common';
import {
NavigateToLensContext,
PartitionVisConfiguration,
} from '@kbn/visualizations-plugin/common';
import { Vis } from '@kbn/visualizations-plugin/public';
export type ConvertPieToLensVisualization = (
vis: Vis<PartitionVisParams>,
timefilter?: TimefilterContract
) => Promise<NavigateToLensContext<PartitionVisConfiguration> | null>;

View file

@ -6,13 +6,15 @@
* Side Public License, v 1.
*/
import { CoreSetup, DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
import { CoreSetup, CoreStart, DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
import { VisualizationsSetup } from '@kbn/visualizations-plugin/public';
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { LEGACY_PIE_CHARTS_LIBRARY } from '../common';
import { pieVisType } from './vis_type';
import { setDataViewsStart } from './services';
/** @internal */
export interface VisTypePieSetupDependencies {
@ -21,6 +23,11 @@ export interface VisTypePieSetupDependencies {
usageCollection: UsageCollectionSetup;
}
/** @internal */
export interface VisTypePieStartDependencies {
dataViews: DataViewsPublicPluginStart;
}
/** @internal */
export interface VisTypePiePluginStartDependencies {
data: DataPublicPluginStart;
@ -53,5 +60,7 @@ export class VisTypePiePlugin {
return {};
}
start() {}
start(core: CoreStart, { dataViews }: VisTypePieStartDependencies) {
setDataViewsStart(dataViews);
}
}

View file

@ -9,6 +9,8 @@
import { LegendDisplay } from '@kbn/expression-partition-vis-plugin/common';
import { LegendSize } from '@kbn/visualizations-plugin/common';
const mockUiStateGet = jest.fn().mockReturnValue(() => false);
export const samplePieVis = {
type: {
name: 'pie',
@ -1353,5 +1355,6 @@ export const samplePieVis = {
vis: {
legendOpen: false,
},
get: mockUiStateGet,
},
};

View file

@ -0,0 +1,13 @@
/*
* 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 { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
export const [getDataViewsStart, setDataViewsStart] =
createGetterSetter<DataViewsPublicPluginStart>('dataViews');

View file

@ -21,6 +21,7 @@ import { DEFAULT_PERCENT_DECIMALS } from '../../common';
import { PieTypeProps } from '../types';
import { toExpressionAst } from '../to_ast';
import { getPieOptions } from '../editor/components';
import { convertToLens } from '../convert_to_lens';
export const getPieVisTypeDefinition = ({
showElasticChartsOptions = false,
@ -123,4 +124,10 @@ export const getPieVisTypeDefinition = ({
},
hierarchicalData: true,
requiresSearch: true,
navigateToLens: async (vis, timefilter) => (vis ? convertToLens(vis, timefilter) : null),
getExpressionVariables: async (vis, timeFilter) => {
return {
canNavigateToLens: Boolean(vis?.params ? await convertToLens(vis, timeFilter) : null),
};
},
});

View file

@ -36,6 +36,70 @@ export const OperationsWithReferences = {
export const Operations = { ...OperationsWithSourceField, ...OperationsWithReferences } as const;
export const PartitionChartTypes = {
PIE: 'pie',
DONUT: 'donut',
TREEMAP: 'treemap',
MOSAIC: 'mosaic',
WAFFLE: 'waffle',
} as const;
export const CategoryDisplayTypes = {
DEFAULT: 'default',
INSIDE: 'inside',
HIDE: 'hide',
} as const;
export const NumberDisplayTypes = {
HIDDEN: 'hidden',
PERCENT: 'percent',
VALUE: 'value',
} as const;
export const LegendDisplayTypes = {
DEFAULT: 'default',
SHOW: 'show',
HIDE: 'hide',
} as const;
export const LayerTypes = {
DATA: 'data',
REFERENCELINE: 'referenceLine',
ANNOTATIONS: 'annotations',
} as const;
export const XYCurveTypes = {
LINEAR: 'LINEAR',
CURVE_MONOTONE_X: 'CURVE_MONOTONE_X',
CURVE_STEP_AFTER: 'CURVE_STEP_AFTER',
} as const;
export const YAxisModes = {
AUTO: 'auto',
LEFT: 'left',
RIGHT: 'right',
BOTTOM: 'bottom',
} as const;
export const SeriesTypes = {
BAR: 'bar',
LINE: 'line',
AREA: 'area',
BAR_STACKED: 'bar_stacked',
AREA_STACKED: 'area_stacked',
BAR_HORIZONTAL: 'bar_horizontal',
BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked',
BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked',
AREA_PERCENTAGE_STACKED: 'area_percentage_stacked',
BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked',
} as const;
export const FillTypes = {
NONE: 'none',
ABOVE: 'above',
BELOW: 'below',
} as const;
export const RANGE_MODES = {
Range: 'range',
Histogram: 'histogram',

View file

@ -11,43 +11,27 @@ import { $Values } from '@kbn/utility-types';
import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
import { KibanaQueryOutput } from '@kbn/data-plugin/common';
import { LegendSize } from '../../constants';
export const XYCurveTypes = {
LINEAR: 'LINEAR',
CURVE_MONOTONE_X: 'CURVE_MONOTONE_X',
CURVE_STEP_AFTER: 'CURVE_STEP_AFTER',
} as const;
export const YAxisModes = {
AUTO: 'auto',
LEFT: 'left',
RIGHT: 'right',
BOTTOM: 'bottom',
} as const;
export const SeriesTypes = {
BAR: 'bar',
LINE: 'line',
AREA: 'area',
BAR_STACKED: 'bar_stacked',
AREA_STACKED: 'area_stacked',
BAR_HORIZONTAL: 'bar_horizontal',
BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked',
BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked',
AREA_PERCENTAGE_STACKED: 'area_percentage_stacked',
BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked',
} as const;
export const FillTypes = {
NONE: 'none',
ABOVE: 'above',
BELOW: 'below',
} as const;
import {
CategoryDisplayTypes,
PartitionChartTypes,
NumberDisplayTypes,
LegendDisplayTypes,
FillTypes,
SeriesTypes,
YAxisModes,
XYCurveTypes,
LayerTypes,
} from '../constants';
export type FillType = $Values<typeof FillTypes>;
export type SeriesType = $Values<typeof SeriesTypes>;
export type YAxisMode = $Values<typeof YAxisModes>;
export type XYCurveType = $Values<typeof XYCurveTypes>;
export type PartitionChartType = $Values<typeof PartitionChartTypes>;
export type CategoryDisplayType = $Values<typeof CategoryDisplayTypes>;
export type NumberDisplayType = $Values<typeof NumberDisplayTypes>;
export type LegendDisplayType = $Values<typeof LegendDisplayTypes>;
export type LayerType = $Values<typeof LayerTypes>;
export interface AxisExtentConfig {
mode: 'full' | 'custom' | 'dataBounds';
@ -217,4 +201,34 @@ export interface MetricVisConfiguration {
maxCols?: number;
}
export type Configuration = XYConfiguration | TableVisConfiguration | MetricVisConfiguration;
export interface PartitionLayerState {
layerId: string;
layerType: LayerType;
primaryGroups: string[];
secondaryGroups?: string[];
metric?: string;
collapseFns?: Record<string, string>;
numberDisplay: NumberDisplayType;
categoryDisplay: CategoryDisplayType;
legendDisplay: LegendDisplayType;
legendPosition?: Position;
showValuesInLegend?: boolean;
nestedLegend?: boolean;
percentDecimals?: number;
emptySizeRatio?: number;
legendMaxLines?: number;
legendSize?: LegendSize;
truncateLegend?: boolean;
}
export interface PartitionVisConfiguration {
shape: PartitionChartType;
layers: PartitionLayerState[];
palette?: PaletteOutput;
}
export type Configuration =
| XYConfiguration
| TableVisConfiguration
| PartitionVisConfiguration
| MetricVisConfiguration;

View file

@ -24,14 +24,26 @@ import {
sortColumns,
} from './utils';
const areVisSchemasValid = (visSchemas: Schemas, unsupported: Array<keyof Schemas>) => {
const usedUnsupportedSchemas = unsupported.filter(
(schema) => visSchemas[schema] && visSchemas[schema]?.length
);
return !usedUnsupportedSchemas.length;
};
export const getColumnsFromVis = <T>(
vis: Vis<T>,
timefilter: TimefilterContract,
dataView: DataView,
{ splits, buckets }: { splits: Array<keyof Schemas>; buckets: Array<keyof Schemas> } = {
splits: [],
buckets: [],
},
{
splits = [],
buckets = [],
unsupported = [],
}: {
splits?: Array<keyof Schemas>;
buckets?: Array<keyof Schemas>;
unsupported?: Array<keyof Schemas>;
} = {},
config?: {
dropEmptyRowsInDateHistogram?: boolean;
}
@ -41,7 +53,7 @@ export const getColumnsFromVis = <T>(
timeRange: timefilter.getAbsoluteTime(),
});
if (!isValidVis(visSchemas)) {
if (!isValidVis(visSchemas) || !areVisSchemasValid(visSchemas, unsupported)) {
return null;
}
@ -111,8 +123,8 @@ export const getColumnsFromVis = <T>(
const columnsWithoutReferenced = getColumnsWithoutReferenced(columns);
return {
metrics: getColumnIds(metrics),
buckets: getColumnIds([...bucketColumns, ...splitBucketColumns, ...customBucketColumns]),
metrics: getColumnIds(columnsWithoutReferenced.filter((с) => !с.isBucketed)),
buckets: getColumnIds(columnsWithoutReferenced.filter((c) => c.isBucketed)),
bucketCollapseFn: getBucketCollapseFn(visSchemas.metric, customBucketColumns),
columnsWithoutReferenced,
columns,

View file

@ -96,6 +96,7 @@ const TopNav = ({
[doReload]
);
const uiStateJSON = useMemo(() => vis.uiState.toJSON(), [vis.uiState]);
useEffect(() => {
const asyncGetTriggerContext = async () => {
if (vis.type.navigateToLens) {
@ -107,7 +108,14 @@ const TopNav = ({
}
};
asyncGetTriggerContext();
}, [services.data.query.timefilter.timefilter, vis, vis.type, vis.params, vis.data.indexPattern]);
}, [
services.data.query.timefilter.timefilter,
vis,
vis.type,
vis.params,
uiStateJSON?.vis,
vis.data.indexPattern,
]);
const displayEditInLensItem = Boolean(vis.type.navigateToLens && editInLensConfig);
const config = useMemo(() => {

View file

@ -534,12 +534,8 @@ function getTopSuggestion(
);
});
// We prefer unchanged or reduced suggestions when switching
// charts since that allows you to switch from A to B and back
// to A with the greatest chance of preserving your original state.
return (
suggestions.find((s) => s.changeType === 'unchanged') ||
suggestions.find((s) => s.changeType === 'reduced') ||
suggestions.find((s) => s.changeType === 'unchanged' || s.changeType === 'reduced') ||
suggestions[0]
);
}

View file

@ -1774,7 +1774,7 @@ describe('IndexPattern Data Source suggestions', () => {
state: expect.objectContaining({
layers: {
test: expect.objectContaining({
columnOrder: ['column-id-3', 'column-id-2', 'column-id-1'],
columnOrder: ['column-id-2', 'column-id-3', 'column-id-1'],
columns: {
'column-id-1': expect.objectContaining({
operationType: 'count',
@ -1804,10 +1804,10 @@ describe('IndexPattern Data Source suggestions', () => {
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'column-id-3',
columnId: 'column-id-2',
}),
expect.objectContaining({
columnId: 'column-id-2',
columnId: 'column-id-3',
}),
expect.objectContaining({
columnId: 'column-id-1',

View file

@ -341,6 +341,7 @@ function createNewLayerWithMetricAggregationFromVizEditor(
newLayer = insertNewColumn({
...column,
layer: newLayer,
respectOrder: true,
});
}
});

View file

@ -68,6 +68,7 @@ interface ColumnChange {
columnParams?: Record<string, unknown>;
initialParams?: { params: Record<string, unknown> }; // TODO: bind this to the op parameter
references?: Array<Omit<ColumnChange, 'layer'>>;
respectOrder?: boolean;
}
interface ColumnCopy {
@ -362,6 +363,7 @@ export function insertNewColumn({
columnParams,
initialParams,
references,
respectOrder,
}: ColumnChange): IndexPatternLayer {
const operationDefinition = operationDefinitionMap[op];
@ -394,7 +396,14 @@ export function insertNewColumn({
: operationDefinition.buildColumn({ ...baseOptions, layer });
return updateDefaultLabels(
addOperationFn(layer, buildColumnFn, columnId, visualizationGroups, targetGroup),
addOperationFn(
layer,
buildColumnFn,
columnId,
visualizationGroups,
targetGroup,
respectOrder
),
indexPattern
);
}
@ -445,7 +454,14 @@ export function insertNewColumn({
)
: operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, referenceIds });
return updateDefaultLabels(
addOperationFn(tempLayer, buildColumnFn, columnId, visualizationGroups, targetGroup),
addOperationFn(
tempLayer,
buildColumnFn,
columnId,
visualizationGroups,
targetGroup,
respectOrder
),
indexPattern
);
}
@ -468,7 +484,8 @@ export function insertNewColumn({
operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }),
columnId,
visualizationGroups,
targetGroup
targetGroup,
respectOrder
),
indexPattern
);
@ -508,7 +525,7 @@ export function insertNewColumn({
const isBucketed = Boolean(possibleOperation.isBucketed);
const addOperationFn = isBucketed ? addBucket : addMetric;
return updateDefaultLabels(
addOperationFn(layer, newColumn, columnId, visualizationGroups, targetGroup),
addOperationFn(layer, newColumn, columnId, visualizationGroups, targetGroup, respectOrder),
indexPattern
);
}
@ -1154,7 +1171,8 @@ function addBucket(
column: BaseIndexPatternColumn,
addedColumnId: string,
visualizationGroups: VisualizationDimensionGroupConfig[],
targetGroup?: string
targetGroup?: string,
respectOrder?: boolean
): IndexPatternLayer {
const [buckets, metrics] = partition(
layer.columnOrder,
@ -1166,7 +1184,7 @@ function addBucket(
);
let updatedColumnOrder: string[] = [];
if (oldDateHistogramIndex > -1 && column.operationType === 'terms') {
if (oldDateHistogramIndex > -1 && column.operationType === 'terms' && !respectOrder) {
// Insert the new terms bucket above the first date histogram
updatedColumnOrder = [
...buckets.slice(0, oldDateHistogramIndex),

View file

@ -35,7 +35,7 @@ export const visualizeAggBasedVisAction = (application: ApplicationStart) =>
type: ACTION_CONVERT_TO_LENS,
payload,
originatingApp: i18n.translate('xpack.lens.AggBasedLabel', {
defaultMessage: 'Aggregation based visualization',
defaultMessage: 'aggregation based visualization',
}),
},
});

View file

@ -17,7 +17,7 @@ import type {
IInterpreterRenderHandlers,
Datatable,
} from '@kbn/expressions-plugin/public';
import type { NavigateToLensContext } from '@kbn/visualizations-plugin/common';
import type { Configuration, NavigateToLensContext } from '@kbn/visualizations-plugin/common';
import { Adapters } from '@kbn/inspector-plugin/public';
import type { Query } from '@kbn/es-query';
import type {
@ -217,13 +217,13 @@ export interface InitializationOptions {
isFullEditor?: boolean;
}
export type VisualizeEditorContext = {
export type VisualizeEditorContext<T extends Configuration = Configuration> = {
savedObjectId?: string;
embeddableId?: string;
vizEditorOriginatingAppUrl?: string;
originatingApp?: string;
isVisualizeAction: boolean;
} & NavigateToLensContext;
} & NavigateToLensContext<T>;
export interface GetDropPropsArgs<T = unknown> {
state: T;
@ -1129,7 +1129,7 @@ export interface Visualization<T = unknown, P = unknown> {
getSuggestionFromConvertToLensContext?: (
props: VisualizationStateFromContextChangeProps
) => Suggestion;
) => Suggestion | undefined;
}
// Use same technique as TriggerContext

View file

@ -161,11 +161,19 @@ export const getDatatableVisualization = ({
},
});
const changeType = table.changeType;
const changeFactor =
changeType === 'reduced' || changeType === 'layers'
? 0.3
: changeType === 'unchanged'
? 0.5
: 1;
return [
{
title,
// table with >= 10 columns will have a score of 0.4, fewer columns reduce score
score: (Math.min(table.columns.length, 10) / 10) * 0.4,
score: (Math.min(table.columns.length, 10) / 10) * 0.4 * changeFactor,
state: {
...(state || {}),
layerId: table.layerId,

View file

@ -93,7 +93,7 @@ describe('suggestions', () => {
).toHaveLength(0);
});
it('should reject date operations', () => {
it('should hide date operations', () => {
expect(
suggestions({
table: {
@ -118,11 +118,17 @@ describe('suggestions', () => {
},
state: undefined,
keptLayerIds: ['first'],
})
).toHaveLength(0);
}).map((s) => [s.hide, s.score])
).toEqual([
[true, 0],
[true, 0],
[true, 0],
[true, 0],
[true, 0],
]);
});
it('should reject histogram operations', () => {
it('should hide histogram operations', () => {
expect(
suggestions({
table: {
@ -147,8 +153,14 @@ describe('suggestions', () => {
},
state: undefined,
keptLayerIds: ['first'],
})
).toHaveLength(0);
}).map((s) => [s.hide, s.score])
).toEqual([
[true, 0],
[true, 0],
[true, 0],
[true, 0],
[true, 0],
]);
});
it('should not reject histogram operations in case of switching between partition charts', () => {

View file

@ -29,15 +29,10 @@ function hasIntervalScale(columns: TableSuggestionColumn[]) {
}
function shouldReject({ table, keptLayerIds, state }: SuggestionRequest<PieVisualizationState>) {
// Histograms are not good for pi. But we should not reject them on switching between partition charts.
const shouldRejectIntervals =
state?.shape && isPartitionShape(state.shape) ? false : hasIntervalScale(table.columns);
return (
keptLayerIds.length > 1 ||
(keptLayerIds.length && table.layerId !== keptLayerIds[0]) ||
table.changeType === 'reorder' ||
shouldRejectIntervals ||
table.columns.some((col) => col.operation.isStaticValue)
);
}
@ -111,6 +106,10 @@ export function suggestions({
const results: Array<VisualizationSuggestion<PieVisualizationState>> = [];
// Histograms are not good for pi. But we should not hide suggestion on switching between partition charts.
const shouldHideSuggestion =
state?.shape && isPartitionShape(state.shape) ? false : hasIntervalScale(table.columns);
if (
groups.length <= PartitionChartsMeta.pie.maxBuckets &&
!hasCustomSuggestionsExists(subVisualizationId)
@ -309,11 +308,11 @@ export function suggestions({
return [...results]
.map((suggestion) => ({
...suggestion,
score: suggestion.score + 0.05 * groups.length,
score: shouldHideSuggestion ? 0 : suggestion.score + 0.05 * groups.length,
}))
.sort((a, b) => b.score - a.score)
.map((suggestion) => ({
...suggestion,
hide: incompleteConfiguration || suggestion.hide,
hide: shouldHideSuggestion || incompleteConfiguration || suggestion.hide,
}));
}

View file

@ -14,11 +14,14 @@ import { ThemeServiceStart } from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
import { EuiSpacer } from '@elastic/eui';
import { PartitionVisConfiguration } from '@kbn/visualizations-plugin/common/convert_to_lens';
import type {
Visualization,
OperationMetadata,
AccessorConfig,
VisualizationDimensionGroupConfig,
Suggestion,
VisualizeEditorContext,
} from '../../types';
import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression';
import { CategoryDisplay, layerTypes, LegendDisplay, NumberDisplay } from '../../../common';
@ -27,6 +30,17 @@ import { PartitionChartsMeta } from './partition_charts_meta';
import { DimensionEditor, PieToolbar } from './toolbar';
import { checkTableForContainsSmallValues } from './render_helpers';
import { PieChartTypes, PieLayerState, PieVisualizationState } from '../../../common';
import { IndexPatternLayer } from '../..';
interface DatatableDatasourceState {
[prop: string]: unknown;
layers: IndexPatternLayer[];
}
export interface PartitionSuggestion extends Suggestion {
datasourceState: DatatableDatasourceState;
visualizationState: PieVisualizationState;
}
function newLayerState(layerId: string): PieLayerState {
return {
@ -42,6 +56,12 @@ function newLayerState(layerId: string): PieLayerState {
};
}
function isPartitionVisConfiguration(
context: VisualizeEditorContext
): context is VisualizeEditorContext<PartitionVisConfiguration> {
return context.type === 'lnsPie';
}
const bucketedOperations = (op: OperationMetadata) => op.isBucketed;
const numberMetricOperations = (op: OperationMetadata) =>
!op.isBucketed && op.dataType === 'number' && !op.isStaticValue;
@ -422,6 +442,29 @@ export const getPieVisualization = ({
return warningMessages;
},
getSuggestionFromConvertToLensContext(props) {
const context = props.context;
if (!isPartitionVisConfiguration(context)) {
return;
}
if (!props.suggestions.length) {
return;
}
const suggestionByShape = (props.suggestions as PartitionSuggestion[]).find(
(suggestion) => suggestion.visualizationState.shape === context.configuration.shape
);
if (!suggestionByShape) {
return;
}
return {
...suggestionByShape,
visualizationState: {
...suggestionByShape.visualizationState,
...context.configuration,
},
};
},
getErrorMessages(state) {
const hasTooManyBucketDimensions = state.layers
.map(

View file

@ -615,7 +615,12 @@ function getScore(
changeType: TableChangeType
) {
// Unchanged table suggestions half the score because the underlying data doesn't change
const changeFactor = changeType === 'unchanged' ? 0.5 : 1;
const changeFactor =
changeType === 'reduced' || changeType === 'layers'
? 0.3
: changeType === 'unchanged'
? 0.5
: 1;
// chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score
return (((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3) * changeFactor;
}

View file

@ -86,7 +86,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
loadTestFile(require.resolve('./error_handling'));
loadTestFile(require.resolve('./lens_tagging'));
loadTestFile(require.resolve('./lens_reporting'));
loadTestFile(require.resolve('./tsvb_open_in_lens'));
loadTestFile(require.resolve('./open_in_lens'));
// keep these two last in the group in this order because they are messing with the default saved objects
loadTestFile(require.resolve('./rollup'));
loadTestFile(require.resolve('./no_data'));

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Agg based Vis to Lens', function () {
loadTestFile(require.resolve('./pie'));
});
}

View file

@ -0,0 +1,68 @@
/*
* 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, visEditor, lens, timePicker, header } = getPageObjects([
'visualize',
'lens',
'visEditor',
'timePicker',
'header',
]);
const testSubjects = getService('testSubjects');
const pieChart = getService('pieChart');
describe('Pie', function describeIndexTests() {
const isNewChartsLibraryEnabled = true;
before(async () => {
await visualize.initTests(isNewChartsLibraryEnabled);
});
beforeEach(async () => {
await visualize.navigateToNewAggBasedVisualization();
await visualize.clickPieChart();
await visualize.clickNewSearch();
await timePicker.setDefaultAbsoluteRange();
});
it('should hide the "Edit Visualization in Lens" menu item if no split slices were defined', async () => {
const button = await testSubjects.exists('visualizeEditInLensButton');
expect(button).to.eql(false);
});
it('should show the "Edit Visualization in Lens" menu item', async () => {
await visEditor.clickBucket('Split slices');
await visEditor.selectAggregation('Terms');
await visEditor.selectField('machine.os.raw');
await header.waitUntilLoadingHasFinished();
await visEditor.clickGo(isNewChartsLibraryEnabled);
const button = await testSubjects.exists('visualizeEditInLensButton');
expect(button).to.eql(true);
});
it('should convert to Lens', async () => {
const expectedTableData = ['ios', 'osx', 'win 7', 'win 8', 'win xp'];
await visEditor.clickBucket('Split slices');
await visEditor.selectAggregation('Terms');
await visEditor.selectField('machine.os.raw');
await header.waitUntilLoadingHasFinished();
await visEditor.clickGo(isNewChartsLibraryEnabled);
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('partitionVisChart');
await pieChart.expectPieChartLabels(expectedTableData, isNewChartsLibraryEnabled);
});
});
}

View file

@ -0,0 +1,15 @@
/*
* 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 { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Open in Lens', function () {
loadTestFile(require.resolve('./tsvb'));
loadTestFile(require.resolve('./agg_based'));
});
}

View file

@ -0,0 +1,101 @@
/*
* 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, timeToVisualize, dashboard, canvas } = getPageObjects([
'visualBuilder',
'visualize',
'lens',
'timeToVisualize',
'dashboard',
'canvas',
]);
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const panelActions = getService('dashboardPanelActions');
const dashboardAddPanel = getService('dashboardAddPanel');
describe('Dashboard to TSVB to Lens', function describeIndexTests() {
before(async () => {
await visualize.initTests();
});
it('should convert a by value TSVB viz to a Lens viz', async () => {
await visualize.navigateToNewVisualization();
await visualize.clickVisualBuilder();
await visualBuilder.checkVisualBuilderIsPresent();
await visualBuilder.resetPage();
await testSubjects.click('visualizeSaveButton');
await timeToVisualize.saveFromModal('My TSVB to Lens viz 1', {
addToDashboard: 'new',
saveToLibrary: false,
});
await dashboard.waitForRenderComplete();
const originalEmbeddableCount = await canvas.getEmbeddableCount();
await panelActions.openContextMenu();
await panelActions.clickEdit();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(await dimensions[1].getVisibleText()).to.be('Count of records');
});
await lens.saveAndReturn();
await retry.try(async () => {
const embeddableCount = await canvas.getEmbeddableCount();
expect(embeddableCount).to.eql(originalEmbeddableCount);
});
await panelActions.removePanel();
});
it('should convert a by reference TSVB viz to a Lens viz', async () => {
await dashboardAddPanel.clickEditorMenuButton();
await dashboardAddPanel.clickVisType('metrics');
await testSubjects.click('visualizesaveAndReturnButton');
// save it to library
const originalPanel = await testSubjects.find('embeddablePanelHeading-');
await panelActions.saveToLibrary('My TSVB to Lens viz 2', originalPanel);
await dashboard.waitForRenderComplete();
const originalEmbeddableCount = await canvas.getEmbeddableCount();
await panelActions.openContextMenu();
await panelActions.clickEdit();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('legacyMtrVis');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(await dimensions[1].getVisibleText()).to.be('Count of records');
});
await lens.saveAndReturn();
await retry.try(async () => {
const embeddableCount = await canvas.getEmbeddableCount();
expect(embeddableCount).to.eql(originalEmbeddableCount);
});
const panel = await testSubjects.find(`embeddablePanelHeading-`);
const descendants = await testSubjects.findAllDescendant(
'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION',
panel
);
expect(descendants.length).to.equal(0);
await panelActions.removePanel();
});
});
}

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('TSVB to Lens', function () {
loadTestFile(require.resolve('./metric'));
loadTestFile(require.resolve('./timeseries'));
loadTestFile(require.resolve('./dashboard'));
});
}

View file

@ -0,0 +1,44 @@
/*
* 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 } = getPageObjects(['visualBuilder', 'visualize', 'lens']);
const testSubjects = getService('testSubjects');
describe('Metric', function describeIndexTests() {
before(async () => {
await visualize.initTests();
});
beforeEach(async () => {
await visualize.navigateToNewVisualization();
await visualize.clickVisualBuilder();
await visualBuilder.checkVisualBuilderIsPresent();
await visualBuilder.resetPage();
await visualBuilder.clickMetric();
await visualBuilder.clickDataTab('metric');
});
it('should show the "Edit Visualization in Lens" menu item', async () => {
const button = await testSubjects.exists('visualizeEditInLensButton');
expect(button).to.eql(true);
});
it('should convert to Lens', async () => {
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('mtrVis');
const metricData = await lens.getMetricVisualizationData();
expect(metricData[0].title).to.eql('Count of records');
});
});
}

View file

@ -0,0 +1,87 @@
/*
* 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');
const find = getService('find');
const filterBar = getService('filterBar');
const queryBar = getService('queryBar');
describe('Time Series', function describeIndexTests() {
before(async () => {
await visualize.initTests();
});
it('should show the "Edit Visualization in Lens" menu item for a count aggregation', async () => {
await visualize.navigateToNewVisualization();
await visualize.clickVisualBuilder();
await visualBuilder.checkVisualBuilderIsPresent();
await visualBuilder.resetPage();
const isMenuItemVisible = await find.existsByCssSelector(
'[data-test-subj="visualizeEditInLensButton"]'
);
expect(isMenuItemVisible).to.be(true);
});
it('visualizes field to Lens and loads fields to the dimesion editor', async () => {
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(dimensions).to.have.length(2);
expect(await dimensions[0].getVisibleText()).to.be('@timestamp');
expect(await dimensions[1].getVisibleText()).to.be('Count of records');
});
});
it('navigates back to TSVB when the Back button is clicked', async () => {
const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton');
goBackBtn.click();
await visualBuilder.checkVisualBuilderIsPresent();
await retry.try(async () => {
const actualCount = await visualBuilder.getRhythmChartLegendValue();
expect(actualCount).to.be('56');
});
});
it('should preserve app filters in lens', async () => {
await filterBar.addFilter('extension', 'is', 'css');
await header.waitUntilLoadingHasFinished();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
expect(await filterBar.hasFilter('extension', 'css')).to.be(true);
});
it('should preserve query in lens', async () => {
const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton');
goBackBtn.click();
await visualBuilder.checkVisualBuilderIsPresent();
await queryBar.setQuery('machine.os : ios');
await queryBar.submitQuery();
await header.waitUntilLoadingHasFinished();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
expect(await queryBar.getQueryString()).to.equal('machine.os : ios');
});
});
}

View file

@ -1,192 +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; 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, header, lens, timeToVisualize, dashboard, canvas } =
getPageObjects([
'visualBuilder',
'visualize',
'header',
'lens',
'timeToVisualize',
'dashboard',
'canvas',
]);
const testSubjects = getService('testSubjects');
const find = getService('find');
const dashboardAddPanel = getService('dashboardAddPanel');
const panelActions = getService('dashboardPanelActions');
const retry = getService('retry');
const filterBar = getService('filterBar');
const queryBar = getService('queryBar');
describe('TSVB to Lens', function describeIndexTests() {
before(async () => {
await visualize.initTests();
});
describe('Time Series', () => {
it('should show the "Edit Visualization in Lens" menu item for a count aggregation', async () => {
await visualize.navigateToNewVisualization();
await visualize.clickVisualBuilder();
await visualBuilder.checkVisualBuilderIsPresent();
await visualBuilder.resetPage();
const isMenuItemVisible = await find.existsByCssSelector(
'[data-test-subj="visualizeEditInLensButton"]'
);
expect(isMenuItemVisible).to.be(true);
});
it('visualizes field to Lens and loads fields to the dimesion editor', async () => {
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(dimensions).to.have.length(2);
expect(await dimensions[0].getVisibleText()).to.be('@timestamp');
expect(await dimensions[1].getVisibleText()).to.be('Count of records');
});
});
it('navigates back to TSVB when the Back button is clicked', async () => {
const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton');
goBackBtn.click();
await visualBuilder.checkVisualBuilderIsPresent();
await retry.try(async () => {
const actualCount = await visualBuilder.getRhythmChartLegendValue();
expect(actualCount).to.be('56');
});
});
it('should preserve app filters in lens', async () => {
await filterBar.addFilter('extension', 'is', 'css');
await header.waitUntilLoadingHasFinished();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
expect(await filterBar.hasFilter('extension', 'css')).to.be(true);
});
it('should preserve query in lens', async () => {
const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton');
goBackBtn.click();
await visualBuilder.checkVisualBuilderIsPresent();
await queryBar.setQuery('machine.os : ios');
await queryBar.submitQuery();
await header.waitUntilLoadingHasFinished();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
expect(await queryBar.getQueryString()).to.equal('machine.os : ios');
});
});
describe('Metric', () => {
beforeEach(async () => {
await visualize.navigateToNewVisualization();
await visualize.clickVisualBuilder();
await visualBuilder.checkVisualBuilderIsPresent();
await visualBuilder.resetPage();
await visualBuilder.clickMetric();
await visualBuilder.clickDataTab('metric');
});
it('should show the "Edit Visualization in Lens" menu item', async () => {
const button = await testSubjects.exists('visualizeEditInLensButton');
expect(button).to.eql(true);
});
it('should convert to Lens', async () => {
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('mtrVis');
const metricData = await lens.getMetricVisualizationData();
expect(metricData[0].title).to.eql('Count of records');
});
});
describe('Dashboard to TSVB to Lens', () => {
it('should convert a by value TSVB viz to a Lens viz', async () => {
await visualize.navigateToNewVisualization();
await visualize.clickVisualBuilder();
await visualBuilder.checkVisualBuilderIsPresent();
await visualBuilder.resetPage();
await testSubjects.click('visualizeSaveButton');
await timeToVisualize.saveFromModal('My TSVB to Lens viz 1', {
addToDashboard: 'new',
saveToLibrary: false,
});
await dashboard.waitForRenderComplete();
const originalEmbeddableCount = await canvas.getEmbeddableCount();
await panelActions.openContextMenu();
await panelActions.clickEdit();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('xyVisChart');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(await dimensions[1].getVisibleText()).to.be('Count of records');
});
await lens.saveAndReturn();
await retry.try(async () => {
const embeddableCount = await canvas.getEmbeddableCount();
expect(embeddableCount).to.eql(originalEmbeddableCount);
});
await panelActions.removePanel();
});
it('should convert a by reference TSVB viz to a Lens viz', async () => {
await dashboardAddPanel.clickEditorMenuButton();
await dashboardAddPanel.clickVisType('metrics');
await testSubjects.click('visualizesaveAndReturnButton');
// save it to library
const originalPanel = await testSubjects.find('embeddablePanelHeading-');
await panelActions.saveToLibrary('My TSVB to Lens viz 2', originalPanel);
await dashboard.waitForRenderComplete();
const originalEmbeddableCount = await canvas.getEmbeddableCount();
await panelActions.openContextMenu();
await panelActions.clickEdit();
const button = await testSubjects.find('visualizeEditInLensButton');
await button.click();
await lens.waitForVisualization('legacyMtrVis');
await retry.try(async () => {
const dimensions = await testSubjects.findAll('lns-dimensionTrigger');
expect(await dimensions[1].getVisibleText()).to.be('Count of records');
});
await lens.saveAndReturn();
await retry.try(async () => {
const embeddableCount = await canvas.getEmbeddableCount();
expect(embeddableCount).to.eql(originalEmbeddableCount);
});
const panel = await testSubjects.find(`embeddablePanelHeading-`);
const descendants = await testSubjects.findAllDescendant(
'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION',
panel
);
expect(descendants.length).to.equal(0);
await panelActions.removePanel();
});
});
});
}