mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* [Lens] Add clear layer feature * Move clear / remove layer out of the context menu * Address code review comments * Remove xpack.lens.xyChart.deleteLayer translation * Get rid of unused Lens translations Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Chris Davies <github@christophilus.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
69c8aa4595
commit
f1afb6f826
27 changed files with 1019 additions and 640 deletions
|
@ -72,6 +72,42 @@ describe('Datatable Visualization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getLayerIds', () => {
|
||||
it('return the layer ids', () => {
|
||||
const state: DatatableVisualizationState = {
|
||||
layers: [
|
||||
{
|
||||
layerId: 'baz',
|
||||
columns: ['a', 'b', 'c'],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(datatableVisualization.getLayerIds(state)).toEqual(['baz']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearLayer', () => {
|
||||
it('should reset the layer', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce('testid');
|
||||
const state: DatatableVisualizationState = {
|
||||
layers: [
|
||||
{
|
||||
layerId: 'baz',
|
||||
columns: ['a', 'b', 'c'],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(datatableVisualization.clearLayer(state, 'baz')).toMatchObject({
|
||||
layers: [
|
||||
{
|
||||
layerId: 'baz',
|
||||
columns: ['testid'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSuggestions', () => {
|
||||
function numCol(columnId: string): TableSuggestionColumn {
|
||||
return {
|
||||
|
@ -188,6 +224,7 @@ describe('Datatable Visualization', () => {
|
|||
|
||||
mount(
|
||||
<DataTableLayer
|
||||
layerId="layer1"
|
||||
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
|
||||
frame={frame}
|
||||
layer={layer}
|
||||
|
@ -224,6 +261,7 @@ describe('Datatable Visualization', () => {
|
|||
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
||||
const component = mount(
|
||||
<DataTableLayer
|
||||
layerId="layer1"
|
||||
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
|
||||
frame={frame}
|
||||
layer={layer}
|
||||
|
@ -258,6 +296,7 @@ describe('Datatable Visualization', () => {
|
|||
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
||||
const component = mount(
|
||||
<DataTableLayer
|
||||
layerId="layer1"
|
||||
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
|
||||
frame={frame}
|
||||
layer={layer}
|
||||
|
@ -290,6 +329,7 @@ describe('Datatable Visualization', () => {
|
|||
frame.datasourceLayers = { a: datasource.publicAPIMock };
|
||||
const component = mount(
|
||||
<DataTableLayer
|
||||
layerId="layer1"
|
||||
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
|
||||
frame={frame}
|
||||
layer={layer}
|
||||
|
|
|
@ -6,19 +6,18 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { EuiForm, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { MultiColumnEditor } from '../multi_column_editor';
|
||||
import {
|
||||
SuggestionRequest,
|
||||
Visualization,
|
||||
VisualizationProps,
|
||||
VisualizationLayerConfigProps,
|
||||
VisualizationSuggestion,
|
||||
Operation,
|
||||
} from '../types';
|
||||
import { generateId } from '../id_generator';
|
||||
import { NativeRenderer } from '../native_renderer';
|
||||
import chartTableSVG from '../assets/chart_datatable.svg';
|
||||
|
||||
export interface LayerState {
|
||||
|
@ -56,7 +55,7 @@ export function DataTableLayer({
|
|||
state,
|
||||
setState,
|
||||
dragDropContext,
|
||||
}: { layer: LayerState } & VisualizationProps<DatatableVisualizationState>) {
|
||||
}: { layer: LayerState } & VisualizationLayerConfigProps<DatatableVisualizationState>) {
|
||||
const datasource = frame.datasourceLayers[layer.layerId];
|
||||
|
||||
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
|
||||
|
@ -64,32 +63,24 @@ export function DataTableLayer({
|
|||
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
|
||||
|
||||
return (
|
||||
<EuiPanel className="lnsConfigPanel__panel" paddingSize="s">
|
||||
<NativeRenderer
|
||||
render={datasource.renderLayerPanel}
|
||||
nativeProps={{ layerId: layer.layerId }}
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.datatable.columns', { defaultMessage: 'Columns' })}
|
||||
>
|
||||
<MultiColumnEditor
|
||||
accessors={sortedColumns}
|
||||
datasource={datasource}
|
||||
dragDropContext={dragDropContext}
|
||||
filterOperations={allOperations}
|
||||
layerId={layer.layerId}
|
||||
onAdd={() => setState(updateColumns(state, layer, columns => [...columns, generateId()]))}
|
||||
onRemove={column =>
|
||||
setState(updateColumns(state, layer, columns => columns.filter(c => c !== column)))
|
||||
}
|
||||
testSubj="datatable_columns"
|
||||
data-test-subj="datatable_multicolumnEditor"
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.datatable.columns', { defaultMessage: 'Columns' })}
|
||||
>
|
||||
<MultiColumnEditor
|
||||
accessors={sortedColumns}
|
||||
datasource={datasource}
|
||||
dragDropContext={dragDropContext}
|
||||
filterOperations={allOperations}
|
||||
layerId={layer.layerId}
|
||||
onAdd={() => setState(updateColumns(state, layer, columns => [...columns, generateId()]))}
|
||||
onRemove={column =>
|
||||
setState(updateColumns(state, layer, columns => columns.filter(c => c !== column)))
|
||||
}
|
||||
testSubj="datatable_columns"
|
||||
data-test-subj="datatable_multicolumnEditor"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -110,7 +101,17 @@ export const datatableVisualization: Visualization<
|
|||
},
|
||||
],
|
||||
|
||||
getDescription(state) {
|
||||
getLayerIds(state) {
|
||||
return state.layers.map(l => l.layerId);
|
||||
},
|
||||
|
||||
clearLayer(state) {
|
||||
return {
|
||||
layers: state.layers.map(l => newLayerState(l.layerId)),
|
||||
};
|
||||
},
|
||||
|
||||
getDescription() {
|
||||
return {
|
||||
icon: chartTableSVG,
|
||||
label: i18n.translate('xpack.lens.datatable.label', {
|
||||
|
@ -187,17 +188,18 @@ export const datatableVisualization: Visualization<
|
|||
];
|
||||
},
|
||||
|
||||
renderConfigPanel: (domElement, props) =>
|
||||
render(
|
||||
<I18nProvider>
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{props.state.layers.map(layer => (
|
||||
<DataTableLayer key={layer.layerId} layer={layer} {...props} />
|
||||
))}
|
||||
</EuiForm>
|
||||
</I18nProvider>,
|
||||
domElement
|
||||
),
|
||||
renderLayerConfigPanel(domElement, props) {
|
||||
const layer = props.state.layers.find(l => l.layerId === props.layerId);
|
||||
|
||||
if (layer) {
|
||||
render(
|
||||
<I18nProvider>
|
||||
<DataTableLayer {...props} layer={layer} />
|
||||
</I18nProvider>,
|
||||
domElement
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
toExpression(state, frame) {
|
||||
const layer = state.layers[0];
|
||||
|
|
|
@ -81,7 +81,6 @@ export function ChartSwitch(props: Props) {
|
|||
trackUiEvent(`chart_switch`);
|
||||
|
||||
switchToSuggestion(
|
||||
props.framePublicAPI,
|
||||
props.dispatch,
|
||||
{
|
||||
...selection,
|
||||
|
|
|
@ -4,14 +4,36 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useContext, memo } from 'react';
|
||||
import React, { useMemo, useContext, memo, useState } from 'react';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiToolTip,
|
||||
EuiButton,
|
||||
EuiForm,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NativeRenderer } from '../../native_renderer';
|
||||
import { Action } from './state_management';
|
||||
import { Visualization, FramePublicAPI, Datasource } from '../../types';
|
||||
import {
|
||||
Visualization,
|
||||
FramePublicAPI,
|
||||
Datasource,
|
||||
VisualizationLayerConfigProps,
|
||||
} from '../../types';
|
||||
import { DragContext } from '../../drag_drop';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { generateId } from '../../id_generator';
|
||||
import { removeLayer, appendLayer } from './layer_actions';
|
||||
|
||||
interface ConfigPanelWrapperProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
activeVisualizationId: string | null;
|
||||
|
@ -28,17 +50,8 @@ interface ConfigPanelWrapperProps {
|
|||
}
|
||||
|
||||
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
|
||||
const context = useContext(DragContext);
|
||||
const setVisualizationState = useMemo(
|
||||
() => (newState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch]
|
||||
);
|
||||
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
|
||||
const { visualizationState } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -52,19 +65,235 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config
|
|||
dispatch={props.dispatch}
|
||||
framePublicAPI={props.framePublicAPI}
|
||||
/>
|
||||
{props.activeVisualizationId && props.visualizationState !== null && (
|
||||
<div className="lnsConfigPanelWrapper">
|
||||
<NativeRenderer
|
||||
render={props.visualizationMap[props.activeVisualizationId].renderConfigPanel}
|
||||
nativeProps={{
|
||||
dragDropContext: context,
|
||||
state: props.visualizationState,
|
||||
setState: setVisualizationState,
|
||||
frame: props.framePublicAPI,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{activeVisualization && visualizationState && (
|
||||
<LayerPanels {...props} activeVisualization={activeVisualization} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function LayerPanels(
|
||||
props: ConfigPanelWrapperProps & {
|
||||
activeDatasourceId: string;
|
||||
activeVisualization: Visualization;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
framePublicAPI,
|
||||
activeVisualization,
|
||||
visualizationState,
|
||||
dispatch,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
} = props;
|
||||
const dragDropContext = useContext(DragContext);
|
||||
const setState = useMemo(
|
||||
() => (newState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch, activeVisualization]
|
||||
);
|
||||
const layerIds = activeVisualization.getLayerIds(visualizationState);
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{layerIds.map(layerId => (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
activeVisualization={activeVisualization}
|
||||
dragDropContext={dragDropContext}
|
||||
state={setState}
|
||||
setState={setState}
|
||||
frame={framePublicAPI}
|
||||
isOnlyLayer={layerIds.length === 1}
|
||||
onRemove={() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'REMOVE_OR_CLEAR_LAYER',
|
||||
updater: state =>
|
||||
removeLayer({
|
||||
activeVisualization,
|
||||
layerId,
|
||||
trackUiEvent,
|
||||
datasourceMap,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{activeVisualization.appendLayer && (
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiToolTip
|
||||
className="eui-fullWidth"
|
||||
content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', {
|
||||
defaultMessage:
|
||||
'Use multiple layers to combine chart types or visualize different index patterns.',
|
||||
})}
|
||||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
className="lnsConfigPanel__addLayerBtn"
|
||||
fullWidth
|
||||
size="s"
|
||||
data-test-subj={`lnsXY_layer_add`}
|
||||
aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.xyChart.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'ADD_LAYER',
|
||||
updater: state =>
|
||||
appendLayer({
|
||||
activeVisualization,
|
||||
generateId,
|
||||
trackUiEvent,
|
||||
activeDatasource: datasourceMap[activeDatasourceId],
|
||||
state,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
iconType="plusInCircleFilled"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
function LayerPanel(
|
||||
props: ConfigPanelWrapperProps &
|
||||
VisualizationLayerConfigProps<unknown> & {
|
||||
isOnlyLayer: boolean;
|
||||
activeVisualization: Visualization;
|
||||
onRemove: () => void;
|
||||
}
|
||||
) {
|
||||
const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props;
|
||||
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
|
||||
const layerConfigProps = {
|
||||
layerId,
|
||||
dragDropContext: props.dragDropContext,
|
||||
state: props.visualizationState,
|
||||
setState: props.setState,
|
||||
frame: props.framePublicAPI,
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel className="lnsConfigPanel__panel" paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LayerSettings
|
||||
layerId={layerId}
|
||||
layerConfigProps={layerConfigProps}
|
||||
activeVisualization={activeVisualization}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{datasourcePublicAPI && (
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<NativeRenderer
|
||||
render={datasourcePublicAPI.renderLayerPanel}
|
||||
nativeProps={{ layerId }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderLayerConfigPanel}
|
||||
nativeProps={layerConfigProps}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="lns_layer_remove"
|
||||
onClick={() => {
|
||||
// If we don't blur the remove / clear button, it remains focused
|
||||
// which is a strange UX in this case. e.target.blur doesn't work
|
||||
// due to who knows what, but probably event re-writing. Additionally,
|
||||
// activeElement does not have blur so, we need to do some casting + safeguards.
|
||||
const el = (document.activeElement as unknown) as { blur: () => void };
|
||||
|
||||
if (el && el.blur) {
|
||||
el.blur();
|
||||
}
|
||||
|
||||
onRemove();
|
||||
}}
|
||||
>
|
||||
{isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayer', {
|
||||
defaultMessage: 'Reset layer',
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayer', {
|
||||
defaultMessage: 'Delete layer',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function LayerSettings({
|
||||
layerId,
|
||||
activeVisualization,
|
||||
layerConfigProps,
|
||||
}: {
|
||||
layerId: string;
|
||||
activeVisualization: Visualization;
|
||||
layerConfigProps: VisualizationLayerConfigProps;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (!activeVisualization.renderLayerContextMenu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={`lnsLayerPopover_${layerId}`}
|
||||
panelPaddingSize="s"
|
||||
ownFocus
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType={activeVisualization.getLayerContextMenuIcon?.(layerConfigProps) || 'gear'}
|
||||
aria-label={i18n.translate('xpack.lens.editLayerSettings', {
|
||||
defaultMessage: 'Edit layer settings',
|
||||
})}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
data-test-subj="lns_layer_settings"
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
anchorPosition="leftUp"
|
||||
>
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderLayerContextMenu}
|
||||
nativeProps={layerConfigProps}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ReactWrapper } from 'enzyme';
|
|||
import { EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
|
||||
import { EditorFrame } from './editor_frame';
|
||||
import { Visualization, DatasourcePublicAPI, DatasourceSuggestion } from '../../types';
|
||||
import { DatasourcePublicAPI, DatasourceSuggestion, Visualization } from '../../types';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import {
|
||||
|
@ -24,7 +24,11 @@ import { FrameLayout } from './frame_layout';
|
|||
|
||||
// calling this function will wait for all pending Promises from mock
|
||||
// datasources to be processed by its callers.
|
||||
const waitForPromises = () => new Promise(resolve => setTimeout(resolve));
|
||||
async function waitForPromises(n = 3) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function generateSuggestion(state = {}): DatasourceSuggestion {
|
||||
return {
|
||||
|
@ -88,6 +92,9 @@ describe('editor_frame', () => {
|
|||
],
|
||||
};
|
||||
|
||||
mockVisualization.getLayerIds.mockReturnValue(['first']);
|
||||
mockVisualization2.getLayerIds.mockReturnValue(['second']);
|
||||
|
||||
mockDatasource = createMockDatasource();
|
||||
mockDatasource2 = createMockDatasource();
|
||||
|
||||
|
@ -202,7 +209,7 @@ describe('editor_frame', () => {
|
|||
);
|
||||
});
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).not.toHaveBeenCalled();
|
||||
expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled();
|
||||
expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -294,6 +301,7 @@ describe('editor_frame', () => {
|
|||
|
||||
it('should remove layer on active datasource on frame api call', async () => {
|
||||
const initialState = { datasource2: '' };
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState));
|
||||
mockDatasource2.getLayers.mockReturnValue(['abc', 'def']);
|
||||
mockDatasource2.removeLayer.mockReturnValue({ removed: true });
|
||||
|
@ -361,7 +369,7 @@ describe('editor_frame', () => {
|
|||
|
||||
it('should initialize visualization state and render config panel', async () => {
|
||||
const initialState = {};
|
||||
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
|
@ -382,7 +390,7 @@ describe('editor_frame', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({ state: initialState })
|
||||
);
|
||||
|
@ -390,6 +398,7 @@ describe('editor_frame', () => {
|
|||
|
||||
it('should render the resulting expression using the expression renderer', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
|
||||
const instance = mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
|
@ -508,7 +517,6 @@ describe('editor_frame', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
|
||||
instance.update();
|
||||
|
@ -601,6 +609,7 @@ describe('editor_frame', () => {
|
|||
|
||||
describe('state update', () => {
|
||||
it('should re-render config panel after state update', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
|
@ -619,14 +628,14 @@ describe('editor_frame', () => {
|
|||
await waitForPromises();
|
||||
|
||||
const updatedState = {};
|
||||
const setVisualizationState = (mockVisualization.renderConfigPanel as jest.Mock).mock
|
||||
const setVisualizationState = (mockVisualization.renderLayerConfigPanel as jest.Mock).mock
|
||||
.calls[0][1].setState;
|
||||
act(() => {
|
||||
setVisualizationState(updatedState);
|
||||
});
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenLastCalledWith(
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
state: updatedState,
|
||||
|
@ -635,6 +644,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
it('should re-render data panel after state update', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
|
@ -689,10 +699,13 @@ describe('editor_frame', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
const updatedPublicAPI = {};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(
|
||||
(updatedPublicAPI as unknown) as DatasourcePublicAPI
|
||||
);
|
||||
const updatedPublicAPI: DatasourcePublicAPI = {
|
||||
renderLayerPanel: jest.fn(),
|
||||
renderDimensionPanel: jest.fn(),
|
||||
getOperationForColumnId: jest.fn(),
|
||||
getTableSpec: jest.fn(),
|
||||
};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
|
||||
|
||||
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
|
||||
.setState;
|
||||
|
@ -700,8 +713,8 @@ describe('editor_frame', () => {
|
|||
setDatasourceState({});
|
||||
});
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenLastCalledWith(
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
frame: expect.objectContaining({
|
||||
|
@ -754,10 +767,10 @@ describe('editor_frame', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalled();
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalled();
|
||||
|
||||
const datasourceLayers =
|
||||
mockVisualization.renderConfigPanel.mock.calls[0][1].frame.datasourceLayers;
|
||||
mockVisualization.renderLayerConfigPanel.mock.calls[0][1].frame.datasourceLayers;
|
||||
expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock);
|
||||
expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock);
|
||||
expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock);
|
||||
|
@ -919,7 +932,7 @@ describe('editor_frame', () => {
|
|||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mockDatasource.getLayers.mockReturnValue(['first', 'second']);
|
||||
mockDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
|
||||
{
|
||||
state: {},
|
||||
|
@ -1018,7 +1031,7 @@ describe('editor_frame', () => {
|
|||
|
||||
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
|
||||
expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState);
|
||||
expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({ state: { initial: true } })
|
||||
);
|
||||
|
@ -1032,9 +1045,11 @@ describe('editor_frame', () => {
|
|||
expect(mockDatasource.publicAPIMock.getTableSpec).toHaveBeenCalled();
|
||||
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
|
||||
expect(mockVisualization2.initialize).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ datasourceLayers: { first: mockDatasource.publicAPIMock } })
|
||||
expect.objectContaining({
|
||||
datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }),
|
||||
})
|
||||
);
|
||||
expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({ state: { initial: true } })
|
||||
);
|
||||
|
@ -1102,6 +1117,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
it('should display top 5 suggestions in descending order', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
const instance = mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
|
@ -1185,6 +1201,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
it('should switch to suggested visualization', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
|
||||
const newDatasourceState = {};
|
||||
const suggestionVisState = {};
|
||||
const instance = mount(
|
||||
|
@ -1228,8 +1245,8 @@ describe('editor_frame', () => {
|
|||
.simulate('click');
|
||||
});
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(1);
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(1);
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
state: suggestionVisState,
|
||||
|
@ -1244,6 +1261,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
it('should switch to best suggested visualization on field drop', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
const suggestionVisState = {};
|
||||
const instance = mount(
|
||||
<EditorFrame
|
||||
|
@ -1293,7 +1311,7 @@ describe('editor_frame', () => {
|
|||
.simulate('drop');
|
||||
});
|
||||
|
||||
expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
state: suggestionVisState,
|
||||
|
@ -1302,6 +1320,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
it('should use the currently selected visualization if possible on field drop', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
|
||||
const suggestionVisState = {};
|
||||
const instance = mount(
|
||||
<EditorFrame
|
||||
|
@ -1366,7 +1385,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
state: suggestionVisState,
|
||||
|
@ -1375,10 +1394,12 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
it('should use the highest priority suggestion available', async () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
|
||||
const suggestionVisState = {};
|
||||
const mockVisualization3 = {
|
||||
...createMockVisualization(),
|
||||
id: 'testVis3',
|
||||
getLayerIds: () => ['third'],
|
||||
visualizationTypes: [
|
||||
{
|
||||
icon: 'empty',
|
||||
|
@ -1460,7 +1481,7 @@ describe('editor_frame', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(mockVisualization3.renderConfigPanel).toHaveBeenCalledWith(
|
||||
expect(mockVisualization3.renderLayerConfigPanel).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
state: suggestionVisState,
|
||||
|
@ -1633,13 +1654,16 @@ describe('editor_frame', () => {
|
|||
await waitForPromises();
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
(instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: () => ({
|
||||
newState: true,
|
||||
}),
|
||||
datasourceId: 'testDatasource',
|
||||
act(() => {
|
||||
(instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: () => ({
|
||||
newState: true,
|
||||
}),
|
||||
datasourceId: 'testDatasource',
|
||||
});
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(3);
|
||||
|
|
|
@ -52,6 +52,8 @@ export interface EditorFrameProps {
|
|||
export function EditorFrame(props: EditorFrameProps) {
|
||||
const [state, dispatch] = useReducer(reducer, props, getInitialState);
|
||||
const { onError } = props;
|
||||
const activeVisualization =
|
||||
state.visualization.activeId && props.visualizationMap[state.visualization.activeId];
|
||||
|
||||
const allLoaded = Object.values(state.datasourceStates).every(
|
||||
({ isLoading }) => typeof isLoading === 'boolean' && !isLoading
|
||||
|
@ -125,7 +127,20 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
|
||||
return newLayerId;
|
||||
},
|
||||
removeLayers: (layerIds: string[]) => {
|
||||
|
||||
removeLayers(layerIds: string[]) {
|
||||
if (activeVisualization && activeVisualization.removeLayer && state.visualization.state) {
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
newState: layerIds.reduce(
|
||||
(acc, layerId) =>
|
||||
activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
|
||||
state.visualization.state
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
layerIds.forEach(layerId => {
|
||||
const layerDatasourceId = Object.entries(props.datasourceMap).find(
|
||||
([datasourceId, datasource]) =>
|
||||
|
@ -158,16 +173,15 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
|
||||
// Initialize visualization as soon as all datasources are ready
|
||||
useEffect(() => {
|
||||
if (allLoaded && state.visualization.state === null && state.visualization.activeId !== null) {
|
||||
const initialVisualizationState = props.visualizationMap[
|
||||
state.visualization.activeId
|
||||
].initialize(framePublicAPI);
|
||||
if (allLoaded && state.visualization.state === null && activeVisualization) {
|
||||
const initialVisualizationState = activeVisualization.initialize(framePublicAPI);
|
||||
dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
newState: initialVisualizationState,
|
||||
});
|
||||
}
|
||||
}, [allLoaded, state.visualization.activeId, state.visualization.state]);
|
||||
}, [allLoaded, activeVisualization, state.visualization.state]);
|
||||
|
||||
// The frame needs to call onChange every time its internal state changes
|
||||
useEffect(() => {
|
||||
|
@ -176,11 +190,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
? props.datasourceMap[state.activeDatasourceId]
|
||||
: undefined;
|
||||
|
||||
const visualization = state.visualization.activeId
|
||||
? props.visualizationMap[state.visualization.activeId]
|
||||
: undefined;
|
||||
|
||||
if (!activeDatasource || !visualization) {
|
||||
if (!activeDatasource || !activeVisualization) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -208,13 +218,14 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
}),
|
||||
{}
|
||||
),
|
||||
visualization,
|
||||
visualization: activeVisualization,
|
||||
state,
|
||||
framePublicAPI,
|
||||
});
|
||||
|
||||
props.onChange({ filterableIndexPatterns: indexPatterns, doc });
|
||||
}, [
|
||||
activeVisualization,
|
||||
state.datasourceStates,
|
||||
state.visualization,
|
||||
props.query,
|
||||
|
@ -248,6 +259,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
configPanel={
|
||||
allLoaded && (
|
||||
<ConfigPanelWrapper
|
||||
activeDatasourceId={state.activeDatasourceId!}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={state.datasourceStates}
|
||||
visualizationMap={props.visualizationMap}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { removeLayer, appendLayer } from './layer_actions';
|
||||
|
||||
function createTestArgs(initialLayerIds: string[]) {
|
||||
const trackUiEvent = jest.fn();
|
||||
const testDatasource = (datasourceId: string) => ({
|
||||
id: datasourceId,
|
||||
clearLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).map((id: string) =>
|
||||
id === layerId ? `${datasourceId}_clear_${layerId}` : id
|
||||
),
|
||||
removeLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
||||
insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||
});
|
||||
|
||||
const activeVisualization = {
|
||||
clearLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).map((id: string) => (id === layerId ? `vis_clear_${layerId}` : id)),
|
||||
removeLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
||||
getLayerIds: (layerIds: unknown) => layerIds as string[],
|
||||
appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||
};
|
||||
|
||||
return {
|
||||
state: {
|
||||
activeDatasourceId: 'ds1',
|
||||
datasourceStates: {
|
||||
ds1: {
|
||||
isLoading: false,
|
||||
state: initialLayerIds.slice(0, 1),
|
||||
},
|
||||
ds2: {
|
||||
isLoading: false,
|
||||
state: initialLayerIds.slice(1),
|
||||
},
|
||||
},
|
||||
title: 'foo',
|
||||
visualization: {
|
||||
activeId: 'vis1',
|
||||
state: initialLayerIds,
|
||||
},
|
||||
},
|
||||
activeVisualization,
|
||||
datasourceMap: {
|
||||
ds1: testDatasource('ds1'),
|
||||
ds2: testDatasource('ds2'),
|
||||
},
|
||||
trackUiEvent,
|
||||
};
|
||||
}
|
||||
|
||||
describe('removeLayer', () => {
|
||||
it('should clear the layer if it is the only layer', () => {
|
||||
const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs(['layer1']);
|
||||
const newState = removeLayer({
|
||||
activeVisualization,
|
||||
datasourceMap,
|
||||
layerId: 'layer1',
|
||||
state,
|
||||
trackUiEvent,
|
||||
});
|
||||
|
||||
expect(newState.visualization.state).toEqual(['vis_clear_layer1']);
|
||||
expect(newState.datasourceStates.ds1.state).toEqual(['ds1_clear_layer1']);
|
||||
expect(newState.datasourceStates.ds2.state).toEqual([]);
|
||||
expect(trackUiEvent).toHaveBeenCalledWith('layer_cleared');
|
||||
});
|
||||
|
||||
it('should remove the layer if it is not the only layer', () => {
|
||||
const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([
|
||||
'layer1',
|
||||
'layer2',
|
||||
]);
|
||||
const newState = removeLayer({
|
||||
activeVisualization,
|
||||
datasourceMap,
|
||||
layerId: 'layer1',
|
||||
state,
|
||||
trackUiEvent,
|
||||
});
|
||||
|
||||
expect(newState.visualization.state).toEqual(['layer2']);
|
||||
expect(newState.datasourceStates.ds1.state).toEqual([]);
|
||||
expect(newState.datasourceStates.ds2.state).toEqual(['layer2']);
|
||||
expect(trackUiEvent).toHaveBeenCalledWith('layer_removed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendLayer', () => {
|
||||
it('should add the layer to the datasource and visualization', () => {
|
||||
const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([
|
||||
'layer1',
|
||||
'layer2',
|
||||
]);
|
||||
const newState = appendLayer({
|
||||
activeDatasource: datasourceMap.ds1,
|
||||
activeVisualization,
|
||||
generateId: () => 'foo',
|
||||
state,
|
||||
trackUiEvent,
|
||||
});
|
||||
|
||||
expect(newState.visualization.state).toEqual(['layer1', 'layer2', 'foo']);
|
||||
expect(newState.datasourceStates.ds1.state).toEqual(['layer1', 'foo']);
|
||||
expect(newState.datasourceStates.ds2.state).toEqual(['layer2']);
|
||||
expect(trackUiEvent).toHaveBeenCalledWith('layer_added');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { EditorFrameState } from './state_management';
|
||||
import { Datasource, Visualization } from '../../types';
|
||||
|
||||
interface RemoveLayerOptions {
|
||||
trackUiEvent: (name: string) => void;
|
||||
state: EditorFrameState;
|
||||
layerId: string;
|
||||
activeVisualization: Pick<Visualization, 'getLayerIds' | 'clearLayer' | 'removeLayer'>;
|
||||
datasourceMap: Record<string, Pick<Datasource, 'clearLayer' | 'removeLayer'>>;
|
||||
}
|
||||
|
||||
interface AppendLayerOptions {
|
||||
trackUiEvent: (name: string) => void;
|
||||
state: EditorFrameState;
|
||||
generateId: () => string;
|
||||
activeDatasource: Pick<Datasource, 'insertLayer' | 'id'>;
|
||||
activeVisualization: Pick<Visualization, 'appendLayer'>;
|
||||
}
|
||||
|
||||
export function removeLayer(opts: RemoveLayerOptions): EditorFrameState {
|
||||
const { state, trackUiEvent: trackUiEvent, activeVisualization, layerId, datasourceMap } = opts;
|
||||
const isOnlyLayer = activeVisualization
|
||||
.getLayerIds(state.visualization.state)
|
||||
.every(id => id === opts.layerId);
|
||||
|
||||
trackUiEvent(isOnlyLayer ? 'layer_cleared' : 'layer_removed');
|
||||
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: _.mapValues(state.datasourceStates, (datasourceState, datasourceId) => {
|
||||
const datasource = datasourceMap[datasourceId!];
|
||||
return {
|
||||
...datasourceState,
|
||||
state: isOnlyLayer
|
||||
? datasource.clearLayer(datasourceState.state, layerId)
|
||||
: datasource.removeLayer(datasourceState.state, layerId),
|
||||
};
|
||||
}),
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
state:
|
||||
isOnlyLayer || !activeVisualization.removeLayer
|
||||
? activeVisualization.clearLayer(state.visualization.state, layerId)
|
||||
: activeVisualization.removeLayer(state.visualization.state, layerId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function appendLayer({
|
||||
trackUiEvent,
|
||||
activeVisualization,
|
||||
state,
|
||||
generateId,
|
||||
activeDatasource,
|
||||
}: AppendLayerOptions): EditorFrameState {
|
||||
trackUiEvent('layer_added');
|
||||
|
||||
if (!activeVisualization.appendLayer) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const layerId = generateId();
|
||||
|
||||
return {
|
||||
...state,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
[activeDatasource.id]: {
|
||||
...state.datasourceStates[activeDatasource.id],
|
||||
state: activeDatasource.insertLayer(
|
||||
state.datasourceStates[activeDatasource.id].state,
|
||||
layerId
|
||||
),
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
state: activeVisualization.appendLayer(state.visualization.state, layerId),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -119,6 +119,7 @@ describe('editor_frame state management', () => {
|
|||
},
|
||||
{
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: 'testVis',
|
||||
newState: newVisState,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -31,6 +31,13 @@ export type Action =
|
|||
type: 'UPDATE_TITLE';
|
||||
title: string;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_STATE';
|
||||
// Just for diagnostics, so we can determine what action
|
||||
// caused this update.
|
||||
subType: string;
|
||||
updater: (prevState: EditorFrameState) => EditorFrameState;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_DATASOURCE_STATE';
|
||||
updater: unknown | ((prevState: unknown) => unknown);
|
||||
|
@ -39,6 +46,7 @@ export type Action =
|
|||
}
|
||||
| {
|
||||
type: 'UPDATE_VISUALIZATION_STATE';
|
||||
visualizationId: string;
|
||||
newState: unknown;
|
||||
clearStagedPreview?: boolean;
|
||||
}
|
||||
|
@ -128,6 +136,8 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
|||
return action.state;
|
||||
case 'UPDATE_TITLE':
|
||||
return { ...state, title: action.title };
|
||||
case 'UPDATE_STATE':
|
||||
return action.updater(state);
|
||||
case 'UPDATE_LAYER':
|
||||
return {
|
||||
...state,
|
||||
|
@ -249,6 +259,12 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
|
|||
if (!state.visualization.activeId) {
|
||||
throw new Error('Invariant: visualization state got updated without active visualization');
|
||||
}
|
||||
// This is a safeguard that prevents us from accidentally updating the
|
||||
// wrong visualization. This occurs in some cases due to the uncoordinated
|
||||
// way we manage state across plugins.
|
||||
if (state.visualization.activeId !== action.visualizationId) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
visualization: {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
|
|||
import {
|
||||
Visualization,
|
||||
Datasource,
|
||||
FramePublicAPI,
|
||||
TableChangeType,
|
||||
TableSuggestion,
|
||||
DatasourceSuggestion,
|
||||
|
@ -130,7 +129,6 @@ function getVisualizationSuggestions(
|
|||
}
|
||||
|
||||
export function switchToSuggestion(
|
||||
frame: FramePublicAPI,
|
||||
dispatch: (action: Action) => void,
|
||||
suggestion: Pick<
|
||||
Suggestion,
|
||||
|
@ -145,5 +143,6 @@ export function switchToSuggestion(
|
|||
datasourceState: suggestion.datasourceState,
|
||||
datasourceId: suggestion.datasourceId!,
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
}
|
||||
|
|
|
@ -320,7 +320,7 @@ export function SuggestionPanel({
|
|||
} else {
|
||||
trackSuggestionEvent(`position_${index}_of_${suggestions.length}`);
|
||||
setLastSelectedSuggestion(index);
|
||||
switchToSuggestion(frame, dispatch, suggestion);
|
||||
switchToSuggestion(dispatch, suggestion);
|
||||
}
|
||||
}}
|
||||
selected={index === lastSelectedSuggestion}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { ExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public';
|
||||
import { Visualization, FramePublicAPI, TableSuggestion } from '../../types';
|
||||
import { FramePublicAPI, TableSuggestion, Visualization } from '../../types';
|
||||
import {
|
||||
createMockVisualization,
|
||||
createMockDatasource,
|
||||
|
|
|
@ -126,12 +126,7 @@ export function InnerWorkspacePanel({
|
|||
if (suggestionForDraggedField) {
|
||||
trackUiEvent('drop_onto_workspace');
|
||||
trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty');
|
||||
switchToSuggestion(
|
||||
framePublicAPI,
|
||||
dispatch,
|
||||
suggestionForDraggedField,
|
||||
'SWITCH_VISUALIZATION'
|
||||
);
|
||||
switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,14 @@ import {
|
|||
import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
|
||||
import { DatasourcePublicAPI, FramePublicAPI, Visualization, Datasource } from '../types';
|
||||
import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types';
|
||||
import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './plugin';
|
||||
|
||||
export function createMockVisualization(): jest.Mocked<Visualization> {
|
||||
return {
|
||||
id: 'TEST_VIS',
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
getLayerIds: jest.fn(_state => ['layer1']),
|
||||
visualizationTypes: [
|
||||
{
|
||||
icon: 'empty',
|
||||
|
@ -32,7 +34,7 @@ export function createMockVisualization(): jest.Mocked<Visualization> {
|
|||
getPersistableState: jest.fn(_state => _state),
|
||||
getSuggestions: jest.fn(_options => []),
|
||||
initialize: jest.fn((_frame, _state?) => ({})),
|
||||
renderConfigPanel: jest.fn(),
|
||||
renderLayerConfigPanel: jest.fn(),
|
||||
toExpression: jest.fn((_state, _frame) => null),
|
||||
toPreviewExpression: jest.fn((_state, _frame) => null),
|
||||
};
|
||||
|
@ -52,7 +54,8 @@ export function createMockDatasource(): DatasourceMock {
|
|||
|
||||
return {
|
||||
id: 'mockindexpattern',
|
||||
getDatasourceSuggestionsForField: jest.fn((_state, item) => []),
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
|
||||
getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []),
|
||||
getPersistableState: jest.fn(),
|
||||
getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
|
||||
|
|
|
@ -132,11 +132,7 @@ export function getIndexPatternDatasource({
|
|||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[newLayerId]: {
|
||||
indexPatternId: state.currentIndexPatternId,
|
||||
columns: {},
|
||||
columnOrder: [],
|
||||
},
|
||||
[newLayerId]: blankLayer(state.currentIndexPatternId),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -151,6 +147,16 @@ export function getIndexPatternDatasource({
|
|||
};
|
||||
},
|
||||
|
||||
clearLayer(state: IndexPatternPrivateState, layerId: string) {
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: blankLayer(state.currentIndexPatternId),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
getLayers(state: IndexPatternPrivateState) {
|
||||
return Object.keys(state.layers);
|
||||
},
|
||||
|
@ -280,3 +286,11 @@ export function getIndexPatternDatasource({
|
|||
|
||||
return indexPatternDatasource;
|
||||
}
|
||||
|
||||
function blankLayer(indexPatternId: string) {
|
||||
return {
|
||||
indexPatternId,
|
||||
columns: {},
|
||||
columnOrder: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ describe('MetricConfigPanel', () => {
|
|||
const state = testState();
|
||||
const component = mount(
|
||||
<MetricConfigPanel
|
||||
layerId="bar"
|
||||
dragDropContext={dragDropContext}
|
||||
setState={jest.fn()}
|
||||
frame={{
|
||||
|
|
|
@ -6,41 +6,34 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { State } from './types';
|
||||
import { VisualizationProps, OperationMetadata } from '../types';
|
||||
import { VisualizationLayerConfigProps, OperationMetadata } from '../types';
|
||||
import { NativeRenderer } from '../native_renderer';
|
||||
|
||||
const isMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number';
|
||||
|
||||
export function MetricConfigPanel(props: VisualizationProps<State>) {
|
||||
const { state, frame } = props;
|
||||
const [datasource] = Object.values(frame.datasourceLayers);
|
||||
const [layerId] = Object.keys(frame.datasourceLayers);
|
||||
export function MetricConfigPanel(props: VisualizationLayerConfigProps<State>) {
|
||||
const { state, frame, layerId } = props;
|
||||
const datasource = frame.datasourceLayers[layerId];
|
||||
|
||||
return (
|
||||
<EuiPanel className="lnsConfigPanel__panel" paddingSize="s">
|
||||
<NativeRenderer render={datasource.renderLayerPanel} nativeProps={{ layerId }} />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.metric.valueLabel', {
|
||||
defaultMessage: 'Value',
|
||||
})}
|
||||
>
|
||||
<NativeRenderer
|
||||
data-test-subj={'lns_metric_valueDimensionPanel'}
|
||||
render={datasource.renderDimensionPanel}
|
||||
nativeProps={{
|
||||
layerId,
|
||||
columnId: state.accessor,
|
||||
dragDropContext: props.dragDropContext,
|
||||
filterOperations: isMetric,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.metric.valueLabel', {
|
||||
defaultMessage: 'Value',
|
||||
})}
|
||||
>
|
||||
<NativeRenderer
|
||||
data-test-subj={'lns_metric_valueDimensionPanel'}
|
||||
render={datasource.renderDimensionPanel}
|
||||
nativeProps={{
|
||||
layerId,
|
||||
columnId: state.accessor,
|
||||
dragDropContext: props.dragDropContext,
|
||||
filterOperations: isMetric,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,22 @@ describe('metric_visualization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getLayerIds', () => {
|
||||
it('returns the layer id', () => {
|
||||
expect(metricVisualization.getLayerIds(exampleState())).toEqual(['l1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearLayer', () => {
|
||||
it('returns a clean layer', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce('test-id1');
|
||||
expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({
|
||||
accessor: 'test-id1',
|
||||
layerId: 'l1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getPersistableState', () => {
|
||||
it('persists the state as given', () => {
|
||||
expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState());
|
||||
|
|
|
@ -54,6 +54,17 @@ export const metricVisualization: Visualization<State, PersistableState> = {
|
|||
},
|
||||
],
|
||||
|
||||
clearLayer(state) {
|
||||
return {
|
||||
...state,
|
||||
accessor: generateId(),
|
||||
};
|
||||
},
|
||||
|
||||
getLayerIds(state) {
|
||||
return [state.layerId];
|
||||
},
|
||||
|
||||
getDescription() {
|
||||
return {
|
||||
icon: chartMetricSVG,
|
||||
|
@ -76,7 +87,7 @@ export const metricVisualization: Visualization<State, PersistableState> = {
|
|||
|
||||
getPersistableState: state => state,
|
||||
|
||||
renderConfigPanel: (domElement, props) =>
|
||||
renderLayerConfigPanel: (domElement, props) =>
|
||||
render(
|
||||
<I18nProvider>
|
||||
<MetricConfigPanel {...props} />
|
||||
|
|
|
@ -135,6 +135,7 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
|
||||
insertLayer: (state: T, newLayerId: string) => T;
|
||||
removeLayer: (state: T, layerId: string) => T;
|
||||
clearLayer: (state: T, layerId: string) => T;
|
||||
getLayers: (state: T) => string[];
|
||||
|
||||
renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps<T>) => void;
|
||||
|
@ -237,7 +238,8 @@ export interface LensMultiTable {
|
|||
};
|
||||
}
|
||||
|
||||
export interface VisualizationProps<T = unknown> {
|
||||
export interface VisualizationLayerConfigProps<T = unknown> {
|
||||
layerId: string;
|
||||
dragDropContext: DragContextState;
|
||||
frame: FramePublicAPI;
|
||||
state: T;
|
||||
|
@ -325,6 +327,18 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
|
||||
visualizationTypes: VisualizationType[];
|
||||
|
||||
getLayerIds: (state: T) => string[];
|
||||
|
||||
clearLayer: (state: T, layerId: string) => T;
|
||||
|
||||
removeLayer?: (state: T, layerId: string) => T;
|
||||
|
||||
appendLayer?: (state: T, layerId: string) => T;
|
||||
|
||||
getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined;
|
||||
|
||||
renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerConfigProps<T>) => void;
|
||||
|
||||
getDescription: (
|
||||
state: T
|
||||
) => {
|
||||
|
@ -339,7 +353,7 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
|
||||
getPersistableState: (state: T) => P;
|
||||
|
||||
renderConfigPanel: (domElement: Element, props: VisualizationProps<T>) => void;
|
||||
renderLayerConfigPanel: (domElement: Element, props: VisualizationLayerConfigProps<T>) => void;
|
||||
|
||||
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ import React from 'react';
|
|||
import { ReactWrapper } from 'enzyme';
|
||||
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
|
||||
import { EuiButtonGroupProps } from '@elastic/eui';
|
||||
import { XYConfigPanel } from './xy_config_panel';
|
||||
import { XYConfigPanel, LayerContextMenu } from './xy_config_panel';
|
||||
import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types';
|
||||
import { State, XYState } from './types';
|
||||
import { State } from './types';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { NativeRendererProps } from '../native_renderer';
|
||||
import { generateId } from '../id_generator';
|
||||
|
@ -46,15 +46,6 @@ describe('XYConfigPanel', () => {
|
|||
.props();
|
||||
}
|
||||
|
||||
function openComponentPopover(component: ReactWrapper<unknown>, layerId: string) {
|
||||
component
|
||||
.find(`[data-test-subj="lnsXY_layer_${layerId}"]`)
|
||||
.first()
|
||||
.find(`[data-test-subj="lnsXY_layer_advanced"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
frame = createMockFramePublicAPI();
|
||||
frame.datasourceLayers = {
|
||||
|
@ -67,55 +58,55 @@ describe('XYConfigPanel', () => {
|
|||
test.skip('allows toggling the y axis gridlines', () => {});
|
||||
test.skip('allows toggling the x axis gridlines', () => {});
|
||||
|
||||
test('enables stacked chart types even when there is no split series', () => {
|
||||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{ ...state, layers: [{ ...state.layers[0], xAccessor: 'shazm' }] }}
|
||||
/>
|
||||
);
|
||||
describe('LayerContextMenu', () => {
|
||||
test('enables stacked chart types even when there is no split series', () => {
|
||||
const state = testState();
|
||||
const component = mount(
|
||||
<LayerContextMenu
|
||||
layerId={state.layers[0].layerId}
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{ ...state, layers: [{ ...state.layers[0], xAccessor: 'shazm' }] }}
|
||||
/>
|
||||
);
|
||||
|
||||
openComponentPopover(component, 'first');
|
||||
const options = component
|
||||
.find('[data-test-subj="lnsXY_seriesType"]')
|
||||
.first()
|
||||
.prop('options') as EuiButtonGroupProps['options'];
|
||||
|
||||
const options = component
|
||||
.find('[data-test-subj="lnsXY_seriesType"]')
|
||||
.first()
|
||||
.prop('options') as EuiButtonGroupProps['options'];
|
||||
expect(options!.map(({ id }) => id)).toEqual([
|
||||
'bar',
|
||||
'bar_stacked',
|
||||
'line',
|
||||
'area',
|
||||
'area_stacked',
|
||||
]);
|
||||
|
||||
expect(options!.map(({ id }) => id)).toEqual([
|
||||
'bar',
|
||||
'bar_stacked',
|
||||
'line',
|
||||
'area',
|
||||
'area_stacked',
|
||||
]);
|
||||
expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
|
||||
});
|
||||
|
||||
expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
|
||||
});
|
||||
test('shows only horizontal bar options when in horizontal mode', () => {
|
||||
const state = testState();
|
||||
const component = mount(
|
||||
<LayerContextMenu
|
||||
layerId={state.layers[0].layerId}
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{ ...state, layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }] }}
|
||||
/>
|
||||
);
|
||||
|
||||
test('shows only horizontal bar options when in horizontal mode', () => {
|
||||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{ ...state, layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }] }}
|
||||
/>
|
||||
);
|
||||
const options = component
|
||||
.find('[data-test-subj="lnsXY_seriesType"]')
|
||||
.first()
|
||||
.prop('options') as EuiButtonGroupProps['options'];
|
||||
|
||||
openComponentPopover(component, 'first');
|
||||
|
||||
const options = component
|
||||
.find('[data-test-subj="lnsXY_seriesType"]')
|
||||
.first()
|
||||
.prop('options') as EuiButtonGroupProps['options'];
|
||||
|
||||
expect(options!.map(({ id }) => id)).toEqual(['bar_horizontal', 'bar_horizontal_stacked']);
|
||||
expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
|
||||
expect(options!.map(({ id }) => id)).toEqual(['bar_horizontal', 'bar_horizontal_stacked']);
|
||||
expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test('the x dimension panel accepts only bucketed operations', () => {
|
||||
|
@ -123,6 +114,7 @@ describe('XYConfigPanel', () => {
|
|||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
layerId={state.layers[0].layerId}
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
|
@ -159,6 +151,7 @@ describe('XYConfigPanel', () => {
|
|||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
layerId={state.layers[0].layerId}
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
|
@ -190,6 +183,7 @@ describe('XYConfigPanel', () => {
|
|||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
layerId={state.layers[0].layerId}
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={setState}
|
||||
|
@ -197,8 +191,6 @@ describe('XYConfigPanel', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
openComponentPopover(component, 'first');
|
||||
|
||||
const onRemove = component
|
||||
.find('[data-test-subj="lensXY_yDimensionPanel"]')
|
||||
.first()
|
||||
|
@ -223,6 +215,7 @@ describe('XYConfigPanel', () => {
|
|||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
layerId={state.layers[0].layerId}
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={setState}
|
||||
|
@ -247,165 +240,4 @@ describe('XYConfigPanel', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('layers', () => {
|
||||
it('adds layers', () => {
|
||||
frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
|
||||
(generateId as jest.Mock).mockReturnValue('accessor');
|
||||
const setState = jest.fn();
|
||||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={setState}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsXY_layer_add"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(frame.addNewLayer).toHaveBeenCalled();
|
||||
expect(setState).toHaveBeenCalledTimes(1);
|
||||
expect(generateId).toHaveBeenCalledTimes(4);
|
||||
expect(setState.mock.calls[0][0]).toMatchObject({
|
||||
layers: [
|
||||
...state.layers,
|
||||
expect.objectContaining({
|
||||
layerId: 'newLayerId',
|
||||
xAccessor: 'accessor',
|
||||
accessors: ['accessor'],
|
||||
splitAccessor: 'accessor',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use series type of existing layers if they all have the same', () => {
|
||||
frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
|
||||
frame.datasourceLayers.second = createMockDatasource().publicAPIMock;
|
||||
(generateId as jest.Mock).mockReturnValue('accessor');
|
||||
const setState = jest.fn();
|
||||
const state: XYState = {
|
||||
...testState(),
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
seriesType: 'line',
|
||||
layerId: 'first',
|
||||
splitAccessor: 'baz',
|
||||
xAccessor: 'foo',
|
||||
accessors: ['bar'],
|
||||
},
|
||||
{
|
||||
seriesType: 'line',
|
||||
layerId: 'second',
|
||||
splitAccessor: 'baz',
|
||||
xAccessor: 'foo',
|
||||
accessors: ['bar'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={setState}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsXY_layer_add"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(setState.mock.calls[0][0]).toMatchObject({
|
||||
layers: [
|
||||
...state.layers,
|
||||
expect.objectContaining({
|
||||
seriesType: 'line',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use preffered series type if there are already various different layers', () => {
|
||||
frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
|
||||
frame.datasourceLayers.second = createMockDatasource().publicAPIMock;
|
||||
(generateId as jest.Mock).mockReturnValue('accessor');
|
||||
const setState = jest.fn();
|
||||
const state: XYState = {
|
||||
...testState(),
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
seriesType: 'area',
|
||||
layerId: 'first',
|
||||
splitAccessor: 'baz',
|
||||
xAccessor: 'foo',
|
||||
accessors: ['bar'],
|
||||
},
|
||||
{
|
||||
seriesType: 'line',
|
||||
layerId: 'second',
|
||||
splitAccessor: 'baz',
|
||||
xAccessor: 'foo',
|
||||
accessors: ['bar'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={setState}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsXY_layer_add"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(setState.mock.calls[0][0]).toMatchObject({
|
||||
layers: [
|
||||
...state.layers,
|
||||
expect.objectContaining({
|
||||
seriesType: 'bar',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes layers', () => {
|
||||
const setState = jest.fn();
|
||||
const state = testState();
|
||||
const component = mount(
|
||||
<XYConfigPanel
|
||||
dragDropContext={dragDropContext}
|
||||
frame={frame}
|
||||
setState={setState}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
|
||||
openComponentPopover(component, 'first');
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsXY_layer_remove"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(frame.removeLayers).toHaveBeenCalled();
|
||||
expect(setState).toHaveBeenCalledTimes(1);
|
||||
expect(setState.mock.calls[0][0]).toMatchObject({
|
||||
layers: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,25 +5,11 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiPopoverFooter,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { State, SeriesType, LayerConfig, visualizationTypes } from './types';
|
||||
import { VisualizationProps, OperationMetadata } from '../types';
|
||||
import { EuiButtonGroup, EuiFormRow } from '@elastic/eui';
|
||||
import { State, SeriesType, visualizationTypes } from './types';
|
||||
import { VisualizationLayerConfigProps, OperationMetadata } from '../types';
|
||||
import { NativeRenderer } from '../native_renderer';
|
||||
import { MultiColumnEditor } from '../multi_column_editor';
|
||||
import { generateId } from '../id_generator';
|
||||
|
@ -45,253 +31,140 @@ function updateLayer(state: State, layer: UnwrapArray<State['layers']>, index: n
|
|||
};
|
||||
}
|
||||
|
||||
function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig {
|
||||
return {
|
||||
layerId,
|
||||
seriesType,
|
||||
xAccessor: generateId(),
|
||||
accessors: [generateId()],
|
||||
splitAccessor: generateId(),
|
||||
};
|
||||
}
|
||||
export function LayerContextMenu(props: VisualizationLayerConfigProps<State>) {
|
||||
const { state, layerId } = props;
|
||||
const horizontalOnly = isHorizontalChart(state.layers);
|
||||
const index = state.layers.findIndex(l => l.layerId === layerId);
|
||||
const layer = state.layers[index];
|
||||
|
||||
function LayerSettings({
|
||||
layer,
|
||||
horizontalOnly,
|
||||
setSeriesType,
|
||||
removeLayer,
|
||||
}: {
|
||||
layer: LayerConfig;
|
||||
horizontalOnly: boolean;
|
||||
setSeriesType: (seriesType: SeriesType) => void;
|
||||
removeLayer: () => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { icon } = visualizationTypes.find(c => c.id === layer.seriesType)!;
|
||||
if (!layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={`lnsXYSeriesTypePopover_${layer.layerId}`}
|
||||
panelPaddingSize="s"
|
||||
ownFocus
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType={icon || 'lnsBarVertical'}
|
||||
aria-label={i18n.translate('xpack.lens.xyChart.layerSettings', {
|
||||
defaultMessage: 'Edit layer settings',
|
||||
})}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
data-test-subj="lnsXY_layer_advanced"
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
anchorPosition="leftUp"
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.chartTypeLabel', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.chartTypeLabel', {
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.xyChart.chartTypeLegend', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.xyChart.chartTypeLegend', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
name="chartType"
|
||||
className="eui-displayInlineBlock"
|
||||
data-test-subj="lnsXY_seriesType"
|
||||
options={visualizationTypes
|
||||
.filter(t => isHorizontalSeries(t.id as SeriesType) === horizontalOnly)
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
label: t.label,
|
||||
iconType: t.icon || 'empty',
|
||||
}))}
|
||||
idSelected={layer.seriesType}
|
||||
onChange={seriesType => {
|
||||
trackUiEvent('xy_change_layer_display');
|
||||
setSeriesType(seriesType as SeriesType);
|
||||
}}
|
||||
isIconOnly
|
||||
buttonSize="compressed"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiPopoverFooter className="eui-textCenter">
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="lnsXY_layer_remove"
|
||||
onClick={removeLayer}
|
||||
>
|
||||
{i18n.translate('xpack.lens.xyChart.deleteLayer', {
|
||||
defaultMessage: 'Delete layer',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiPopoverFooter>
|
||||
</EuiPopover>
|
||||
name="chartType"
|
||||
className="eui-displayInlineBlock"
|
||||
data-test-subj="lnsXY_seriesType"
|
||||
options={visualizationTypes
|
||||
.filter(t => isHorizontalSeries(t.id as SeriesType) === horizontalOnly)
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
label: t.label,
|
||||
iconType: t.icon || 'empty',
|
||||
}))}
|
||||
idSelected={layer.seriesType}
|
||||
onChange={seriesType => {
|
||||
trackUiEvent('xy_change_layer_display');
|
||||
props.setState(
|
||||
updateLayer(state, { ...layer, seriesType: seriesType as SeriesType }, index)
|
||||
);
|
||||
}}
|
||||
isIconOnly
|
||||
buttonSize="compressed"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function XYConfigPanel(props: VisualizationProps<State>) {
|
||||
const { state, setState, frame } = props;
|
||||
const horizontalOnly = isHorizontalChart(state.layers);
|
||||
export function XYConfigPanel(props: VisualizationLayerConfigProps<State>) {
|
||||
const { state, setState, frame, layerId } = props;
|
||||
const index = props.state.layers.findIndex(l => l.layerId === layerId);
|
||||
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layer = props.state.layers[index];
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{state.layers.map((layer, index) => (
|
||||
<EuiPanel
|
||||
className="lnsConfigPanel__panel"
|
||||
key={layer.layerId}
|
||||
data-test-subj={`lnsXY_layer_${layer.layerId}`}
|
||||
paddingSize="s"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LayerSettings
|
||||
layer={layer}
|
||||
horizontalOnly={horizontalOnly}
|
||||
setSeriesType={seriesType =>
|
||||
setState(updateLayer(state, { ...layer, seriesType }, index))
|
||||
}
|
||||
removeLayer={() => {
|
||||
trackUiEvent('xy_layer_removed');
|
||||
frame.removeLayers([layer.layerId]);
|
||||
setState({ ...state, layers: state.layers.filter(l => l !== layer) });
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<NativeRenderer
|
||||
data-test-subj="lnsXY_layerHeader"
|
||||
render={props.frame.datasourceLayers[layer.layerId].renderLayerPanel}
|
||||
nativeProps={{ layerId: layer.layerId }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.xyChart.xAxisLabel', {
|
||||
defaultMessage: 'X-axis',
|
||||
})}
|
||||
>
|
||||
<NativeRenderer
|
||||
data-test-subj="lnsXY_xDimensionPanel"
|
||||
render={props.frame.datasourceLayers[layer.layerId].renderDimensionPanel}
|
||||
nativeProps={{
|
||||
columnId: layer.xAccessor,
|
||||
dragDropContext: props.dragDropContext,
|
||||
filterOperations: isBucketed,
|
||||
suggestedPriority: 1,
|
||||
layerId: layer.layerId,
|
||||
hideGrouping: true,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
data-test-subj="lnsXY_yDimensionPanel"
|
||||
label={i18n.translate('xpack.lens.xyChart.yAxisLabel', {
|
||||
defaultMessage: 'Y-axis',
|
||||
})}
|
||||
>
|
||||
<MultiColumnEditor
|
||||
accessors={layer.accessors}
|
||||
datasource={frame.datasourceLayers[layer.layerId]}
|
||||
dragDropContext={props.dragDropContext}
|
||||
onAdd={() =>
|
||||
setState(
|
||||
updateLayer(
|
||||
state,
|
||||
{
|
||||
...layer,
|
||||
accessors: [...layer.accessors, generateId()],
|
||||
},
|
||||
index
|
||||
)
|
||||
)
|
||||
}
|
||||
onRemove={accessor =>
|
||||
setState(
|
||||
updateLayer(
|
||||
state,
|
||||
{
|
||||
...layer,
|
||||
accessors: layer.accessors.filter(col => col !== accessor),
|
||||
},
|
||||
index
|
||||
)
|
||||
)
|
||||
}
|
||||
filterOperations={isNumericMetric}
|
||||
data-test-subj="lensXY_yDimensionPanel"
|
||||
testSubj="lensXY_yDimensionPanel"
|
||||
layerId={layer.layerId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.xyChart.splitSeries', {
|
||||
defaultMessage: 'Break down by',
|
||||
})}
|
||||
>
|
||||
<NativeRenderer
|
||||
data-test-subj="lnsXY_splitDimensionPanel"
|
||||
render={props.frame.datasourceLayers[layer.layerId].renderDimensionPanel}
|
||||
nativeProps={{
|
||||
columnId: layer.splitAccessor,
|
||||
dragDropContext: props.dragDropContext,
|
||||
filterOperations: isBucketed,
|
||||
suggestedPriority: 0,
|
||||
layerId: layer.layerId,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
))}
|
||||
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiToolTip
|
||||
className="eui-fullWidth"
|
||||
content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', {
|
||||
defaultMessage:
|
||||
'Use multiple layers to combine chart types or visualize different index patterns.',
|
||||
})}
|
||||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
className="lnsConfigPanel__addLayerBtn"
|
||||
fullWidth
|
||||
size="s"
|
||||
data-test-subj={`lnsXY_layer_add`}
|
||||
aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.xyChart.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
onClick={() => {
|
||||
trackUiEvent('xy_layer_added');
|
||||
const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType));
|
||||
setState({
|
||||
...state,
|
||||
layers: [
|
||||
...state.layers,
|
||||
newLayerState(
|
||||
usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType,
|
||||
frame.addNewLayer()
|
||||
),
|
||||
],
|
||||
});
|
||||
}}
|
||||
iconType="plusInCircleFilled"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiForm>
|
||||
<>
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.xyChart.xAxisLabel', {
|
||||
defaultMessage: 'X-axis',
|
||||
})}
|
||||
>
|
||||
<NativeRenderer
|
||||
data-test-subj="lnsXY_xDimensionPanel"
|
||||
render={props.frame.datasourceLayers[layer.layerId].renderDimensionPanel}
|
||||
nativeProps={{
|
||||
columnId: layer.xAccessor,
|
||||
dragDropContext: props.dragDropContext,
|
||||
filterOperations: isBucketed,
|
||||
suggestedPriority: 1,
|
||||
layerId: layer.layerId,
|
||||
hideGrouping: true,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
data-test-subj="lnsXY_yDimensionPanel"
|
||||
label={i18n.translate('xpack.lens.xyChart.yAxisLabel', {
|
||||
defaultMessage: 'Y-axis',
|
||||
})}
|
||||
>
|
||||
<MultiColumnEditor
|
||||
accessors={layer.accessors}
|
||||
datasource={frame.datasourceLayers[layer.layerId]}
|
||||
dragDropContext={props.dragDropContext}
|
||||
onAdd={() =>
|
||||
setState(
|
||||
updateLayer(
|
||||
state,
|
||||
{
|
||||
...layer,
|
||||
accessors: [...layer.accessors, generateId()],
|
||||
},
|
||||
index
|
||||
)
|
||||
)
|
||||
}
|
||||
onRemove={accessor =>
|
||||
setState(
|
||||
updateLayer(
|
||||
state,
|
||||
{
|
||||
...layer,
|
||||
accessors: layer.accessors.filter(col => col !== accessor),
|
||||
},
|
||||
index
|
||||
)
|
||||
)
|
||||
}
|
||||
filterOperations={isNumericMetric}
|
||||
data-test-subj="lensXY_yDimensionPanel"
|
||||
testSubj="lensXY_yDimensionPanel"
|
||||
layerId={layer.layerId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__axis"
|
||||
label={i18n.translate('xpack.lens.xyChart.splitSeries', {
|
||||
defaultMessage: 'Break down by',
|
||||
})}
|
||||
>
|
||||
<NativeRenderer
|
||||
data-test-subj="lnsXY_splitDimensionPanel"
|
||||
render={props.frame.datasourceLayers[layer.layerId].renderDimensionPanel}
|
||||
nativeProps={{
|
||||
columnId: layer.splitAccessor,
|
||||
dragDropContext: props.dragDropContext,
|
||||
filterOperations: isBucketed,
|
||||
suggestedPriority: 0,
|
||||
layerId: layer.layerId,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -137,6 +137,54 @@ describe('xy_visualization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#removeLayer', () => {
|
||||
it('removes the specified layer', () => {
|
||||
const prevState: State = {
|
||||
...exampleState(),
|
||||
layers: [
|
||||
...exampleState().layers,
|
||||
{
|
||||
layerId: 'second',
|
||||
seriesType: 'area',
|
||||
splitAccessor: 'e',
|
||||
xAccessor: 'f',
|
||||
accessors: ['g', 'h'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(xyVisualization.removeLayer!(prevState, 'second')).toEqual(exampleState());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#appendLayer', () => {
|
||||
it('adds a layer', () => {
|
||||
const layers = xyVisualization.appendLayer!(exampleState(), 'foo').layers;
|
||||
expect(layers.length).toEqual(exampleState().layers.length + 1);
|
||||
expect(layers[layers.length - 1]).toMatchObject({ layerId: 'foo' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearLayer', () => {
|
||||
it('clears the specified layer', () => {
|
||||
(generateId as jest.Mock).mockReturnValue('test_empty_id');
|
||||
const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0];
|
||||
expect(layer).toMatchObject({
|
||||
accessors: ['test_empty_id'],
|
||||
layerId: 'first',
|
||||
seriesType: 'bar',
|
||||
splitAccessor: 'test_empty_id',
|
||||
xAccessor: 'test_empty_id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLayerIds', () => {
|
||||
it('returns layerids', () => {
|
||||
expect(xyVisualization.getLayerIds(exampleState())).toEqual(['first']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toExpression', () => {
|
||||
let mockDatasource: ReturnType<typeof createMockDatasource>;
|
||||
let frame: ReturnType<typeof createMockFramePublicAPI>;
|
||||
|
|
|
@ -11,9 +11,9 @@ import { Position } from '@elastic/charts';
|
|||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getSuggestions } from './xy_suggestions';
|
||||
import { XYConfigPanel } from './xy_config_panel';
|
||||
import { XYConfigPanel, LayerContextMenu } from './xy_config_panel';
|
||||
import { Visualization } from '../types';
|
||||
import { State, PersistableState, SeriesType, visualizationTypes } from './types';
|
||||
import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
|
||||
import { toExpression, toPreviewExpression } from './to_expression';
|
||||
import { generateId } from '../id_generator';
|
||||
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
|
||||
|
@ -67,6 +67,40 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
|||
|
||||
visualizationTypes,
|
||||
|
||||
getLayerIds(state) {
|
||||
return state.layers.map(l => l.layerId);
|
||||
},
|
||||
|
||||
removeLayer(state, layerId) {
|
||||
return {
|
||||
...state,
|
||||
layers: state.layers.filter(l => l.layerId !== layerId),
|
||||
};
|
||||
},
|
||||
|
||||
appendLayer(state, layerId) {
|
||||
const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType));
|
||||
return {
|
||||
...state,
|
||||
layers: [
|
||||
...state.layers,
|
||||
newLayerState(
|
||||
usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType,
|
||||
layerId
|
||||
),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
clearLayer(state, layerId) {
|
||||
return {
|
||||
...state,
|
||||
layers: state.layers.map(l =>
|
||||
l.layerId !== layerId ? l : newLayerState(state.preferredSeriesType, layerId)
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
getDescription(state) {
|
||||
const { icon, label } = getDescription(state);
|
||||
const chartLabel = i18n.translate('xpack.lens.xyVisualization.chartLabel', {
|
||||
|
@ -113,7 +147,7 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
|||
|
||||
getPersistableState: state => state,
|
||||
|
||||
renderConfigPanel: (domElement, props) =>
|
||||
renderLayerConfigPanel: (domElement, props) =>
|
||||
render(
|
||||
<I18nProvider>
|
||||
<XYConfigPanel {...props} />
|
||||
|
@ -121,6 +155,30 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
|||
domElement
|
||||
),
|
||||
|
||||
getLayerContextMenuIcon({ state, layerId }) {
|
||||
const layer = state.layers.find(l => l.layerId === layerId);
|
||||
return visualizationTypes.find(t => t.id === layer?.seriesType)?.icon;
|
||||
},
|
||||
|
||||
renderLayerContextMenu(domElement, props) {
|
||||
render(
|
||||
<I18nProvider>
|
||||
<LayerContextMenu {...props} />
|
||||
</I18nProvider>,
|
||||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
toExpression,
|
||||
toPreviewExpression,
|
||||
};
|
||||
|
||||
function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig {
|
||||
return {
|
||||
layerId,
|
||||
seriesType,
|
||||
xAccessor: generateId(),
|
||||
accessors: [generateId()],
|
||||
splitAccessor: generateId(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6963,10 +6963,8 @@
|
|||
"xpack.lens.xyChart.addLayerTooltip": "複数のレイヤーを使用すると、グラフタイプを組み合わせたり、別のインデックスパターンを可視化したりすることができます。",
|
||||
"xpack.lens.xyChart.chartTypeLabel": "チャートタイプ",
|
||||
"xpack.lens.xyChart.chartTypeLegend": "チャートタイプ",
|
||||
"xpack.lens.xyChart.deleteLayer": "レイヤーを削除",
|
||||
"xpack.lens.xyChart.help": "X/Y チャート",
|
||||
"xpack.lens.xyChart.isVisible.help": "判例の表示・非表示を指定します。",
|
||||
"xpack.lens.xyChart.layerSettings": "レイヤー設定を編集",
|
||||
"xpack.lens.xyChart.legend.help": "チャートの凡例を構成します。",
|
||||
"xpack.lens.xyChart.nestUnderRoot": "データセット全体",
|
||||
"xpack.lens.xyChart.position.help": "凡例の配置を指定します。",
|
||||
|
|
|
@ -6962,10 +6962,8 @@
|
|||
"xpack.lens.xyChart.addLayerTooltip": "使用多个图层以组合图表类型或可视化不同的索引模式。",
|
||||
"xpack.lens.xyChart.chartTypeLabel": "图表类型",
|
||||
"xpack.lens.xyChart.chartTypeLegend": "图表类型",
|
||||
"xpack.lens.xyChart.deleteLayer": "删除图层",
|
||||
"xpack.lens.xyChart.help": "X/Y 图表",
|
||||
"xpack.lens.xyChart.isVisible.help": "指定图例是否可见。",
|
||||
"xpack.lens.xyChart.layerSettings": "编辑图层设置",
|
||||
"xpack.lens.xyChart.legend.help": "配置图表图例。",
|
||||
"xpack.lens.xyChart.nestUnderRoot": "整个数据集",
|
||||
"xpack.lens.xyChart.position.help": "指定图例位置。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue