[Lens] Allow users to choose a layer type when adding a new layer (#179532)

## Summary

Fixes https://github.com/elastic/kibana/issues/179135

The PR is ready to be reviewed, but I have to wait for @MichaelMarcialis
input.

Adds options for layer type when adding a new layer:

<img width="436" alt="Screenshot 2024-03-28 at 13 30 00"
src="88c1c56a-4f79-4e8f-a176-f3da5feb3898">

<img width="423" alt="Screenshot 2024-03-28 at 13 29 49"
src="c67884a4-7b7e-40b9-b005-cf627e3a81fb">

---------

Co-authored-by: Marco Vettorello <vettorello.marco@gmail.com>
This commit is contained in:
Marta Bondyra 2024-04-04 20:50:08 +02:00 committed by GitHub
parent b87b1da1cf
commit ede6a53caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 222 additions and 25 deletions

View file

@ -238,10 +238,9 @@ export function LayerPanels(
[dispatchLens, props.framePublicAPI.dataViews.indexPatterns, props.indexPatternService]
);
const addLayer: AddLayerFunction = (layerType, extraArg, ignoreInitialValues) => {
const addLayer: AddLayerFunction = (layerType, extraArg, ignoreInitialValues, seriesType) => {
const layerId = generateId();
dispatchLens(addLayerAction({ layerId, layerType, extraArg, ignoreInitialValues }));
dispatchLens(addLayerAction({ layerId, layerType, extraArg, ignoreInitialValues, seriesType }));
setNextFocusedLayerId(layerId);
};
@ -335,6 +334,7 @@ export function LayerPanels(
})}
{!hideAddLayerButton &&
activeVisualization?.getAddLayerButtonComponent?.({
state: visualization.state,
supportedLayers: activeVisualization.getSupportedLayers(
visualization.state,
props.framePublicAPI

View file

@ -13,6 +13,7 @@ import { History } from 'history';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { EventAnnotationGroupConfig } from '@kbn/event-annotation-common';
import { DragDropIdentifier, DropType } from '@kbn/dom-drag-drop';
import { SeriesType } from '@kbn/visualizations-plugin/common';
import { LensEmbeddableInput } from '..';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import type {
@ -244,6 +245,7 @@ export const addLayer = createAction<{
layerType: LayerType;
extraArg: unknown;
ignoreInitialValues?: boolean;
seriesType?: SeriesType;
}>('lens/addLayer');
export const onDropToDimension = createAction<{
source: DragDropIdentifier;
@ -908,7 +910,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
.addCase(
addLayer,
(state, { payload: { layerId, layerType, extraArg, ignoreInitialValues } }) => {
(state, { payload: { layerId, layerType, extraArg, seriesType, ignoreInitialValues } }) => {
if (!state.activeDatasourceId || !state.visualization.activeId) {
return state;
}
@ -924,7 +926,8 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
layerId,
layerType,
currentDataViewsId,
extraArg
extraArg,
seriesType
);
const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);

View file

@ -17,7 +17,11 @@ import type {
Datatable,
ExpressionRendererEvent,
} from '@kbn/expressions-plugin/public';
import type { Configuration, NavigateToLensContext } from '@kbn/visualizations-plugin/common';
import type {
Configuration,
NavigateToLensContext,
SeriesType,
} from '@kbn/visualizations-plugin/common';
import type { Query } from '@kbn/es-query';
import type {
UiActionsStart,
@ -1000,7 +1004,8 @@ interface VisualizationStateFromContextChangeProps {
export type AddLayerFunction<T = unknown> = (
layerType: LayerType,
extraArg?: T,
ignoreInitialValues?: boolean
ignoreInitialValues?: boolean,
seriesType?: SeriesType
) => void;
export type AnnotationGroups = Record<string, EventAnnotationGroupConfig>;
@ -1024,7 +1029,8 @@ export type RegisterLibraryAnnotationGroupFunction = (groupInfo: {
id: string;
group: EventAnnotationGroupConfig;
}) => void;
interface AddLayerButtonProps {
interface AddLayerButtonProps<T> {
state: T;
supportedLayers: VisualizationLayerDescription[];
addLayer: AddLayerFunction;
ensureIndexPattern: (specOrId: DataViewSpec | string) => Promise<void>;
@ -1110,7 +1116,8 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
layerId: string,
type: LayerType,
indexPatternId: string,
extraArg?: ExtraAppendLayerArg
extraArg?: ExtraAppendLayerArg,
seriesType?: SeriesType
) => T;
/** Retrieve a list of supported layer types with initialization data */
@ -1254,8 +1261,8 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
label: string;
}) => null | ReactElement<{ columnId: string; label: string }>;
getAddLayerButtonComponent?: (
props: AddLayerButtonProps
) => null | ReactElement<AddLayerButtonProps>;
props: AddLayerButtonProps<T>
) => null | ReactElement<AddLayerButtonProps<T>>;
/**
* Creates map of columns ids and unique lables. Used only for noDatasource layers
*/

View file

@ -0,0 +1,123 @@
/*
* 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 React from 'react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { AddLayerButton } from './add_layer';
import { XYState } from './types';
import { Position } from '@elastic/charts';
import { LayerTypes } from '@kbn/visualizations-plugin/common';
import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks';
import { IconChartBarAnnotations } from '@kbn/chart-icons';
describe('AddLayerButton', () => {
const addLayer = jest.fn();
const renderAddLayerButton = () => {
const state: XYState = {
legend: { position: Position.Bottom, isVisible: true },
valueLabels: 'show',
preferredSeriesType: 'bar',
layers: [
{
layerId: 'first',
layerType: LayerTypes.DATA,
seriesType: 'area',
splitAccessor: 'd',
xAccessor: 'a',
accessors: ['b', 'c'],
},
],
};
const supportedLayers = [
{
type: LayerTypes.DATA,
label: 'Visualization',
},
{
type: LayerTypes.REFERENCELINE,
label: LayerTypes.REFERENCELINE,
},
{
type: LayerTypes.ANNOTATIONS,
label: 'Annotations',
icon: IconChartBarAnnotations,
disabled: true,
},
];
const rtlRender = render(
<AddLayerButton
state={state}
supportedLayers={supportedLayers}
addLayer={addLayer}
eventAnnotationService={eventAnnotationServiceMock}
/>
);
return {
...rtlRender,
clickAddLayer: () => {
fireEvent.click(screen.getByLabelText('Add layer'));
},
clickVisualizationButton: () => {
fireEvent.click(screen.getByRole('button', { name: 'Visualization' }));
},
clickSeriesOptionsButton: (seriesType = 'line') => {
const lineOption = screen.getByTestId(`lnsXY_seriesType-${seriesType}`);
fireEvent.click(lineOption);
},
waitForSeriesOptions: async () => {
await waitFor(() => {
expect(screen.queryByTestId('lnsXY_seriesType-area')).toBeInTheDocument();
});
},
getSeriesTypeOptions: () => {
return within(
screen.getByTestId('contextMenuPanelTitleButton').parentElement as HTMLElement
)
.getAllByRole('button')
.map((el) => el.textContent);
},
};
};
afterEach(() => {
jest.clearAllMocks();
});
it('renders all compatible series types', async () => {
const { clickAddLayer, clickVisualizationButton, waitForSeriesOptions, getSeriesTypeOptions } =
renderAddLayerButton();
clickAddLayer();
clickVisualizationButton();
await waitForSeriesOptions();
expect(getSeriesTypeOptions()).toEqual([
'Select visualization type',
'Bar vertical',
'Bar vertical stacked',
'Bar vertical percentage',
'Area',
'Area stacked',
'Area percentage',
'Line',
]);
});
it('calls addLayer with a proper series type when button is clicked', async () => {
const {
clickAddLayer,
clickVisualizationButton,
waitForSeriesOptions,
clickSeriesOptionsButton,
} = renderAddLayerButton();
clickAddLayer();
clickVisualizationButton();
await waitForSeriesOptions();
clickSeriesOptionsButton('line');
expect(addLayer).toHaveBeenCalledWith(LayerTypes.DATA, undefined, undefined, 'line');
});
});

View file

@ -20,16 +20,27 @@ import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'
import { AddLayerFunction, VisualizationLayerDescription } from '../../types';
import { LoadAnnotationLibraryFlyout } from './load_annotation_library_flyout';
import type { ExtraAppendLayerArg } from './visualization';
import { SeriesType, XYState, visualizationTypes } from './types';
import { isHorizontalChart, isHorizontalSeries } from './state_helpers';
import { getDataLayers } from './visualization_helpers';
import { ExperimentalBadge } from '../../shared_components';
interface AddLayerButtonProps {
state: XYState;
supportedLayers: VisualizationLayerDescription[];
addLayer: AddLayerFunction<ExtraAppendLayerArg>;
eventAnnotationService: EventAnnotationServiceType;
isInlineEditing?: boolean;
}
export enum AddLayerPanelType {
main = 'main',
selectAnnotationMethod = 'selectAnnotationMethod',
selectVisualizationType = 'selectVisualizationType',
}
export function AddLayerButton({
state,
supportedLayers,
addLayer,
eventAnnotationService,
@ -47,7 +58,7 @@ export function AddLayerButton({
toolTipContent,
}: typeof supportedLayers[0]) => {
return {
panel: 1,
panel: AddLayerPanelType.selectAnnotationMethod,
toolTipContent,
disabled,
name: (
@ -66,6 +77,33 @@ export function AddLayerButton({
};
};
const dataPanel = ({
type,
label,
icon,
disabled,
toolTipContent,
}: typeof supportedLayers[0]) => {
return {
panel: AddLayerPanelType.selectVisualizationType,
toolTipContent,
disabled,
name: <span className="lnsLayerAddButtonLabel">{label}</span>,
className: 'lnsLayerAddButton',
icon: icon && <EuiIcon size="m" type={icon} />,
['data-test-subj']: `lnsLayerAddButton-${type}`,
};
};
const horizontalOnly = isHorizontalChart(state.layers);
const availableVisTypes = visualizationTypes.filter(
(t) => isHorizontalSeries(t.id as SeriesType) === horizontalOnly
);
const currentLayerVisType =
availableVisTypes.findIndex((t) => t.id === getDataLayers(state.layers)?.[0]?.seriesType) || 0;
return (
<>
<EuiPopover
@ -93,10 +131,11 @@ export function AddLayerButton({
panelPaddingSize="none"
>
<EuiContextMenu
initialPanelId={0}
initialPanelId={AddLayerPanelType.main}
size="s"
panels={[
{
id: 0,
id: AddLayerPanelType.main,
title: i18n.translate('xpack.lens.configPanel.selectLayerType', {
defaultMessage: 'Select layer type',
}),
@ -105,6 +144,8 @@ export function AddLayerButton({
const { type, label, icon, disabled, toolTipContent } = props;
if (type === LayerTypes.ANNOTATIONS) {
return annotationPanel(props);
} else if (type === LayerTypes.DATA) {
return dataPanel(props);
}
return {
toolTipContent,
@ -121,7 +162,7 @@ export function AddLayerButton({
}),
},
{
id: 1,
id: AddLayerPanelType.selectAnnotationMethod,
initialFocusedItemIndex: 0,
title: i18n.translate('xpack.lens.configPanel.selectAnnotationMethod', {
defaultMessage: 'Select annotation method',
@ -151,6 +192,22 @@ export function AddLayerButton({
},
],
},
{
id: AddLayerPanelType.selectVisualizationType,
initialFocusedItemIndex: currentLayerVisType,
title: i18n.translate('xpack.lens.layerPanel.selectVisualizationType', {
defaultMessage: 'Select visualization type',
}),
items: availableVisTypes.map((t) => ({
name: t.fullLabel || t.label,
icon: t.icon && <EuiIcon size="m" type={t.icon} />,
onClick: () => {
addLayer(LayerTypes.DATA, undefined, undefined, t.id as SeriesType);
toggleLayersChoice(false);
},
'data-test-subj': `lnsXY_seriesType-${t.id}`,
})),
},
]}
/>
</EuiPopover>

View file

@ -208,20 +208,20 @@ export const getXyVisualization = ({
return state;
},
appendLayer(state, layerId, layerType, indexPatternId, extraArg) {
appendLayer(state, layerId, layerType, indexPatternId, extraArg, seriesType) {
if (layerType === 'metricTrendline') {
return state;
}
const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType;
return {
...state,
layers: [
...state.layers,
newLayerState({
seriesType: firstUsedSeriesType || state.preferredSeriesType,
layerId,
layerType,
seriesType:
seriesType || getDataLayers(state.layers)?.[0]?.seriesType || state.preferredSeriesType,
indexPatternId,
extraArg,
}),
@ -734,7 +734,7 @@ export const getXyVisualization = ({
<AddLayerButton
{...props}
eventAnnotationService={eventAnnotationService}
addLayer={async (type, loadedGroupInfo) => {
addLayer={async (type, loadedGroupInfo, _, seriesType) => {
if (type === LayerTypes.ANNOTATIONS && loadedGroupInfo) {
await props.ensureIndexPattern(
loadedGroupInfo.dataViewSpec ?? loadedGroupInfo.indexPatternId
@ -745,8 +745,7 @@ export const getXyVisualization = ({
group: loadedGroupInfo,
});
}
props.addLayer(type, loadedGroupInfo, !!loadedGroupInfo);
props.addLayer(type, loadedGroupInfo, !!loadedGroupInfo, seriesType);
}}
/>
);

View file

@ -164,7 +164,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
await PageObjects.lens.createLayer();
await PageObjects.lens.createLayer('data', undefined, 'bar');
expect(await PageObjects.lens.getLayerType(1)).to.eql('Bar vertical');
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',

View file

@ -988,7 +988,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
*/
async createLayer(
layerType: 'data' | 'referenceLine' | 'annotations' = 'data',
annotationFromLibraryTitle?: string
annotationFromLibraryTitle?: string,
seriesType = 'bar_stacked'
) {
await testSubjects.click('lnsLayerAddButton');
const layerCount = await this.getLayerCount();
@ -1003,6 +1004,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
if (await testSubjects.exists(`lnsLayerAddButton-${layerType}`)) {
await testSubjects.click(`lnsLayerAddButton-${layerType}`);
if (layerType === 'data') {
await testSubjects.click(`lnsXY_seriesType-${seriesType}`);
}
if (layerType === 'annotations') {
if (!annotationFromLibraryTitle) {
await testSubjects.click('lnsAnnotationLayer_new');

View file

@ -336,7 +336,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
await PageObjects.lens.createLayer();
await PageObjects.lens.createLayer('data', undefined, 'bar');
expect(await PageObjects.lens.getLayerType(1)).to.eql(termTranslator('bar'));
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',

View file

@ -302,7 +302,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
await PageObjects.lens.createLayer();
await PageObjects.lens.createLayer('data', undefined, 'bar');
expect(await PageObjects.lens.getLayerType(1)).to.eql('Bar vertical');
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',