mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
b87b1da1cf
commit
ede6a53caa
10 changed files with 222 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
123
x-pack/plugins/lens/public/visualizations/xy/add_layer.test.tsx
Normal file
123
x-pack/plugins/lens/public/visualizations/xy/add_layer.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue