[Lens] Split apart config panel component to more manageable chunks (#63910)
* [Lens] Split apart config panel component to more manageable chunks * Moving around and renaming SASS appropriately * Remove layer limit Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: cchaos <caroline.horn@elastic.co>
This commit is contained in:
parent
f7ea9b99ba
commit
dd094f2333
|
@ -0,0 +1,7 @@
|
|||
.lnsConfigPanel__addLayerBtn {
|
||||
color: transparentize($euiColorMediumShade, .3);
|
||||
// Remove EuiButton's default shadow to make button more subtle
|
||||
// sass-lint:disable-block no-important
|
||||
box-shadow: none !important;
|
||||
border: 1px dashed currentColor;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.lnsDimensionPopover {
|
||||
line-height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.lnsDimensionPopover__trigger {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
@import 'chart_switch';
|
||||
@import 'config_panel';
|
||||
@import 'dimension_popover';
|
||||
@import 'layer_panel';
|
|
@ -1,8 +1,8 @@
|
|||
.lnsConfigPanel__panel {
|
||||
.lnsLayerPanel {
|
||||
margin-bottom: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsConfigPanel__row {
|
||||
.lnsLayerPanel__row {
|
||||
background: $euiColorLightestShade;
|
||||
padding: $euiSizeS;
|
||||
border-radius: $euiBorderRadius;
|
||||
|
@ -13,15 +13,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lnsConfigPanel__addLayerBtn {
|
||||
color: transparentize($euiColorMediumShade, .3);
|
||||
// Remove EuiButton's default shadow to make button more subtle
|
||||
// sass-lint:disable-block no-important
|
||||
box-shadow: none !important;
|
||||
border: 1px dashed currentColor;
|
||||
}
|
||||
|
||||
.lnsConfigPanel__dimension {
|
||||
.lnsLayerPanel__dimension {
|
||||
@include euiFontSizeS;
|
||||
background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade);
|
||||
border-radius: $euiBorderRadius;
|
||||
|
@ -31,12 +23,7 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lnsConfigPanel__trigger {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lnsConfigPanel__triggerLink {
|
||||
.lnsLayerPanel__triggerLink {
|
||||
padding: $euiSizeS;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -44,7 +31,3 @@
|
|||
min-height: $euiSizeXXL;
|
||||
}
|
||||
|
||||
.lnsConfigPanel__popover {
|
||||
line-height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
|
@ -5,13 +5,17 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createMockVisualization, createMockFramePublicAPI, createMockDatasource } from '../mocks';
|
||||
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
|
||||
import {
|
||||
createMockVisualization,
|
||||
createMockFramePublicAPI,
|
||||
createMockDatasource,
|
||||
} from '../../mocks';
|
||||
import { EuiKeyPadMenuItem } from '@elastic/eui';
|
||||
import { Action } from './state_management';
|
||||
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
|
||||
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types';
|
||||
import { Action } from '../state_management';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
|
||||
describe('chart_switch', () => {
|
||||
function generateVisualization(id: string): jest.Mocked<Visualization> {
|
|
@ -15,10 +15,10 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { flatten } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Visualization, FramePublicAPI, Datasource } from '../../types';
|
||||
import { Action } from './state_management';
|
||||
import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { Visualization, FramePublicAPI, Datasource } from '../../../types';
|
||||
import { Action } from '../state_management';
|
||||
import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
|
||||
interface VisualizationSelection {
|
||||
visualizationId: string;
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 React, { useMemo, memo } from 'react';
|
||||
import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Visualization } from '../../../types';
|
||||
import { ChartSwitch } from './chart_switch';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { removeLayer, appendLayer } from './layer_actions';
|
||||
import { ConfigPanelWrapperProps } from './types';
|
||||
|
||||
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
|
||||
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
|
||||
const { visualizationState } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartSwitch
|
||||
data-test-subj="lnsChartSwitcher"
|
||||
visualizationMap={props.visualizationMap}
|
||||
visualizationId={props.activeVisualizationId}
|
||||
visualizationState={props.visualizationState}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={props.datasourceStates}
|
||||
dispatch={props.dispatch}
|
||||
framePublicAPI={props.framePublicAPI}
|
||||
/>
|
||||
{activeVisualization && visualizationState && (
|
||||
<LayerPanels {...props} activeVisualization={activeVisualization} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function LayerPanels(
|
||||
props: ConfigPanelWrapperProps & {
|
||||
activeDatasourceId: string;
|
||||
activeVisualization: Visualization;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
framePublicAPI,
|
||||
activeVisualization,
|
||||
visualizationState,
|
||||
dispatch,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
} = props;
|
||||
const setVisualizationState = useMemo(
|
||||
() => (newState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch, activeVisualization]
|
||||
);
|
||||
const updateDatasource = useMemo(
|
||||
() => (datasourceId: string, newState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: () => newState,
|
||||
datasourceId,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch]
|
||||
);
|
||||
const updateAll = useMemo(
|
||||
() => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'UPDATE_ALL_STATES',
|
||||
updater: prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
datasourceStates: {
|
||||
...prevState.datasourceStates,
|
||||
[datasourceId]: {
|
||||
state: newDatasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
...prevState.visualization,
|
||||
state: newVisualizationState,
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
[props.dispatch]
|
||||
);
|
||||
const layerIds = activeVisualization.getLayerIds(visualizationState);
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{layerIds.map(layerId => (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
activeVisualization={activeVisualization}
|
||||
visualizationState={visualizationState}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateAll={updateAll}
|
||||
frame={framePublicAPI}
|
||||
isOnlyLayer={layerIds.length === 1}
|
||||
onRemoveLayer={() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'REMOVE_OR_CLEAR_LAYER',
|
||||
updater: state =>
|
||||
removeLayer({
|
||||
activeVisualization,
|
||||
layerId,
|
||||
trackUiEvent,
|
||||
datasourceMap,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{activeVisualization.appendLayer && visualizationState && (
|
||||
<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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiPopover } from '@elastic/eui';
|
||||
import { VisualizationDimensionGroupConfig } from '../../../types';
|
||||
import { DimensionPopoverState } from './types';
|
||||
|
||||
export function DimensionPopover({
|
||||
popoverState,
|
||||
setPopoverState,
|
||||
groups,
|
||||
accessor,
|
||||
groupId,
|
||||
trigger,
|
||||
panel,
|
||||
}: {
|
||||
popoverState: DimensionPopoverState;
|
||||
setPopoverState: (newState: DimensionPopoverState) => void;
|
||||
groups: VisualizationDimensionGroupConfig[];
|
||||
accessor: string;
|
||||
groupId: string;
|
||||
trigger: React.ReactElement;
|
||||
panel: React.ReactElement;
|
||||
}) {
|
||||
const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(accessor)) : false;
|
||||
return (
|
||||
<EuiPopover
|
||||
className="lnsDimensionPopover"
|
||||
anchorClassName="lnsDimensionPopover__trigger"
|
||||
isOpen={
|
||||
popoverState.isOpen &&
|
||||
(popoverState.openId === accessor || (noMatch && popoverState.addingToGroupId === groupId))
|
||||
}
|
||||
closePopover={() => {
|
||||
setPopoverState({ isOpen: false, openId: null, addingToGroupId: null });
|
||||
}}
|
||||
button={trigger}
|
||||
anchorPosition="leftUp"
|
||||
withTitle
|
||||
panelPaddingSize="s"
|
||||
>
|
||||
{panel}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { ConfigPanelWrapper } from './config_panel';
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { EditorFrameState } from './state_management';
|
||||
import { Datasource, Visualization } from '../../types';
|
||||
import { EditorFrameState } from '../state_management';
|
||||
import { Datasource, Visualization } from '../../../types';
|
||||
|
||||
interface RemoveLayerOptions {
|
||||
trackUiEvent: (name: string) => void;
|
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* 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 React, { useContext, useState } from 'react';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { Visualization, FramePublicAPI, StateSetter } from '../../../types';
|
||||
import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { ConfigPanelWrapperProps, DimensionPopoverState } from './types';
|
||||
import { DimensionPopover } from './dimension_popover';
|
||||
|
||||
export function LayerPanel(
|
||||
props: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & {
|
||||
frame: FramePublicAPI;
|
||||
layerId: string;
|
||||
isOnlyLayer: boolean;
|
||||
activeVisualization: Visualization;
|
||||
visualizationState: unknown;
|
||||
updateVisualization: StateSetter<unknown>;
|
||||
updateDatasource: (datasourceId: string, newState: unknown) => void;
|
||||
updateAll: (
|
||||
datasourceId: string,
|
||||
newDatasourcestate: unknown,
|
||||
newVisualizationState: unknown
|
||||
) => void;
|
||||
onRemoveLayer: () => void;
|
||||
}
|
||||
) {
|
||||
const dragDropContext = useContext(DragContext);
|
||||
const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props;
|
||||
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
|
||||
if (!datasourcePublicAPI) {
|
||||
return null;
|
||||
}
|
||||
const layerVisualizationConfigProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: props.visualizationState,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
};
|
||||
const datasourceId = datasourcePublicAPI.datasourceId;
|
||||
const layerDatasourceState = props.datasourceStates[datasourceId].state;
|
||||
const layerDatasource = props.datasourceMap[datasourceId];
|
||||
|
||||
const layerDatasourceDropProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: layerDatasourceState,
|
||||
setState: (newState: unknown) => {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
},
|
||||
};
|
||||
|
||||
const layerDatasourceConfigProps = {
|
||||
...layerDatasourceDropProps,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
};
|
||||
|
||||
const [popoverState, setPopoverState] = useState<DimensionPopoverState>({
|
||||
isOpen: false,
|
||||
openId: null,
|
||||
addingToGroupId: null,
|
||||
});
|
||||
|
||||
const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps);
|
||||
const isEmptyLayer = !groups.some(d => d.accessors.length > 0);
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
<EuiPanel className="lnsLayerPanel" paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LayerSettings
|
||||
layerId={layerId}
|
||||
layerConfigProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
setState: props.updateVisualization,
|
||||
}}
|
||||
activeVisualization={activeVisualization}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{layerDatasource && (
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderLayerPanel}
|
||||
nativeProps={{
|
||||
layerId,
|
||||
state: layerDatasourceState,
|
||||
setState: (updater: unknown) => {
|
||||
const newState =
|
||||
typeof updater === 'function' ? updater(layerDatasourceState) : updater;
|
||||
// Look for removed columns
|
||||
const nextPublicAPI = layerDatasource.getPublicAPI({
|
||||
state: newState,
|
||||
layerId,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
});
|
||||
const nextTable = new Set(
|
||||
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
|
||||
);
|
||||
const removed = datasourcePublicAPI
|
||||
.getTableSpec()
|
||||
.map(({ columnId }) => columnId)
|
||||
.filter(columnId => !nextTable.has(columnId));
|
||||
let nextVisState = props.visualizationState;
|
||||
removed.forEach(columnId => {
|
||||
nextVisState = activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId,
|
||||
prevState: nextVisState,
|
||||
});
|
||||
});
|
||||
|
||||
props.updateAll(datasourceId, newState, nextVisState);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{groups.map((group, index) => {
|
||||
const newId = generateId();
|
||||
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="lnsLayerPanel__row"
|
||||
label={group.groupLabel}
|
||||
key={index}
|
||||
isInvalid={isMissing}
|
||||
error={
|
||||
isMissing
|
||||
? i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
|
||||
defaultMessage: 'Required dimension',
|
||||
})
|
||||
: []
|
||||
}
|
||||
>
|
||||
<>
|
||||
{group.accessors.map(accessor => (
|
||||
<DragDrop
|
||||
key={accessor}
|
||||
className="lnsLayerPanel__dimension"
|
||||
data-test-subj={group.dataTestSubj}
|
||||
droppable={
|
||||
dragDropContext.dragging &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
onDrop={droppedItem => {
|
||||
layerDatasource.onDrop({
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DimensionPopover
|
||||
popoverState={popoverState}
|
||||
setPopoverState={setPopoverState}
|
||||
groups={groups}
|
||||
accessor={accessor}
|
||||
groupId={group.groupId}
|
||||
trigger={
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
suggestedPriority: group.suggestedPriority,
|
||||
togglePopover: () => {
|
||||
if (popoverState.isOpen) {
|
||||
setPopoverState({
|
||||
isOpen: false,
|
||||
openId: null,
|
||||
addingToGroupId: null,
|
||||
});
|
||||
} else {
|
||||
setPopoverState({
|
||||
isOpen: true,
|
||||
openId: accessor,
|
||||
addingToGroupId: null, // not set for existing dimension
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
panel={
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
core: props.core,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<EuiButtonIcon
|
||||
data-test-subj="indexPattern-dimensionPopover-remove"
|
||||
iconType="cross"
|
||||
iconSize="s"
|
||||
size="s"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
})}
|
||||
onClick={() => {
|
||||
trackUiEvent('indexpattern_dimension_removed');
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
layerDatasource.removeColumn({
|
||||
layerId,
|
||||
columnId: accessor,
|
||||
prevState: layerDatasourceState,
|
||||
}),
|
||||
props.activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: accessor,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DragDrop>
|
||||
))}
|
||||
{group.supportsMoreColumns ? (
|
||||
<DragDrop
|
||||
className="lnsLayerPanel__dimension"
|
||||
data-test-subj={group.dataTestSubj}
|
||||
droppable={
|
||||
dragDropContext.dragging &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
onDrop={droppedItem => {
|
||||
const dropSuccess = layerDatasource.onDrop({
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
if (dropSuccess) {
|
||||
props.updateVisualization(
|
||||
activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
columnId: newId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DimensionPopover
|
||||
popoverState={popoverState}
|
||||
setPopoverState={setPopoverState}
|
||||
groups={groups}
|
||||
accessor={newId}
|
||||
groupId={group.groupId}
|
||||
trigger={
|
||||
<div className="lnsLayerPanel__triggerLink">
|
||||
<EuiButtonEmpty
|
||||
iconType="plusInCircleFilled"
|
||||
data-test-subj="lns-empty-dimension"
|
||||
aria-label={i18n.translate('xpack.lens.configure.addConfig', {
|
||||
defaultMessage: 'Add a configuration',
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.configure.addConfig', {
|
||||
defaultMessage: 'Add a configuration',
|
||||
})}
|
||||
onClick={() => {
|
||||
if (popoverState.isOpen) {
|
||||
setPopoverState({
|
||||
isOpen: false,
|
||||
openId: null,
|
||||
addingToGroupId: null,
|
||||
});
|
||||
} else {
|
||||
setPopoverState({
|
||||
isOpen: true,
|
||||
openId: newId,
|
||||
addingToGroupId: group.groupId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.configure.emptyConfig"
|
||||
defaultMessage="Drop a field here"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
}
|
||||
panel={
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
core: props.core,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
suggestedPriority: group.suggestedPriority,
|
||||
|
||||
setState: (newState: unknown) => {
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
newState,
|
||||
activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
columnId: newId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
setPopoverState({
|
||||
isOpen: true,
|
||||
openId: newId,
|
||||
addingToGroupId: null, // clear now that dimension exists
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DragDrop>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
})}
|
||||
|
||||
<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?.blur) {
|
||||
el.blur();
|
||||
}
|
||||
|
||||
onRemoveLayer();
|
||||
}}
|
||||
>
|
||||
{isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayer', {
|
||||
defaultMessage: 'Reset layer',
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayer', {
|
||||
defaultMessage: 'Delete layer',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import { EuiPopover, EuiButtonIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { Visualization, VisualizationLayerWidgetProps } from '../../../types';
|
||||
|
||||
export function LayerSettings({
|
||||
layerId,
|
||||
activeVisualization,
|
||||
layerConfigProps,
|
||||
}: {
|
||||
layerId: string;
|
||||
activeVisualization: Visualization;
|
||||
layerConfigProps: VisualizationLayerWidgetProps;
|
||||
}) {
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { Action } from '../state_management';
|
||||
import {
|
||||
Visualization,
|
||||
FramePublicAPI,
|
||||
Datasource,
|
||||
DatasourceDimensionEditorProps,
|
||||
} from '../../../types';
|
||||
|
||||
export interface ConfigPanelWrapperProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
visualizationMap: Record<string, Visualization>;
|
||||
activeVisualizationId: string | null;
|
||||
dispatch: (action: Action) => void;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
core: DatasourceDimensionEditorProps['core'];
|
||||
}
|
||||
|
||||
export interface DimensionPopoverState {
|
||||
isOpen: boolean;
|
||||
openId: string | null;
|
||||
addingToGroupId: string | null;
|
||||
}
|
|
@ -1,655 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useContext, memo, useState } from 'react';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiToolTip,
|
||||
EuiButton,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NativeRenderer } from '../../native_renderer';
|
||||
import { Action } from './state_management';
|
||||
import {
|
||||
Visualization,
|
||||
FramePublicAPI,
|
||||
Datasource,
|
||||
VisualizationLayerWidgetProps,
|
||||
DatasourceDimensionEditorProps,
|
||||
StateSetter,
|
||||
} from '../../types';
|
||||
import { DragContext, DragDrop, ChildDragDropProvider } 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;
|
||||
dispatch: (action: Action) => void;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
core: DatasourceDimensionEditorProps['core'];
|
||||
}
|
||||
|
||||
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
|
||||
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
|
||||
const { visualizationState } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartSwitch
|
||||
data-test-subj="lnsChartSwitcher"
|
||||
visualizationMap={props.visualizationMap}
|
||||
visualizationId={props.activeVisualizationId}
|
||||
visualizationState={props.visualizationState}
|
||||
datasourceMap={props.datasourceMap}
|
||||
datasourceStates={props.datasourceStates}
|
||||
dispatch={props.dispatch}
|
||||
framePublicAPI={props.framePublicAPI}
|
||||
/>
|
||||
{activeVisualization && visualizationState && (
|
||||
<LayerPanels {...props} activeVisualization={activeVisualization} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function LayerPanels(
|
||||
props: ConfigPanelWrapperProps & {
|
||||
activeDatasourceId: string;
|
||||
activeVisualization: Visualization;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
framePublicAPI,
|
||||
activeVisualization,
|
||||
visualizationState,
|
||||
dispatch,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
} = props;
|
||||
const setVisualizationState = useMemo(
|
||||
() => (newState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_VISUALIZATION_STATE',
|
||||
visualizationId: activeVisualization.id,
|
||||
newState,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch, activeVisualization]
|
||||
);
|
||||
const updateDatasource = useMemo(
|
||||
() => (datasourceId: string, newState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: () => newState,
|
||||
datasourceId,
|
||||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[props.dispatch]
|
||||
);
|
||||
const updateAll = useMemo(
|
||||
() => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => {
|
||||
props.dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'UPDATE_ALL_STATES',
|
||||
updater: prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
datasourceStates: {
|
||||
...prevState.datasourceStates,
|
||||
[datasourceId]: {
|
||||
state: newDatasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
...prevState.visualization,
|
||||
state: newVisualizationState,
|
||||
},
|
||||
stagedPreview: undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
[props.dispatch]
|
||||
);
|
||||
const layerIds = activeVisualization.getLayerIds(visualizationState);
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{layerIds.map(layerId => (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
activeVisualization={activeVisualization}
|
||||
visualizationState={visualizationState}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateAll={updateAll}
|
||||
frame={framePublicAPI}
|
||||
isOnlyLayer={layerIds.length === 1}
|
||||
onRemoveLayer={() => {
|
||||
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: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & {
|
||||
frame: FramePublicAPI;
|
||||
layerId: string;
|
||||
isOnlyLayer: boolean;
|
||||
activeVisualization: Visualization;
|
||||
visualizationState: unknown;
|
||||
updateVisualization: StateSetter<unknown>;
|
||||
updateDatasource: (datasourceId: string, newState: unknown) => void;
|
||||
updateAll: (
|
||||
datasourceId: string,
|
||||
newDatasourcestate: unknown,
|
||||
newVisualizationState: unknown
|
||||
) => void;
|
||||
onRemoveLayer: () => void;
|
||||
}
|
||||
) {
|
||||
const dragDropContext = useContext(DragContext);
|
||||
const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props;
|
||||
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
|
||||
if (!datasourcePublicAPI) {
|
||||
return null;
|
||||
}
|
||||
const layerVisualizationConfigProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: props.visualizationState,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
};
|
||||
const datasourceId = datasourcePublicAPI.datasourceId;
|
||||
const layerDatasourceState = props.datasourceStates[datasourceId].state;
|
||||
const layerDatasource = props.datasourceMap[datasourceId];
|
||||
|
||||
const layerDatasourceDropProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: layerDatasourceState,
|
||||
setState: (newState: unknown) => {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
},
|
||||
};
|
||||
|
||||
const layerDatasourceConfigProps = {
|
||||
...layerDatasourceDropProps,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
};
|
||||
|
||||
const [popoverState, setPopoverState] = useState<{
|
||||
isOpen: boolean;
|
||||
openId: string | null;
|
||||
addingToGroupId: string | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
openId: null,
|
||||
addingToGroupId: null,
|
||||
});
|
||||
|
||||
const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps);
|
||||
const isEmptyLayer = !groups.some(d => d.accessors.length > 0);
|
||||
|
||||
function wrapInPopover(
|
||||
id: string,
|
||||
groupId: string,
|
||||
trigger: React.ReactElement,
|
||||
panel: React.ReactElement
|
||||
) {
|
||||
const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false;
|
||||
return (
|
||||
<EuiPopover
|
||||
className="lnsConfigPanel__popover"
|
||||
anchorClassName="lnsConfigPanel__trigger"
|
||||
isOpen={
|
||||
popoverState.isOpen &&
|
||||
(popoverState.openId === id || (noMatch && popoverState.addingToGroupId === groupId))
|
||||
}
|
||||
closePopover={() => {
|
||||
setPopoverState({ isOpen: false, openId: null, addingToGroupId: null });
|
||||
}}
|
||||
button={trigger}
|
||||
anchorPosition="leftUp"
|
||||
withTitle
|
||||
panelPaddingSize="s"
|
||||
>
|
||||
{panel}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
<EuiPanel className="lnsConfigPanel__panel" paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LayerSettings
|
||||
layerId={layerId}
|
||||
layerConfigProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
setState: props.updateVisualization,
|
||||
}}
|
||||
activeVisualization={activeVisualization}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{layerDatasource && (
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderLayerPanel}
|
||||
nativeProps={{
|
||||
layerId,
|
||||
state: layerDatasourceState,
|
||||
setState: (updater: unknown) => {
|
||||
const newState =
|
||||
typeof updater === 'function' ? updater(layerDatasourceState) : updater;
|
||||
// Look for removed columns
|
||||
const nextPublicAPI = layerDatasource.getPublicAPI({
|
||||
state: newState,
|
||||
layerId,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
});
|
||||
const nextTable = new Set(
|
||||
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
|
||||
);
|
||||
const removed = datasourcePublicAPI
|
||||
.getTableSpec()
|
||||
.map(({ columnId }) => columnId)
|
||||
.filter(columnId => !nextTable.has(columnId));
|
||||
let nextVisState = props.visualizationState;
|
||||
removed.forEach(columnId => {
|
||||
nextVisState = activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId,
|
||||
prevState: nextVisState,
|
||||
});
|
||||
});
|
||||
|
||||
props.updateAll(datasourceId, newState, nextVisState);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{groups.map((group, index) => {
|
||||
const newId = generateId();
|
||||
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="lnsConfigPanel__row"
|
||||
label={group.groupLabel}
|
||||
key={index}
|
||||
isInvalid={isMissing}
|
||||
error={
|
||||
isMissing
|
||||
? i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
|
||||
defaultMessage: 'Required dimension',
|
||||
})
|
||||
: []
|
||||
}
|
||||
>
|
||||
<>
|
||||
{group.accessors.map(accessor => (
|
||||
<DragDrop
|
||||
key={accessor}
|
||||
className="lnsConfigPanel__dimension"
|
||||
data-test-subj={group.dataTestSubj}
|
||||
droppable={
|
||||
dragDropContext.dragging &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
onDrop={droppedItem => {
|
||||
layerDatasource.onDrop({
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{wrapInPopover(
|
||||
accessor,
|
||||
group.groupId,
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
suggestedPriority: group.suggestedPriority,
|
||||
togglePopover: () => {
|
||||
if (popoverState.isOpen) {
|
||||
setPopoverState({
|
||||
isOpen: false,
|
||||
openId: null,
|
||||
addingToGroupId: null,
|
||||
});
|
||||
} else {
|
||||
setPopoverState({
|
||||
isOpen: true,
|
||||
openId: accessor,
|
||||
addingToGroupId: null, // not set for existing dimension
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
core: props.core,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EuiButtonIcon
|
||||
data-test-subj="indexPattern-dimensionPopover-remove"
|
||||
iconType="cross"
|
||||
iconSize="s"
|
||||
size="s"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
})}
|
||||
onClick={() => {
|
||||
trackUiEvent('indexpattern_dimension_removed');
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
layerDatasource.removeColumn({
|
||||
layerId,
|
||||
columnId: accessor,
|
||||
prevState: layerDatasourceState,
|
||||
}),
|
||||
props.activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: accessor,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DragDrop>
|
||||
))}
|
||||
{group.supportsMoreColumns ? (
|
||||
<DragDrop
|
||||
className="lnsConfigPanel__dimension"
|
||||
data-test-subj={group.dataTestSubj}
|
||||
droppable={
|
||||
dragDropContext.dragging &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
onDrop={droppedItem => {
|
||||
const dropSuccess = layerDatasource.onDrop({
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
if (dropSuccess) {
|
||||
props.updateVisualization(
|
||||
activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
columnId: newId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{wrapInPopover(
|
||||
newId,
|
||||
group.groupId,
|
||||
<div className="lnsConfigPanel__triggerLink">
|
||||
<EuiButtonEmpty
|
||||
iconType="plusInCircleFilled"
|
||||
data-test-subj="lns-empty-dimension"
|
||||
aria-label={i18n.translate('xpack.lens.configure.addConfig', {
|
||||
defaultMessage: 'Add a configuration',
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.configure.addConfig', {
|
||||
defaultMessage: 'Add a configuration',
|
||||
})}
|
||||
onClick={() => {
|
||||
if (popoverState.isOpen) {
|
||||
setPopoverState({
|
||||
isOpen: false,
|
||||
openId: null,
|
||||
addingToGroupId: null,
|
||||
});
|
||||
} else {
|
||||
setPopoverState({
|
||||
isOpen: true,
|
||||
openId: newId,
|
||||
addingToGroupId: group.groupId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.configure.emptyConfig"
|
||||
defaultMessage="Drop a field here"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>,
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
core: props.core,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
suggestedPriority: group.suggestedPriority,
|
||||
|
||||
setState: (newState: unknown) => {
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
newState,
|
||||
activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
columnId: newId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
setPopoverState({
|
||||
isOpen: true,
|
||||
openId: newId,
|
||||
addingToGroupId: null, // clear now that dimension exists
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DragDrop>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
})}
|
||||
|
||||
<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?.blur) {
|
||||
el.blur();
|
||||
}
|
||||
|
||||
onRemoveLayer();
|
||||
}}
|
||||
>
|
||||
{isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayer', {
|
||||
defaultMessage: 'Reset layer',
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayer', {
|
||||
defaultMessage: 'Delete layer',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function LayerSettings({
|
||||
layerId,
|
||||
activeVisualization,
|
||||
layerConfigProps,
|
||||
}: {
|
||||
layerId: string;
|
||||
activeVisualization: Visualization;
|
||||
layerConfigProps: VisualizationLayerWidgetProps;
|
||||
}) {
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../../types';
|
||||
import { reducer, getInitialState } from './state_management';
|
||||
import { DataPanelWrapper } from './data_panel_wrapper';
|
||||
import { ConfigPanelWrapper } from './config_panel_wrapper';
|
||||
import { ConfigPanelWrapper } from './config_panel';
|
||||
import { FrameLayout } from './frame_layout';
|
||||
import { SuggestionPanel } from './suggestion_panel';
|
||||
import { WorkspacePanel } from './workspace_panel';
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
@import 'chart_switch';
|
||||
@import 'config_panel_wrapper';
|
||||
@import 'config_panel/index';
|
||||
@import 'data_panel_wrapper';
|
||||
@import 'expression_renderer';
|
||||
@import 'frame_layout';
|
||||
@import 'suggestion_panel';
|
||||
@import 'workspace_panel_wrapper';
|
||||
|
||||
|
|
|
@ -192,7 +192,7 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
|
|||
return (
|
||||
<EuiLink
|
||||
id={columnId}
|
||||
className="lnsConfigPanel__triggerLink"
|
||||
className="lnsLayerPanel__triggerLink"
|
||||
onClick={() => {
|
||||
props.togglePopover();
|
||||
}}
|
||||
|
|
|
@ -273,7 +273,7 @@ export type VisualizationLayerWidgetProps<T = unknown> = VisualizationConfigProp
|
|||
setState: (newState: T) => void;
|
||||
};
|
||||
|
||||
type VisualizationDimensionGroupConfig = SharedDimensionProps & {
|
||||
export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
|
||||
groupLabel: string;
|
||||
|
||||
/** ID is passed back to visualization. For example, `x` */
|
||||
|
@ -368,55 +368,86 @@ export interface VisualizationType {
|
|||
}
|
||||
|
||||
export interface Visualization<T = unknown, P = unknown> {
|
||||
/** Plugin ID, such as "lnsXY" */
|
||||
id: string;
|
||||
|
||||
visualizationTypes: VisualizationType[];
|
||||
/**
|
||||
* Initialize is allowed to modify the state stored in memory. The initialize function
|
||||
* is called with a previous state in two cases:
|
||||
* - Loadingn from a saved visualization
|
||||
* - When using suggestions, the suggested state is passed in
|
||||
*/
|
||||
initialize: (frame: FramePublicAPI, state?: P) => T;
|
||||
/**
|
||||
* Can remove any state that should not be persisted to saved object, such as UI state
|
||||
*/
|
||||
getPersistableState: (state: T) => P;
|
||||
|
||||
/**
|
||||
* Visualizations must provide at least one type for the chart switcher,
|
||||
* but can register multiple subtypes
|
||||
*/
|
||||
visualizationTypes: VisualizationType[];
|
||||
/**
|
||||
* If the visualization has subtypes, update the subtype in state.
|
||||
*/
|
||||
switchVisualizationType?: (visualizationTypeId: string, state: T) => T;
|
||||
/** Description is displayed as the clickable text in the chart switcher */
|
||||
getDescription: (state: T) => { icon?: IconType; label: string };
|
||||
|
||||
/** Frame needs to know which layers the visualization is currently using */
|
||||
getLayerIds: (state: T) => string[];
|
||||
/** Reset button on each layer triggers this */
|
||||
clearLayer: (state: T, layerId: string) => T;
|
||||
/** Optional, if the visualization supports multiple layers */
|
||||
removeLayer?: (state: T, layerId: string) => T;
|
||||
/** Track added layers in internal state */
|
||||
appendLayer?: (state: T, layerId: string) => T;
|
||||
|
||||
// Layer context menu is used by visualizations for styling the entire layer
|
||||
// For example, the XY visualization uses this to have multiple chart types
|
||||
getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined;
|
||||
renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void;
|
||||
|
||||
/**
|
||||
* For consistency across different visualizations, the dimension configuration UI is standardized
|
||||
*/
|
||||
getConfiguration: (
|
||||
props: VisualizationConfigProps<T>
|
||||
) => { groups: VisualizationDimensionGroupConfig[] };
|
||||
|
||||
getDescription: (
|
||||
state: T
|
||||
) => {
|
||||
icon?: IconType;
|
||||
label: string;
|
||||
};
|
||||
|
||||
switchVisualizationType?: (visualizationTypeId: string, state: T) => T;
|
||||
|
||||
// For initializing from saved object
|
||||
initialize: (frame: FramePublicAPI, state?: P) => T;
|
||||
|
||||
getPersistableState: (state: T) => P;
|
||||
|
||||
// Actions triggered by the frame which tell the datasource that a dimension is being changed
|
||||
setDimension: (
|
||||
props: VisualizationDimensionChangeProps<T> & {
|
||||
groupId: string;
|
||||
}
|
||||
) => T;
|
||||
removeDimension: (props: VisualizationDimensionChangeProps<T>) => T;
|
||||
|
||||
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||
/**
|
||||
* Popover contents that open when the user clicks the contextMenuIcon. This can be used
|
||||
* for extra configurability, such as for styling the legend or axis
|
||||
*/
|
||||
renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void;
|
||||
/**
|
||||
* Visualizations can provide a custom icon which will open a layer-specific popover
|
||||
* If no icon is provided, gear icon is default
|
||||
*/
|
||||
getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined;
|
||||
|
||||
/**
|
||||
* Epression to render a preview version of the chart in very constraint space.
|
||||
* The frame is telling the visualization to update or set a dimension based on user interaction
|
||||
* groupId is coming from the groupId provided in getConfiguration
|
||||
*/
|
||||
setDimension: (props: VisualizationDimensionChangeProps<T> & { groupId: string }) => T;
|
||||
/**
|
||||
* The frame is telling the visualization to remove a dimension. The visualization needs to
|
||||
* look at its internal state to determine which dimension is being affected.
|
||||
*/
|
||||
removeDimension: (props: VisualizationDimensionChangeProps<T>) => T;
|
||||
|
||||
/**
|
||||
* The frame will call this function on all visualizations at different times. The
|
||||
* main use cases where visualization suggestions are requested are:
|
||||
* - When dragging a field
|
||||
* - When opening the chart switcher
|
||||
* If the state is provided when requesting suggestions, the visualization is active.
|
||||
* Most visualizations will apply stricter filtering to suggestions when they are active,
|
||||
* because suggestions have the potential to remove the users's work in progress.
|
||||
*/
|
||||
getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>;
|
||||
|
||||
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||
/**
|
||||
* Expression to render a preview version of the chart in very constrained space.
|
||||
* If there is no expression provided, the preview icon is used.
|
||||
*/
|
||||
toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||
|
||||
// The frame will call this function on all visualizations when the table changes, or when
|
||||
// rendering additional ways of using the data
|
||||
getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue