From 7b6aa3c674cc8daff60a12bc9e3c8f5f94f0ae68 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 19 Feb 2020 16:01:20 -0500 Subject: [PATCH 01/16] [Lens] Declarative right panel --- .../datatable_visualization/visualization.tsx | 115 ++--- .../editor_frame/config_panel_wrapper.tsx | 382 +++++++++++++--- .../editor_frame/editor_frame.tsx | 120 ++--- .../editor_frame/frame_layout.tsx | 27 +- .../dimension_panel/dimension_panel.tsx | 314 +++++++------ .../dimension_panel/popover_editor.tsx | 426 +++++++++--------- .../indexpattern_datasource/indexpattern.tsx | 112 +++-- .../public/indexpattern_datasource/loader.ts | 1 + .../metric_config_panel.test.tsx | 69 --- .../metric_config_panel.tsx | 39 -- .../metric_visualization.tsx | 43 +- .../lens/public/metric_visualization/types.ts | 2 +- .../lens/public/multi_column_editor/index.ts | 7 - .../multi_column_editor.test.tsx | 71 --- .../multi_column_editor.tsx | 63 --- x-pack/legacy/plugins/lens/public/types.ts | 124 ++++- .../lens/public/xy_visualization/types.ts | 4 +- .../xy_visualization/xy_config_panel.tsx | 102 +---- .../public/xy_visualization/xy_suggestions.ts | 3 +- .../xy_visualization/xy_visualization.tsx | 102 ++++- 20 files changed, 1106 insertions(+), 1020 deletions(-) delete mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts delete mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 79a018635134f..a1e48fd9869cb 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -4,20 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -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, - VisualizationLayerConfigProps, - VisualizationSuggestion, - Operation, -} from '../types'; -import { generateId } from '../id_generator'; +import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; import chartTableSVG from '../assets/chart_datatable.svg'; export interface LayerState { @@ -32,58 +20,10 @@ export interface DatatableVisualizationState { function newLayerState(layerId: string): LayerState { return { layerId, - columns: [generateId()], + columns: [], }; } -function updateColumns( - state: DatatableVisualizationState, - layer: LayerState, - fn: (columns: string[]) => string[] -) { - const columns = fn(layer.columns); - const updatedLayer = { ...layer, columns }; - const layers = state.layers.map(l => (l.layerId === layer.layerId ? updatedLayer : l)); - return { ...state, layers }; -} - -const allOperations = () => true; - -export function DataTableLayer({ - layer, - frame, - state, - setState, - dragDropContext, -}: { layer: LayerState } & VisualizationLayerConfigProps) { - const datasource = frame.datasourceLayers[layer.layerId]; - - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - - return ( - - 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" - /> - - ); -} - export const datatableVisualization: Visualization< DatatableVisualizationState, DatatableVisualizationState @@ -188,17 +128,48 @@ export const datatableVisualization: Visualization< ]; }, - renderLayerConfigPanel(domElement, props) { - const layer = props.state.layers.find(l => l.layerId === props.layerId); + getLayerOptions(props) { + return { + dimensions: [ + { + layerId: props.state.layers[0].layerId, + accessors: props.state.layers[0].columns, + dimensionId: 'columns', + dimensionLabel: i18n.translate('xpack.lens.datatable.columns', { + defaultMessage: 'Columns', + }), + supportsMoreColumns: true, + filterOperations: () => true, + }, + ], + }; + }, - if (layer) { - render( - - - , - domElement - ); - } + setDimension({ dimensionId, layerId, columnId, prevState }) { + return { + ...prevState, + layers: prevState.layers.map(l => + l.layerId === layerId + ? { + ...l, + columns: l.columns.filter(c => c !== columnId), + } + : l + ), + }; + }, + removeDimension({ prevState, columnId, layerId }) { + return { + ...prevState, + layers: prevState.layers.map(l => + l.layerId === layerId + ? { + ...l, + columns: l.columns.filter(c => c !== columnId), + } + : l + ), + }; }, toExpression(state, frame) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 1422ee86be3e9..0aadc69501f2b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -16,8 +16,10 @@ import { 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 { @@ -25,8 +27,10 @@ import { FramePublicAPI, Datasource, VisualizationLayerConfigProps, + DatasourceDimensionEditorProps, + StateSetter, } from '../../types'; -import { DragContext } from '../../drag_drop'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { ChartSwitch } from './chart_switch'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { generateId } from '../../id_generator'; @@ -47,6 +51,7 @@ interface ConfigPanelWrapperProps { state: unknown; } >; + core: DatasourceDimensionEditorProps['core']; } export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { @@ -86,18 +91,28 @@ function LayerPanels( activeDatasourceId, datasourceMap, } = props; - const dragDropContext = useContext(DragContext); - const setState = useMemo( + const setVisualizationState = useMemo( () => (newState: unknown) => { props.dispatch({ type: 'UPDATE_VISUALIZATION_STATE', visualizationId: activeVisualization.id, newState, - clearStagedPreview: false, + clearStagedPreview: true, }); }, [props.dispatch, activeVisualization] ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: true, + }); + }, + [props.dispatch] + ); const layerIds = activeVisualization.getLayerIds(visualizationState); return ( @@ -108,9 +123,9 @@ function LayerPanels( key={layerId} layerId={layerId} activeVisualization={activeVisualization} - dragDropContext={dragDropContext} - state={setState} - setState={setState} + visualizationState={visualizationState} + updateVisualization={setVisualizationState} + updateDatasource={updateDatasource} frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} onRemove={() => { @@ -174,85 +189,314 @@ function LayerPanels( } function LayerPanel( - props: ConfigPanelWrapperProps & - VisualizationLayerConfigProps & { - isOnlyLayer: boolean; - activeVisualization: Visualization; - onRemove: () => void; - } + props: Exclude & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + onRemove: () => void; + } ) { + const dragDropContext = useContext(DragContext); const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - const layerConfigProps = { + const layerVisualizationConfigProps = { layerId, - dragDropContext: props.dragDropContext, + dragDropContext, state: props.visualizationState, - setState: props.setState, + setState: props.updateVisualization, frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; - return ( - - - - - + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; + + const layerDatasourceConfigProps = { + ...layerDatasourceDropProps, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; - {datasourcePublicAPI && ( - - ({ + isOpen: false, + openId: null, + }); + + function wrapInPopover(id: string, trigger: React.ReactElement, panel: React.ReactElement) { + return ( + { + setPopoverState({ isOpen: false, openId: null }); + }} + button={trigger} + display="block" + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + + ); + } + + return ( + + + + + - )} - - + {datasourcePublicAPI && ( + + + + )} + - + + + {activeVisualization + .getLayerOptions(layerVisualizationConfigProps) + .dimensions.map((dimension, index) => { + const newId = generateId(); + return ( + + <> + {dimension.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: dimension.filterOperations, + }); + }} + > + {wrapInPopover( + accessor, + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + }); + } + }, + }} + />, + + )} - + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateVisualization( + props.activeVisualization.removeDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {dimension.supportsMoreColumns ? ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: dimension.filterOperations, + }); + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + }} + > + {wrapInPopover( + dimension.dimensionLabel, + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: dimension.dimensionLabel, + }); + } + }} + size="xs" + > + + , + - - { - // 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 }; + setState: (newState: unknown) => { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + props.updateDatasource(datasourceId, newState); + }, + }} + /> + )} + + ) : null} + + + ); + })} - if (el && el.blur) { - el.blur(); - } + - onRemove(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - - + + + { + // 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', + })} + + + + + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index c8d7cf29a3561..1cf74beddbbb8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -21,6 +21,7 @@ import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; +import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; @@ -236,74 +237,79 @@ export function EditorFrame(props: EditorFrameProps) { ]); return ( - - } - configPanel={ - allLoaded && ( - + - ) - } - workspacePanel={ - allLoaded && ( - - + ) + } + workspacePanel={ + allLoaded && ( + + + + ) + } + suggestionsPanel={ + allLoaded && ( + - - ) - } - suggestionsPanel={ - allLoaded && ( - - ) - } - /> + ) + } + /> + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index a69da8b49e233..56afe3ed69a73 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; -import { RootDragDropProvider } from '../../drag_drop'; export interface FrameLayoutProps { dataPanel: React.ReactNode; @@ -17,19 +16,17 @@ export interface FrameLayoutProps { export function FrameLayout(props: FrameLayoutProps) { return ( - - -
- {props.dataPanel} - - {props.workspacePanel} - {props.suggestionsPanel} - - - {props.configPanel} - -
-
-
+ +
+ {props.dataPanel} + + {props.workspacePanel} + {props.suggestionsPanel} + + + {props.configPanel} + +
+
); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 972c396f93b43..c240220fd5faa 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -6,25 +6,32 @@ import _ from 'lodash'; import React, { memo, useMemo } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; +import { + DatasourceDimensionTriggerProps, + DatasourceDimensionEditorProps, + DatasourceDimensionDropProps, + DatasourceDimensionDropHandlerProps, +} from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; -import { PopoverEditor } from './popover_editor'; -import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn, deleteColumn } from '../state_helpers'; +import { PopoverEditor, PopoverTrigger } from './popover_editor'; +import { changeColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { DateRange } from '../../../../../../plugins/lens/common'; -export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { - state: IndexPatternPrivateState; - setState: StateSetter; - dragDropContext: DragContextState; +export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< + IndexPatternPrivateState +> & { + uniqueLabel: string; +}; + +export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< + IndexPatternPrivateState +> & { uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; @@ -39,151 +46,174 @@ export interface OperationFieldSupportMatrix { fieldByOperation: Partial>; } -export const IndexPatternDimensionPanelComponent = function IndexPatternDimensionPanel( - props: IndexPatternDimensionPanelProps -) { +type Props = Pick< + DatasourceDimensionDropProps, + 'layerId' | 'columnId' | 'state' | 'filterOperations' +>; +let memoizedFieldSupportFn: (props: Props) => OperationFieldSupportMatrix; +function getOperationFieldSupportMatrix(props: Props): OperationFieldSupportMatrix { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - const operationFieldSupportMatrix = useMemo(() => { - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter(operation => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach(operation => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - }); - }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; - }, [currentIndexPattern, props.filterOperations]); + if (!memoizedFieldSupportFn) { + memoizedFieldSupportFn = _.memoize( + () => { + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter(operation => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach(operation => { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } + }); + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; + }, + () => { + return `${currentIndexPattern.id} ${props.columnId}`; + } + ); + } - const selectedColumn: IndexPatternColumn | null = - props.state.layers[layerId].columns[props.columnId] || null; + return memoizedFieldSupportFn(props); +} + +export function canHandleDrop(props: DatasourceDimensionDropProps) { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const { dragging } = props.dragDropContext; + const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { return Boolean(operationFieldSupportMatrix.operationByField[field.name]); } - function canHandleDrop() { - const { dragging } = props.dragDropContext; - const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; + return ( + isDraggedField(dragging) && + layerIndexPatternId === dragging.indexPatternId && + Boolean(hasOperationForField(dragging.field)) + ); +} - return ( - isDraggedField(dragging) && - layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) - ); +export function onDrop(props: DatasourceDimensionDropHandlerProps) { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const droppedItem = props.droppedItem; + + function hasOperationForField(field: IndexPatternField) { + return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + } + + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { + // TODO: What do we do if we couldn't find a column? + return; } + const operationsForNewField = + operationFieldSupportMatrix.operationByField[droppedItem.field.name]; + + const layerId = props.layerId; + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + + // We need to check if dragging in a new field, was just a field change on the same + // index pattern and on the same operations (therefore checking if the new field supports + // our previous operation) + const hasFieldChanged = + selectedColumn && + hasField(selectedColumn) && + selectedColumn.sourceField !== droppedItem.field.name && + operationsForNewField && + operationsForNewField.includes(selectedColumn.operationType); + + // If only the field has changed use the onFieldChange method on the operation to get the + // new column, otherwise use the regular buildColumn to get a new column. + const newColumn = hasFieldChanged + ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) + : buildColumn({ + op: operationsForNewField ? operationsForNewField[0] : undefined, + columns: props.state.layers[props.layerId].columns, + indexPattern: currentIndexPattern, + layerId, + suggestedPriority: props.suggestedPriority, + field: droppedItem.field, + }); + + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + + props.setState( + changeColumn({ + state: props.state, + layerId, + columnId: props.columnId, + newColumn, + // If the field has changed, the onFieldChange method needs to take care of everything including moving + // over params. If we create a new column above we want changeColumn to move over params. + keepParams: !hasFieldChanged, + }) + ); +} + +export const IndexPatternDimensionTriggerComponent = function IndexPatternDimensionTrigger( + props: IndexPatternDimensionTriggerProps +) { + const layerId = props.layerId; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + + return ( + + ); +}; + +export const IndexPatternDimensionEditorComponent = function IndexPatternDimensionPanel( + props: IndexPatternDimensionEditorProps +) { + const layerId = props.layerId; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + return ( - - { - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { - // TODO: What do we do if we couldn't find a column? - return; - } - - const operationsForNewField = - operationFieldSupportMatrix.operationByField[droppedItem.field.name]; - - // We need to check if dragging in a new field, was just a field change on the same - // index pattern and on the same operations (therefore checking if the new field supports - // our previous operation) - const hasFieldChanged = - selectedColumn && - hasField(selectedColumn) && - selectedColumn.sourceField !== droppedItem.field.name && - operationsForNewField && - operationsForNewField.includes(selectedColumn.operationType); - - // If only the field has changed use the onFieldChange method on the operation to get the - // new column, otherwise use the regular buildColumn to get a new column. - const newColumn = hasFieldChanged - ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) - : buildColumn({ - op: operationsForNewField ? operationsForNewField[0] : undefined, - columns: props.state.layers[props.layerId].columns, - indexPattern: currentIndexPattern, - layerId, - suggestedPriority: props.suggestedPriority, - field: droppedItem.field, - }); - - trackUiEvent('drop_onto_dimension'); - const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); - trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - - props.setState( - changeColumn({ - state: props.state, - layerId, - columnId: props.columnId, - newColumn, - // If the field has changed, the onFieldChange method needs to take care of everything including moving - // over params. If we create a new column above we want changeColumn to move over params. - keepParams: !hasFieldChanged, - }) - ); - }} - > - - {selectedColumn && ( - { - trackUiEvent('indexpattern_dimension_removed'); - props.setState( - deleteColumn({ - state: props.state, - layerId, - columnId: props.columnId, - }) - ); - if (props.onRemove) { - props.onRemove(props.columnId); - } - }} - /> - )} - - + ); }; -export const IndexPatternDimensionPanel = memo(IndexPatternDimensionPanelComponent); +export const IndexPatternDimensionTrigger = memo(IndexPatternDimensionTriggerComponent); +export const IndexPatternDimensionEditor = memo(IndexPatternDimensionEditorComponent); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 98773c04db4a6..3af5a94de2446 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -9,7 +9,6 @@ import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiPopover, EuiFlexItem, EuiFlexGroup, EuiSideNav, @@ -22,7 +21,11 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { IndexPatternDimensionPanelProps, OperationFieldSupportMatrix } from './dimension_panel'; +import { + IndexPatternDimensionTriggerProps, + IndexPatternDimensionEditorProps, + OperationFieldSupportMatrix, +} from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -38,7 +41,13 @@ import { trackUiEvent } from '../../lens_ui_telemetry'; const operationPanels = getOperationDisplay(); -export interface PopoverEditorProps extends IndexPatternDimensionPanelProps { +export interface PopoverTriggerProps extends IndexPatternDimensionTriggerProps { + selectedColumn?: IndexPatternColumn; + operationFieldSupportMatrix: OperationFieldSupportMatrix; + currentIndexPattern: IndexPattern; +} + +export interface PopoverEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; operationFieldSupportMatrix: OperationFieldSupportMatrix; currentIndexPattern: IndexPattern; @@ -66,11 +75,9 @@ export function PopoverEditor(props: PopoverEditorProps) { setState, layerId, currentIndexPattern, - uniqueLabel, hideGrouping, } = props; const { operationByField, fieldByOperation } = operationFieldSupportMatrix; - const [isPopoverOpen, setPopoverOpen] = useState(false); const [ incompatibleSelectedOperationType, setInvalidOperationType, @@ -184,227 +191,206 @@ export function PopoverEditor(props: PopoverEditorProps) { } return ( - { - setPopoverOpen(!isPopoverOpen); +
+ + + { + setState( + deleteColumn({ + state, + layerId, + columnId, + }) + ); }} - data-test-subj="indexPattern-configure-dimension" - aria-label={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - title={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - > - {uniqueLabel} - - ) : ( - <> - setPopoverOpen(!isPopoverOpen)} - size="xs" - > - - - - ) - } - isOpen={isPopoverOpen} - closePopover={() => { - setPopoverOpen(false); - setInvalidOperationType(null); - }} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {isPopoverOpen && ( - - - { - setState( - deleteColumn({ - state, - layerId, - columnId, - }) - ); - }} - onChoose={choice => { - let column: IndexPatternColumn; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // If we just changed the field are not in an error state and the operation didn't change, - // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); - } else { - // Otherwise we'll use the buildColumn method to calculate a new column - const compatibleOperations = - ('field' in choice && - operationFieldSupportMatrix.operationByField[choice.field]) || - []; - let operation; - if (compatibleOperations.length > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations[0]; - } else if ('field' in choice) { - operation = choice.operationType; - } - column = buildColumn({ - columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], - indexPattern: currentIndexPattern, - layerId: props.layerId, - suggestedPriority: props.suggestedPriority, - op: operation as OperationType, - }); + onChoose={choice => { + let column: IndexPatternColumn; + if ( + !incompatibleSelectedOperationType && + selectedColumn && + 'field' in choice && + choice.operationType === selectedColumn.operationType + ) { + // If we just changed the field are not in an error state and the operation didn't change, + // we use the operations onFieldChange method to calculate the new column. + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + } else { + // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && + operationFieldSupportMatrix.operationByField[choice.field]) || + []; + let operation; + if (compatibleOperations.length > 0) { + operation = + incompatibleSelectedOperationType && + compatibleOperations.includes(incompatibleSelectedOperationType) + ? incompatibleSelectedOperationType + : compatibleOperations[0]; + } else if ('field' in choice) { + operation = choice.operationType; } + column = buildColumn({ + columns: props.state.layers[props.layerId].columns, + field: fieldMap[choice.field], + indexPattern: currentIndexPattern, + layerId: props.layerId, + suggestedPriority: props.suggestedPriority, + op: operation as OperationType, + }); + } - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: column, - keepParams: false, - }) - ); - setInvalidOperationType(null); - }} - /> - - - - - - - - {incompatibleSelectedOperationType && selectedColumn && ( - - )} - {incompatibleSelectedOperationType && !selectedColumn && ( - + + + + + + + + {incompatibleSelectedOperationType && selectedColumn && ( + + )} + {incompatibleSelectedOperationType && !selectedColumn && ( + + )} + {!incompatibleSelectedOperationType && ParamEditor && ( + <> + - )} - {!incompatibleSelectedOperationType && ParamEditor && ( - <> - - - - )} - {!incompatibleSelectedOperationType && selectedColumn && ( - - { - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: { - ...selectedColumn, - label: e.target.value, - }, - }) - ); - }} - /> - - )} - - {!hideGrouping && ( - { - setState({ - ...state, - layers: { - ...state.layers, - [props.layerId]: { - ...state.layers[props.layerId], - columnOrder, + + + )} + {!incompatibleSelectedOperationType && selectedColumn && ( + + { + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: { + ...selectedColumn, + label: e.target.value, }, - }, - }); + }) + ); }} /> - )} - - - - - )} - + + )} + + {!hideGrouping && ( + { + setState({ + ...state, + layers: { + ...state.layers, + [props.layerId]: { + ...state.layers[props.layerId], + columnOrder, + }, + }, + }); + }} + /> + )} + + + + + ) +
+ ); +} + +export function PopoverTrigger(props: PopoverTriggerProps) { + const { selectedColumn, columnId, uniqueLabel } = props; + return ( +
+ {selectedColumn ? ( + { + props.togglePopover(); + }} + data-test-subj="indexPattern-configure-dimension" + aria-label={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + title={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + > + {uniqueLabel} + + ) : null} +
); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index afb88d1af7951..12435e24d1a1b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -12,7 +12,8 @@ import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { - DatasourceDimensionPanelProps, + DatasourceDimensionEditorProps, + DatasourceDimensionTriggerProps, DatasourceDataPanelProps, Operation, DatasourceLayerPanelProps, @@ -20,7 +21,12 @@ import { } from '../types'; import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; -import { IndexPatternDimensionPanel } from './dimension_panel'; +import { + IndexPatternDimensionTrigger, + IndexPatternDimensionEditor, + canHandleDrop, + onDrop, +} from './dimension_panel'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -80,6 +86,9 @@ export function uniqueLabels(layers: Record) { }; Object.values(layers).forEach(layer => { + if (!layer.columns) { + return; + } Object.entries(layer.columns).forEach(([columnId, column]) => { columnLabelMap[columnId] = makeUnique(column.label); }); @@ -198,15 +207,72 @@ export function getIndexPatternDatasource({ ); }, - getPublicAPI({ - state, - setState, - layerId, - dateRange, - }: PublicAPIProps) { + renderDimensionTrigger: ( + domElement: Element, + props: DatasourceDimensionTriggerProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderDimensionEditor: ( + domElement: Element, + props: DatasourceDimensionEditorProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + canHandleDrop, + onDrop, + + getPublicAPI({ state, setState, layerId }: PublicAPIProps) { const columnLabelMap = uniqueLabels(state.layers); return { + datasourceId: 'indexpattern', + getTableSpec: () => { return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId })); }, @@ -218,36 +284,6 @@ export function getIndexPatternDatasource({ } return null; }, - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { - render( - - - - - , - domElement - ); - }, renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => { render( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index ed3d8a91b366d..fa6866f9238d4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -134,6 +134,7 @@ export async function changeIndexPattern({ patterns: [id], }); + debugger; setState(s => ({ ...s, layers: isSingleEmptyLayer(state.layers) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx deleted file mode 100644 index eac35f82a50fa..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx +++ /dev/null @@ -1,69 +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 from 'react'; -import { ReactWrapper } from 'enzyme'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { MetricConfigPanel } from './metric_config_panel'; -import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; -import { State } from './types'; -import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; - -describe('MetricConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - - function mockDatasource(): DatasourcePublicAPI { - return createMockDatasource().publicAPIMock; - } - - function testState(): State { - return { - accessor: 'foo', - layerId: 'bar', - }; - } - - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - - test('the value dimension panel only accepts singular numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lns_metric_valueDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual([{ ...exampleOperation, dataType: 'number' }]); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx deleted file mode 100644 index 16e24f247fb68..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx +++ /dev/null @@ -1,39 +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 from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow } from '@elastic/eui'; -import { State } 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: VisualizationLayerConfigProps) { - const { state, frame, layerId } = props; - const datasource = frame.datasourceLayers[layerId]; - - return ( - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx index 6714c05787837..d62321463d725 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -4,23 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; -import { MetricConfigPanel } from './metric_config_panel'; import { Visualization, FramePublicAPI } from '../types'; import { State, PersistableState } from './types'; -import { generateId } from '../id_generator'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, frame: FramePublicAPI, mode: 'reduced' | 'full' = 'full' -): Ast => { +): Ast | null => { + if (!state.accessor) { + return null; + } + const [datasource] = Object.values(frame.datasourceLayers); const operation = datasource && datasource.getOperationForColumnId(state.accessor); @@ -57,7 +56,7 @@ export const metricVisualization: Visualization = { clearLayer(state) { return { ...state, - accessor: generateId(), + accessor: undefined, }; }, @@ -80,22 +79,36 @@ export const metricVisualization: Visualization = { return ( state || { layerId: frame.addNewLayer(), - accessor: generateId(), + accessor: undefined, } ); }, getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getLayerOptions(props) { + return { + dimensions: [ + { + dimensionId: '', + dimensionLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), + layerId: props.state.layerId, + accessors: props.state.accessor ? [props.state.accessor] : [], + supportsMoreColumns: false, + filterOperations: () => true, + }, + ], + }; + }, toExpression, toPreviewExpression: (state: State, frame: FramePublicAPI) => toExpression(state, frame, 'reduced'), + + setDimension({ prevState, columnId }) { + return { ...prevState, accessor: columnId }; + }, + removeDimension({ prevState }) { + return { ...prevState, accessor: undefined }; + }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts index 6348d80b15e2f..d0792afe2f030 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts @@ -6,7 +6,7 @@ export interface State { layerId: string; - accessor: string; + accessor: string | undefined; } export interface MetricConfig extends State { diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts b/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts deleted file mode 100644 index 92bad0dc90766..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './multi_column_editor'; diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx deleted file mode 100644 index 38f48c9cdaf72..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ /dev/null @@ -1,71 +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 from 'react'; -import { createMockDatasource } from '../editor_frame_service/mocks'; -import { MultiColumnEditor } from './multi_column_editor'; -import { mount } from 'enzyme'; - -jest.useFakeTimers(); - -describe('MultiColumnEditor', () => { - it('should add a trailing accessor if the accessor list is empty', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); - - it('should add a trailing accessor if the last accessor is configured', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx deleted file mode 100644 index 422f1dcf60f3c..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx +++ /dev/null @@ -1,63 +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, { useEffect } from 'react'; -import { NativeRenderer } from '../native_renderer'; -import { DatasourcePublicAPI, OperationMetadata } from '../types'; -import { DragContextState } from '../drag_drop'; - -interface Props { - accessors: string[]; - datasource: DatasourcePublicAPI; - dragDropContext: DragContextState; - onRemove: (accessor: string) => void; - onAdd: () => void; - filterOperations: (op: OperationMetadata) => boolean; - suggestedPriority?: 0 | 1 | 2 | undefined; - testSubj: string; - layerId: string; -} - -export function MultiColumnEditor({ - accessors, - datasource, - dragDropContext, - onRemove, - onAdd, - filterOperations, - suggestedPriority, - testSubj, - layerId, -}: Props) { - const lastOperation = datasource.getOperationForColumnId(accessors[accessors.length - 1]); - - useEffect(() => { - if (accessors.length === 0 || lastOperation !== null) { - setTimeout(onAdd); - } - }, [lastOperation]); - - return ( - <> - {accessors.map(accessor => ( -
- -
- ))} - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b62b920429e7c..d0727056827ce 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -35,6 +35,7 @@ export interface EditorFrameProps { savedQuery?: SavedQuery; // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state onChange: (newState: { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; @@ -141,6 +142,10 @@ export interface Datasource { getLayers: (state: T) => string[]; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; + renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; + renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; + canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps) => void; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -156,11 +161,12 @@ export interface Datasource { * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { + datasourceId: string; getTableSpec: () => TableSpec; getOperationForColumnId: (columnId: string) => Operation | null; // Render can be called many times - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; + // renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; } @@ -183,29 +189,38 @@ export interface DatasourceDataPanelProps { } // The only way a visualization has to restrict the query building -export interface DatasourceDimensionPanelProps { - layerId: string; - columnId: string; +export type DatasourceDimensionEditorProps = FrameDimensionPanelProps & { + state: T; + setState: StateSetter; + core: Pick; + dateRange: DateRange; +}; +export type DatasourceDimensionTriggerProps = FrameDimensionPanelProps & { + state: T; dragDropContext: DragContextState; + togglePopover: () => void; +}; +export interface DatasourceLayerPanelProps { + layerId: string; +} + +export interface DatasourceDimensionDropProps { + layerId: string; + columnId: string; // Visualizations can restrict operations based on their own rules filterOperations: (operation: OperationMetadata) => boolean; - - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query suggestedPriority?: DimensionPriority; - onRemove?: (accessor: string) => void; - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control - hideGrouping?: boolean; + state: T; + setState: StateSetter; + dragDropContext: DragContextState; } -export interface DatasourceLayerPanelProps { - layerId: string; -} +export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { + droppedItem: unknown; +}; export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; @@ -242,7 +257,41 @@ export interface LensMultiTable { export interface VisualizationLayerConfigProps { layerId: string; - dragDropContext: DragContextState; + // dragDropContext: DragContextState; + frame: FramePublicAPI; + state: T; + setState: (newState: T) => void; +} + +interface VisualizationDimensionConfig { + dimensionId: string; + // Displayed to user + dimensionLabel: string; + + supportsMoreColumns: boolean; + accessors: string[]; + + // Visualizations can restrict operations based on their own rules + filterOperations: (operation: OperationMetadata) => boolean; + + // Visualizations can hint at the role this dimension would play, which + // affects the default ordering of the query + suggestedPriority?: DimensionPriority; + onRemove?: (accessor: string) => void; + + // Some dimension editors will allow users to change the operation grouping + // from the panel, and this lets the visualization hint that it doesn't want + // users to have that level of control + hideGrouping?: boolean; +} + +export interface VisualizationLayerConfigResult { + dimensions: VisualizationDimensionConfig[]; +} + +export interface VisualizationDimensionPanelProps { + layerId: string; + // dragDropContext: DragContextState; frame: FramePublicAPI; state: T; setState: (newState: T) => void; @@ -305,6 +354,24 @@ export interface VisualizationSuggestion { previewIcon: IconType; } +export interface FrameDimensionPanelProps { + layerId: string; + columnId: string; + + // Visualizations can restrict operations based on their own rules + filterOperations: (operation: OperationMetadata) => boolean; + + // Visualizations can hint at the role this dimension would play, which + // affects the default ordering of the query + suggestedPriority?: DimensionPriority; + onRemove?: (accessor: string) => void; + + // Some dimension editors will allow users to change the operation grouping + // from the panel, and this lets the visualization hint that it doesn't want + // users to have that level of control + hideGrouping?: boolean; +} + export interface FramePublicAPI { datasourceLayers: Record; @@ -312,6 +379,9 @@ export interface FramePublicAPI { query: Query; filters: Filter[]; + // Render can be called many times + // renderDimensionPanel: (domElement: Element, props: FrameDimensionPanelProps) => void; + // Adds a new layer. This has a side effect of updating the datasource state addNewLayer: () => string; removeLayers: (layerIds: string[]) => void; @@ -330,17 +400,17 @@ export interface Visualization { visualizationTypes: VisualizationType[]; getLayerIds: (state: T) => string[]; - clearLayer: (state: T, layerId: string) => T; - removeLayer?: (state: T, layerId: string) => T; - 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: VisualizationLayerConfigProps) => void; + getLayerOptions: (props: VisualizationLayerConfigProps) => VisualizationLayerConfigResult; + getDescription: ( state: T ) => { @@ -355,7 +425,19 @@ export interface Visualization { getPersistableState: (state: T) => P; - renderLayerConfigPanel: (domElement: Element, props: VisualizationLayerConfigProps) => void; + // Actions triggered by the frame which tell the datasource that a dimension is being changed + setDimension: (props: { + layerId: string; + dimensionId: string; + columnId: string; + prevState: T; + }) => T; + removeDimension: (props: { + layerId: string; + dimensionId: string; + columnId: string; + prevState: T; + }) => T; toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts index b49e6fa6b4b6f..f7b4afc76ec4b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts @@ -191,10 +191,10 @@ export type SeriesType = export interface LayerConfig { hide?: boolean; layerId: string; - xAccessor: string; + xAccessor?: string; accessors: string[]; seriesType: SeriesType; - splitAccessor: string; + splitAccessor?: string; } export type LayerArgs = LayerConfig & { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx index dbcfa24395001..4faa5251b6baf 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -9,16 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; 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'; +import { VisualizationLayerConfigProps } from '../types'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; -const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; -const isBucketed = (op: OperationMetadata) => op.isBucketed; - type UnwrapArray = T extends Array ? P : T; function updateLayer(state: State, layer: UnwrapArray, index: number): State { @@ -74,97 +68,3 @@ export function LayerContextMenu(props: VisualizationLayerConfigProps) { ); } - -export function XYConfigPanel(props: VisualizationLayerConfigProps) { - 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 ( - <> - - - - - - 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} - /> - - - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts index 33181b7f3a467..8924d9f5c9b8a 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -15,7 +15,6 @@ import { TableChangeType, } from '../types'; import { State, SeriesType, XYState } from './types'; -import { generateId } from '../id_generator'; import { getIconForSeries } from './state_helpers'; const columnSortOrder = { @@ -356,7 +355,7 @@ function buildSuggestion({ layerId, seriesType, xAccessor: xValue.columnId, - splitAccessor: splitBy ? splitBy.columnId : generateId(), + splitAccessor: splitBy ? splitBy.columnId : undefined, accessors: yValues.map(col => col.columnId), }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index 75d6fcc7d160b..6e6adf474bf11 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -11,17 +11,18 @@ import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { Visualization } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { Visualization, OperationMetadata } 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'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; const defaultIcon = chartBarStackedSVG; const defaultSeriesType = 'bar_stacked'; +const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isBucketed = (op: OperationMetadata) => op.isBucketed; function getDescription(state?: State) { if (!state) { @@ -133,12 +134,10 @@ export const xyVisualization: Visualization = { layers: [ { layerId: frame.addNewLayer(), - accessors: [generateId()], + accessors: [], position: Position.Top, seriesType: defaultSeriesType, showGridlines: false, - splitAccessor: generateId(), - xAccessor: generateId(), }, ], } @@ -147,13 +146,86 @@ export const xyVisualization: Visualization = { getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getLayerOptions(props) { + const layer = props.state.layers.find(l => l.layerId === props.layerId)!; + return { + dimensions: [ + { + dimensionId: 'x', + dimensionLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { + defaultMessage: 'X-axis', + }), + accessors: layer.xAccessor ? [layer.xAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 1, + supportsMoreColumns: !layer.xAccessor, + }, + { + dimensionId: 'y', + dimensionLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { + defaultMessage: 'Y-axis', + }), + accessors: layer.accessors, + filterOperations: isNumericMetric, + supportsMoreColumns: true, + }, + { + dimensionId: 'breakdown', + dimensionLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { + defaultMessage: 'Break down by', + }), + accessors: layer.splitAccessor ? [layer.splitAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 0, + supportsMoreColumns: !layer.splitAccessor, + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId, dimensionId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (dimensionId === 'x') { + newLayer.xAccessor = columnId; + } + if (dimensionId === 'y') { + newLayer.accessors = [...newLayer.accessors.filter(a => a !== columnId), columnId]; + } + if (dimensionId === 'breakdown') { + newLayer.splitAccessor = columnId; + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, + + removeDimension({ prevState, layerId, columnId, dimensionId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (dimensionId === 'x') { + delete newLayer.xAccessor; + } + if (dimensionId === 'y') { + newLayer.accessors = newLayer.accessors.filter(a => a !== columnId); + } + if (dimensionId === 'breakdown') { + delete newLayer.splitAccessor; + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, getLayerContextMenuIcon({ state, layerId }) { const layer = state.layers.find(l => l.layerId === layerId); @@ -177,8 +249,6 @@ function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { return { layerId, seriesType, - xAccessor: generateId(), - accessors: [generateId()], - splitAccessor: generateId(), + accessors: [], }; } From 7ab5ca16ec8361c93638a810974b645fd0e8d5bb Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 21 Feb 2020 15:52:54 -0500 Subject: [PATCH 02/16] Fix memoized operations --- .../datatable_visualization/visualization.tsx | 16 ++-- .../editor_frame/config_panel_wrapper.tsx | 20 +++-- .../dimension_panel/dimension_panel.tsx | 87 +++++++++---------- .../metric_visualization.tsx | 5 +- x-pack/legacy/plugins/lens/public/types.ts | 2 +- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index a1e48fd9869cb..47bd526965f2e 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -145,17 +145,15 @@ export const datatableVisualization: Visualization< }; }, - setDimension({ dimensionId, layerId, columnId, prevState }) { + setDimension({ layerId, columnId, prevState }) { return { ...prevState, - layers: prevState.layers.map(l => - l.layerId === layerId - ? { - ...l, - columns: l.columns.filter(c => c !== columnId), - } - : l - ), + layers: prevState.layers.map(l => { + if (l.layerId !== layerId || l.columns.includes(columnId)) { + return l; + } + return { ...l, columns: [...l.columns, columnId] }; + }), }; }, removeDimension({ prevState, columnId, layerId }) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 0aadc69501f2b..1b3fccfa74366 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -386,20 +386,22 @@ function LayerPanel( }) } onDrop={droppedItem => { - layerDatasource.onDrop({ + const dropSuccess = layerDatasource.onDrop({ ...layerDatasourceDropProps, droppedItem, columnId: newId, filterOperations: dimension.filterOperations, }); - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - dimensionId: dimension.dimensionId, - columnId: newId, - prevState: props.visualizationState, - }) - ); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } }} > {wrapInPopover( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index c240220fd5faa..24c980a1fc140 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -50,49 +50,44 @@ type Props = Pick< DatasourceDimensionDropProps, 'layerId' | 'columnId' | 'state' | 'filterOperations' >; -let memoizedFieldSupportFn: (props: Props) => OperationFieldSupportMatrix; -function getOperationFieldSupportMatrix(props: Props): OperationFieldSupportMatrix { - const layerId = props.layerId; - const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - - if (!memoizedFieldSupportFn) { - memoizedFieldSupportFn = _.memoize( - () => { - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter(operation => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach(operation => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - }); - }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; - }, - () => { - return `${currentIndexPattern.id} ${props.columnId}`; - } - ); +const getOperationFieldSupportMatrix = _.memoize( + (props: Props): OperationFieldSupportMatrix => { + const layerId = props.layerId; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; + + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter(operation => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach(operation => { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } + }); + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; + }, + + (props: Props) => { + return props.layerId + ' ' + props.columnId; } - - return memoizedFieldSupportFn(props); -} +); export function canHandleDrop(props: DatasourceDimensionDropProps) { const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); @@ -111,7 +106,9 @@ export function canHandleDrop(props: DatasourceDimensionDropProps) { +export function onDrop( + props: DatasourceDimensionDropHandlerProps +): boolean { const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); const droppedItem = props.droppedItem; @@ -121,7 +118,7 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps = { layerId: props.state.layerId, accessors: props.state.accessor ? [props.state.accessor] : [], supportsMoreColumns: false, - filterOperations: () => true, + filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', }, ], }; @@ -108,6 +108,7 @@ export const metricVisualization: Visualization = { setDimension({ prevState, columnId }) { return { ...prevState, accessor: columnId }; }, + removeDimension({ prevState }) { return { ...prevState, accessor: undefined }; }, diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index d0727056827ce..62b406cf09167 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -145,7 +145,7 @@ export interface Datasource { renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; - onDrop: (props: DatasourceDimensionDropHandlerProps) => void; + onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; toExpression: (state: T, layerId: string) => Ast | string | null; From c23c3f9f3c0e77ed2cc157b0d2919ad3492dca4c Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 3 Mar 2020 18:23:00 -0500 Subject: [PATCH 03/16] Add error checking --- .../visualization.test.tsx | 32 +- .../datatable_visualization/visualization.tsx | 6 +- .../editor_frame/config_panel_wrapper.tsx | 349 +++++++++--------- .../dimension_panel/dimension_panel.test.tsx | 129 +++---- .../dimension_panel/dimension_panel.tsx | 3 +- .../dimension_panel/popover_editor.tsx | 20 +- .../indexpattern_datasource/indexpattern.tsx | 1 + .../metric_visualization.test.ts | 6 + x-pack/legacy/plugins/lens/public/types.ts | 2 + .../public/xy_visualization/to_expression.ts | 203 +++++----- .../xy_visualization/xy_config_panel.test.tsx | 154 +------- .../public/xy_visualization/xy_expression.tsx | 2 + .../xy_visualization/xy_visualization.test.ts | 132 +++++++ .../xy_visualization/xy_visualization.tsx | 2 + 14 files changed, 520 insertions(+), 521 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0cba22170df1f..972bfe5007b54 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -6,11 +6,7 @@ import React from 'react'; import { createMockDatasource } from '../editor_frame_service/mocks'; -import { - DatatableVisualizationState, - datatableVisualization, - DataTableLayer, -} from './visualization'; +import { DatatableVisualizationState, datatableVisualization } from './visualization'; import { mount } from 'enzyme'; import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; import { generateId } from '../id_generator'; @@ -217,26 +213,16 @@ describe('Datatable Visualization', () => { describe('DataTableLayer', () => { it('allows all kinds of operations', () => { const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - - mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - expect(datasource.publicAPIMock.renderDimensionPanel).toHaveBeenCalled(); - const filterOperations = - datasource.publicAPIMock.renderDimensionPanel.mock.calls[0][1].filterOperations; + const filterOperations = datatableVisualization.getLayerOptions({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + setState, + frame, + }).dimensions[0].filterOperations; const baseOperation: Operation = { dataType: 'string', diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 47bd526965f2e..7dd14e27f397d 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -128,12 +128,12 @@ export const datatableVisualization: Visualization< ]; }, - getLayerOptions(props) { + getLayerOptions({ state }) { return { dimensions: [ { - layerId: props.state.layers[0].layerId, - accessors: props.state.layers[0].columns, + layerId: state.layers[0].layerId, + accessors: state.layers[0].columns, dimensionId: 'columns', dimensionLabel: i18n.translate('xpack.lens.datatable.columns', { defaultMessage: 'Columns', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 1b3fccfa74366..5182581ce4190 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiButtonEmpty, EuiToolTip, + EuiText, EuiButton, EuiForm, EuiFormRow, @@ -238,6 +239,9 @@ function LayerPanel( openId: null, }); + const { dimensions } = activeVisualization.getLayerOptions(layerVisualizationConfigProps); + const isEmptyLayer = !dimensions.some(d => d.accessors.length > 0); + function wrapInPopover(id: string, trigger: React.ReactElement, panel: React.ReactElement) { return ( - {activeVisualization - .getLayerOptions(layerVisualizationConfigProps) - .dimensions.map((dimension, index) => { - const newId = generateId(); - return ( - - <> - {dimension.accessors.map(accessor => ( - { - layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, + {dimensions.map((dimension, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && dimension.required && dimension.accessors.length === 0; + return ( + + <> + {dimension.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: dimension.filterOperations, + }); + }} + > + {wrapInPopover( + accessor, + - {wrapInPopover( - accessor, - { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: accessor, - }); - } - }, - }} - />, - - )} - - { - trackUiEvent('indexpattern_dimension_removed'); - props.updateVisualization( - props.activeVisualization.removeDimension({ - layerId, - dimensionId: dimension.dimensionId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); - }} - /> - - ))} - {dimension.supportsMoreColumns ? ( - { - const dropSuccess = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: newId, - filterOperations: dimension.filterOperations, - }); - if (dropSuccess) { - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - dimensionId: dimension.dimensionId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - } - }} - > - {wrapInPopover( - dimension.dimensionLabel, - { + togglePopover: () => { if (popoverState.isOpen) { setPopoverState({ isOpen: false, @@ -424,45 +341,139 @@ function LayerPanel( } else { setPopoverState({ isOpen: true, - openId: dimension.dimensionLabel, + openId: accessor, }); } - }} - size="xs" - > - - , - , + + )} - setState: (newState: unknown) => { - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - dimensionId: dimension.dimensionId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - props.updateDatasource(datasourceId, newState); - }, - }} + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateVisualization( + props.activeVisualization.removeDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {dimension.supportsMoreColumns ? ( + { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: dimension.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + {wrapInPopover( + dimension.dimensionLabel, + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: dimension.dimensionLabel, + }); + } + }} + size="xs" + > + - )} - - ) : null} - - - ); - })} + , + { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + dimensionId: dimension.dimensionId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + props.updateDatasource(datasourceId, newState); + }, + }} + /> + )} + + ) : null} + + + ); + })} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 56f75ae4b17be..26c600d15e695 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -17,12 +17,12 @@ import { import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { changeColumn } from '../state_helpers'; import { - IndexPatternDimensionPanel, - IndexPatternDimensionPanelComponent, - IndexPatternDimensionPanelProps, + IndexPatternDimensionEditor, + IndexPatternDimensionEditorComponent, + IndexPatternDimensionEditorProps, } from './dimension_panel'; -import { DropHandler, DragContextState } from '../../drag_drop'; -import { createMockedDragDropContext } from '../mocks'; +// import { DropHandler, DragContextState } from '../../drag_drop'; +// import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; @@ -79,11 +79,11 @@ const expectedIndexPatterns = { }, }; -describe('IndexPatternDimensionPanel', () => { +describe('IndexPatternDimensionEditorPanel', () => { let wrapper: ReactWrapper | ShallowWrapper; let state: IndexPatternPrivateState; let setState: jest.Mock; - let defaultProps: IndexPatternDimensionPanelProps; + let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; function openPopover() { @@ -170,7 +170,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should display a configure button if dimension has no column yet', () => { - wrapper = mount(); + wrapper = mount(); expect( wrapper .find('[data-test-subj="indexPattern-configure-dimension"]') @@ -183,14 +183,14 @@ describe('IndexPatternDimensionPanel', () => { const filterOperations = jest.fn().mockReturnValue(true); wrapper = shallow( - + ); expect(filterOperations).toBeCalled(); }); it('should show field select combo box on click', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -201,7 +201,7 @@ describe('IndexPatternDimensionPanel', () => { it('should not show any choices if the filter returns false', () => { wrapper = mount( - false} @@ -219,7 +219,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -253,7 +253,7 @@ describe('IndexPatternDimensionPanel', () => { }, }, }; - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -267,7 +267,7 @@ describe('IndexPatternDimensionPanel', () => { it('should indicate fields which are incompatible for the operation of the current column', () => { wrapper = mount( - { it('should indicate operations which are incompatible for the field of the current column', () => { wrapper = mount( - { }, }; - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -410,7 +410,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -443,7 +443,7 @@ describe('IndexPatternDimensionPanel', () => { it('should keep the field when switching to another operation compatible for this field', () => { wrapper = mount( - { }); it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -509,7 +509,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should update label on label input changes', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -538,7 +538,7 @@ describe('IndexPatternDimensionPanel', () => { describe('transient invalid state', () => { it('should not set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -552,7 +552,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should show error message in invalid state', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -566,7 +566,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -582,7 +582,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should leave error state if the popover gets closed', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -600,7 +600,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -624,7 +624,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -679,7 +679,7 @@ describe('IndexPatternDimensionPanel', () => { }, }; wrapper = mount( - + ); openPopover(); @@ -713,7 +713,7 @@ describe('IndexPatternDimensionPanel', () => { }, }; wrapper = mount( - + ); openPopover(); @@ -738,7 +738,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -776,7 +776,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should support selecting the operation before the field', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -822,7 +822,7 @@ describe('IndexPatternDimensionPanel', () => { }; wrapper = mount( - + ); openPopover(); @@ -849,7 +849,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should select operation directly if only document is possible', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -874,7 +874,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -918,7 +918,7 @@ describe('IndexPatternDimensionPanel', () => { }, }; wrapper = mount( - + ); openPopover(); @@ -937,7 +937,7 @@ describe('IndexPatternDimensionPanel', () => { it('should show all operations that are not filtered out', () => { wrapper = mount( - !op.isBucketed && op.dataType === 'number'} /> @@ -962,7 +962,7 @@ describe('IndexPatternDimensionPanel', () => { }); it('should add a column on selection of a field', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -1014,7 +1014,7 @@ describe('IndexPatternDimensionPanel', () => { }, }, }; - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -1036,31 +1036,8 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should clear the dimension with the clear button', () => { - wrapper = mount(); - - const clearButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-remove"]' - ); - - act(() => { - clearButton.simulate('click'); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - }, - }, - }); - }); - it('should clear the dimension when removing the selection in field combobox', () => { - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -1104,7 +1081,7 @@ describe('IndexPatternDimensionPanel', () => { }, }; - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -1157,7 +1134,7 @@ describe('IndexPatternDimensionPanel', () => { }, }; - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -1207,7 +1184,7 @@ describe('IndexPatternDimensionPanel', () => { }, }; - wrapper = mount(); + wrapper = mount(); openPopover(); @@ -1288,7 +1265,7 @@ describe('IndexPatternDimensionPanel', () => { it('is not droppable if no drag is happening', () => { wrapper = mount( - + ); expect( @@ -1301,7 +1278,7 @@ describe('IndexPatternDimensionPanel', () => { it('is not droppable if the dragged item has no field', () => { wrapper = shallow( - { it('is not droppable if field is not supported by filterOperations', () => { wrapper = shallow( - { it('is droppable if the field is supported by filterOperations', () => { wrapper = shallow( - { it('is notdroppable if the field belongs to another index pattern', () => { wrapper = shallow( - { }; const testState = dragDropState(); wrapper = shallow( - { }; const testState = dragDropState(); wrapper = shallow( - { }; const testState = dragDropState(); wrapper = shallow( - op.dataType === 'number'} layerId="myLayer" diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 5b8e40a29e7a3..f5d38e4d715de 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -15,7 +15,7 @@ import { DatasourceDimensionDropHandlerProps, } from '../../types'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; -import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; +// import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor, PopoverTrigger } from './popover_editor'; @@ -154,6 +154,7 @@ export function onDrop( layerId, suggestedPriority: props.suggestedPriority, field: droppedItem.field, + previousColumn: selectedColumn, }); trackUiEvent('drop_onto_dimension'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index ffdcdb413a04e..cc82d3bf0f873 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -7,7 +7,6 @@ import _ from 'lodash'; import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexItem, EuiFlexGroup, @@ -16,7 +15,6 @@ import { EuiFormRow, EuiFieldText, EuiLink, - EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; import classNames from 'classnames'; @@ -252,6 +250,7 @@ export function PopoverEditor(props: PopoverEditorProps) { layerId: props.layerId, suggestedPriority: props.suggestedPriority, op: operation as OperationType, + previousColumn: selectedColumn, }); } @@ -359,6 +358,23 @@ export function PopoverEditor(props: PopoverEditorProps) { }} /> )} + + {selectedColumn && selectedColumn.dataType === 'number' ? ( + { + setState( + updateColumnParam({ + state, + layerId, + currentColumn: selectedColumn, + paramName: 'format', + value: newFormat, + }) + ); + }} + /> + ) : null} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 12435e24d1a1b..db72887872aad 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -255,6 +255,7 @@ export function getIndexPatternDatasource({ storage={storage} savedObjectsClient={core.savedObjects.client} http={core.http} + data={data} uniqueLabel={columnLabelMap[props.columnId]} {...props} /> diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 88964b95c2ac7..571b578b876d4 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -72,6 +72,12 @@ describe('metric_visualization', () => { }); }); + describe('#setDimension', () => { + }); + + describe('#removeDimension', () => { + }); + describe('#toExpression', () => { it('should map to a valid AST', () => { const datasource: DatasourcePublicAPI = { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 62b406cf09167..8c8d9f2ea0557 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -283,6 +283,8 @@ interface VisualizationDimensionConfig { // from the panel, and this lets the visualization hint that it doesn't want // users to have that level of control hideGrouping?: boolean; + + required?: boolean; } export interface VisualizationLayerConfigResult { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index f0e932d14f281..8e1902a646267 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -36,25 +36,25 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return null; } - const stateWithValidAccessors = { - ...state, - layers: state.layers.map(layer => { - const datasource = frame.datasourceLayers[layer.layerId]; - - const newLayer = { ...layer }; - - if (!datasource.getOperationForColumnId(layer.splitAccessor)) { - delete newLayer.splitAccessor; - } - - return { - ...newLayer, - accessors: layer.accessors.filter(accessor => - Boolean(datasource.getOperationForColumnId(accessor)) - ), - }; - }), - }; + // const stateWithValidAccessors = { + // ...state, + // layers: state.layers.map(layer => { + // const datasource = frame.datasourceLayers[layer.layerId]; + + // const newLayer = { ...layer }; + + // if (!datasource.getOperationForColumnId(layer.splitAccessor)) { + // delete newLayer.splitAccessor; + // } + + // return { + // ...newLayer, + // accessors: layer.accessors.filter(accessor => + // Boolean(datasource.getOperationForColumnId(accessor)) + // ), + // }; + // }), + // }; const metadata: Record> = {}; state.layers.forEach(layer => { @@ -69,7 +69,8 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); return buildExpression( - stateWithValidAccessors, + // stateWithValidAccessors, + state, metadata, frame, xyTitles(state.layers[0], frame) @@ -122,82 +123,92 @@ export const buildExpression = ( metadata: Record>, frame?: FramePublicAPI, { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } -): Ast => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_chart', - arguments: { - xTitle: [xTitle], - yTitle: [yTitle], - legend: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_legendConfig', - arguments: { - isVisible: [state.legend.isVisible], - position: [state.legend.position], +): Ast | null => { + const validLayers = state.layers.filter(layer => layer.xAccessor && layer.accessors.length); + if (!validLayers.length) { + return null; + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_chart', + arguments: { + xTitle: [xTitle], + yTitle: [yTitle], + legend: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_legendConfig', + arguments: { + isVisible: [state.legend.isVisible], + position: [state.legend.position], + }, }, - }, - ], - }, - ], - layers: state.layers.map(layer => { - const columnToLabel: Record = {}; - - if (frame) { - const datasource = frame.datasourceLayers[layer.layerId]; - layer.accessors.concat([layer.splitAccessor]).forEach(accessor => { - const operation = datasource.getOperationForColumnId(accessor); - if (operation && operation.label) { - columnToLabel[accessor] = operation.label; - } - }); - } - - const xAxisOperation = - frame && frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); - - const isHistogramDimension = Boolean( - xAxisOperation && - xAxisOperation.isBucketed && - xAxisOperation.scale && - xAxisOperation.scale !== 'ordinal' - ); - - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_layer', - arguments: { - layerId: [layer.layerId], - - hide: [Boolean(layer.hide)], - - xAccessor: [layer.xAccessor], - yScaleType: [ - getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), - ], - xScaleType: [ - getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), - ], - isHistogram: [isHistogramDimension], - splitAccessor: [layer.splitAccessor], - seriesType: [layer.seriesType], - accessors: layer.accessors, - columnToLabel: [JSON.stringify(columnToLabel)], + ], + }, + ], + layers: validLayers.map(layer => { + const columnToLabel: Record = {}; + + if (frame) { + const datasource = frame.datasourceLayers[layer.layerId]; + layer.accessors + .concat(layer.splitAccessor ? [layer.splitAccessor] : []) + .forEach(accessor => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation && operation.label) { + columnToLabel[accessor] = operation.label; + } + }); + } + + const xAxisOperation = + frame && + frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor!); + + const isHistogramDimension = Boolean( + xAxisOperation && + xAxisOperation.isBucketed && + xAxisOperation.scale && + xAxisOperation.scale !== 'ordinal' + ); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_layer', + arguments: { + layerId: [layer.layerId], + + hide: [Boolean(layer.hide)], + + xAccessor: [layer.xAccessor], + yScaleType: [ + getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), + ], + xScaleType: [ + getScaleType(metadata[layer.layerId][layer.xAccessor!], ScaleType.Linear), + ], + isHistogram: [isHistogramDimension], + splitAccessor: [layer.splitAccessor], + seriesType: [layer.seriesType], + accessors: layer.accessors, + columnToLabel: [JSON.stringify(columnToLabel)], + }, }, - }, - ], - }; - }), + ], + }; + }), + }, }, - }, - ], -}); + ], + }; +}; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 301c4a58a0ffd..d0ea5d9040eb2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -5,22 +5,15 @@ */ import React from 'react'; -import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; -import { NativeRendererProps } from '../native_renderer'; -import { generateId } from '../id_generator'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; -jest.mock('../id_generator'); - -describe('XYConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - +describe('LayerContextMenu', () => { let frame: FramePublicAPI; function testState(): State { @@ -39,13 +32,6 @@ describe('XYConfigPanel', () => { }; } - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - beforeEach(() => { frame = createMockFramePublicAPI(); frame.datasourceLayers = { @@ -64,7 +50,6 @@ describe('XYConfigPanel', () => { const component = mount( { const component = mount( { expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); }); - - test('the x dimension panel accepts only bucketed operations', () => { - // TODO: this should eventually also accept raw operation - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lnsXY_xDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const bucketedOps: Operation[] = [ - { ...exampleOperation, isBucketed: true, dataType: 'number' }, - { ...exampleOperation, isBucketed: true, dataType: 'string' }, - { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, - { ...exampleOperation, isBucketed: true, dataType: 'date' }, - ]; - const ops: Operation[] = [ - ...bucketedOps, - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual(bucketedOps); - }); - - test('the y dimension panel accepts numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const filterOperations = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('filterOperations') as (op: Operation) => boolean; - - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); - }); - - test('allows removal of y dimensions', () => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onRemove = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onRemove') as (accessor: string) => {}; - - onRemove('b'); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'c'], - }, - ], - }); - }); - - test('allows adding a y axis dimension', () => { - (generateId as jest.Mock).mockReturnValueOnce('zed'); - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onAdd = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'b', 'c', 'zed'], - }, - ], - }); - }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index 27fd6e7064042..80fef47ed7a14 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -238,6 +238,8 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC index ) => { if ( + !xAccessor || + !accessors.length || !data.tables[layerId] || data.tables[layerId].rows.length === 0 || data.tables[layerId].rows.every(row => typeof row[xAccessor] === 'undefined') diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index a27a8e7754b86..e6e9f1f9950ff 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -233,3 +233,135 @@ describe('xy_visualization', () => { }); }); }); + +// test('the x dimension panel accepts only bucketed operations', () => { +// // TODO: this should eventually also accept raw operation +// const state = testState(); +// const component = mount( +// +// ); + +// const panel = testSubj(component, 'lnsXY_xDimensionPanel'); +// const nativeProps = (panel as NativeRendererProps).nativeProps; +// const { columnId, filterOperations } = nativeProps; +// const exampleOperation: Operation = { +// dataType: 'number', +// isBucketed: false, +// label: 'bar', +// }; +// const bucketedOps: Operation[] = [ +// { ...exampleOperation, isBucketed: true, dataType: 'number' }, +// { ...exampleOperation, isBucketed: true, dataType: 'string' }, +// { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, +// { ...exampleOperation, isBucketed: true, dataType: 'date' }, +// ]; +// const ops: Operation[] = [ +// ...bucketedOps, +// { ...exampleOperation, dataType: 'number' }, +// { ...exampleOperation, dataType: 'string' }, +// { ...exampleOperation, dataType: 'boolean' }, +// { ...exampleOperation, dataType: 'date' }, +// ]; +// expect(columnId).toEqual('shazm'); +// expect(ops.filter(filterOperations)).toEqual(bucketedOps); +// }); + +// test('the y dimension panel accepts numeric operations', () => { +// const state = testState(); +// const component = mount( +// +// ); + +// const filterOperations = component +// .find('[data-test-subj="lensXY_yDimensionPanel"]') +// .first() +// .prop('filterOperations') as (op: Operation) => boolean; + +// const exampleOperation: Operation = { +// dataType: 'number', +// isBucketed: false, +// label: 'bar', +// }; +// const ops: Operation[] = [ +// { ...exampleOperation, dataType: 'number' }, +// { ...exampleOperation, dataType: 'string' }, +// { ...exampleOperation, dataType: 'boolean' }, +// { ...exampleOperation, dataType: 'date' }, +// ]; +// expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); +// }); + +// test('allows removal of y dimensions', () => { +// const setState = jest.fn(); +// const state = testState(); +// const component = mount( +// +// ); + +// const onRemove = component +// .find('[data-test-subj="lensXY_yDimensionPanel"]') +// .first() +// .prop('onRemove') as (accessor: string) => {}; + +// onRemove('b'); + +// expect(setState).toHaveBeenCalledTimes(1); +// expect(setState.mock.calls[0][0]).toMatchObject({ +// layers: [ +// { +// ...state.layers[0], +// accessors: ['a', 'c'], +// }, +// ], +// }); +// }); + +// test('allows adding a y axis dimension', () => { +// (generateId as jest.Mock).mockReturnValueOnce('zed'); +// const setState = jest.fn(); +// const state = testState(); +// const component = mount( +// +// ); + +// const onAdd = component +// .find('[data-test-subj="lensXY_yDimensionPanel"]') +// .first() +// .prop('onAdd') as () => {}; + +// onAdd(); + +// expect(setState).toHaveBeenCalledTimes(1); +// expect(setState.mock.calls[0][0]).toMatchObject({ +// layers: [ +// { +// ...state.layers[0], +// accessors: ['a', 'b', 'c', 'zed'], +// }, +// ], +// }); +// }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index 6e6adf474bf11..b1ba1fa4e826c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -159,6 +159,7 @@ export const xyVisualization: Visualization = { filterOperations: isBucketed, suggestedPriority: 1, supportsMoreColumns: !layer.xAccessor, + required: true, }, { dimensionId: 'y', @@ -168,6 +169,7 @@ export const xyVisualization: Visualization = { accessors: layer.accessors, filterOperations: isNumericMetric, supportsMoreColumns: true, + required: true, }, { dimensionId: 'breakdown', From 92a8bea5f26b266f412725f6fe6bc39d3f0bd1f0 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 5 Mar 2020 17:57:48 -0500 Subject: [PATCH 04/16] Fix dimension panel tests --- .../dimension_panel/dimension_panel.test.tsx | 1815 ++++++++--------- .../dimension_panel/dimension_panel.tsx | 69 +- x-pack/legacy/plugins/lens/public/types.ts | 1 + .../xy_visualization/xy_suggestions.test.ts | 68 +- 4 files changed, 917 insertions(+), 1036 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 26c600d15e695..41c317ccab290 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -7,27 +7,28 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { - EuiComboBox, - EuiSideNav, - EuiSideNavItemType, - EuiPopover, - EuiFieldNumber, -} from '@elastic/eui'; +import { EuiComboBox, EuiSideNav, EuiSideNavItemType, EuiFieldNumber } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { changeColumn } from '../state_helpers'; import { - IndexPatternDimensionEditor, IndexPatternDimensionEditorComponent, IndexPatternDimensionEditorProps, + onDrop, + canHandleDrop, } from './dimension_panel'; -// import { DropHandler, DragContextState } from '../../drag_drop'; -// import { createMockedDragDropContext } from '../mocks'; +import { DragContextState } from '../../drag_drop'; +import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { + IUiSettingsClient, + SavedObjectsClientContract, + HttpSetup, + CoreSetup, +} from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; +import { OperationMetadata } from '../../types'; jest.mock('ui/new_platform'); jest.mock('../loader'); @@ -80,19 +81,11 @@ const expectedIndexPatterns = { }; describe('IndexPatternDimensionEditorPanel', () => { - let wrapper: ReactWrapper | ShallowWrapper; let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; - function openPopover() { - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); - } - beforeEach(() => { state = { indexPatternRefs: [], @@ -134,7 +127,6 @@ describe('IndexPatternDimensionEditorPanel', () => { dragDropContext = createMockedDragDropContext(); defaultProps = { - dragDropContext, state, setState, dateRange: { fromDate: 'now-1d', toDate: 'now' }, @@ -158,475 +150,582 @@ describe('IndexPatternDimensionEditorPanel', () => { }), } as unknown) as DataPublicPluginStart['fieldFormats'], } as unknown) as DataPublicPluginStart, + core: {} as CoreSetup, }; jest.clearAllMocks(); }); - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); + describe('Editor component', () => { + let wrapper: ReactWrapper | ShallowWrapper; - it('should display a configure button if dimension has no column yet', () => { - wrapper = mount(); - expect( - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .prop('iconType') - ).toEqual('plusInCircleFilled'); - }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); - it('should call the filterOperations function', () => { - const filterOperations = jest.fn().mockReturnValue(true); + it('should call the filterOperations function', () => { + const filterOperations = jest.fn().mockReturnValue(true); - wrapper = shallow( - - ); + wrapper = shallow( + + ); - expect(filterOperations).toBeCalled(); - }); + expect(filterOperations).toBeCalled(); + }); - it('should show field select combo box on click', () => { - wrapper = mount(); + it('should show field select combo box on click', () => { + wrapper = mount(); - openPopover(); + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(1); + }); - expect( - wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') - ).toHaveLength(1); - }); + it('should not show any choices if the filter returns false', () => { + wrapper = mount( + false} + /> + ); - it('should not show any choices if the filter returns false', () => { - wrapper = mount( - false} - /> - ); + expect( + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')! + .prop('options')! + ).toHaveLength(0); + }); - openPopover(); + it('should list all field names and document as a whole in prioritized order', () => { + wrapper = mount(); - expect( - wrapper + const options = wrapper .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')! - .prop('options')! - ).toHaveLength(0); - }); - - it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect(options).toHaveLength(2); + expect(options).toHaveLength(2); - expect(options![0].label).toEqual('Records'); + expect(options![0].label).toEqual('Records'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'timestamp', - 'bytes', - 'memory', - 'source', - ]); - }); + expect(options![1].options!.map(({ label }) => label)).toEqual([ + 'timestamp', + 'bytes', + 'memory', + 'source', + ]); + }); - it('should hide fields that have no data', () => { - const props = { - ...defaultProps, - state: { - ...defaultProps.state, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - source: true, + it('should hide fields that have no data', () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + source: true, + }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }; + wrapper = mount(); - expect(options![1].options!.map(({ label }) => label)).toEqual(['timestamp', 'source']); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields which are incompatible for the operation of the current column', () => { - wrapper = mount( - label)).toEqual(['timestamp', 'source']); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate fields which are incompatible for the operation of the current column', () => { + wrapper = mount( + - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }} + /> + ); - expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); - it('should indicate operations which are incompatible for the field of the current column', () => { - wrapper = mount( - label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate operations which are incompatible for the field of the current column', () => { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - interface ItemType { - name: string; - 'data-test-subj': string; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; + interface ItemType { + name: string; + 'data-test-subj': string; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; - expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( - 'Incompatible' - ); + expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( + 'Incompatible' + ); - expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( - 'Incompatible' - ); - }); + expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( + 'Incompatible' + ); + }); - it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should keep the operation when switching to another field compatible with this operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, + // Private + operationType: 'max', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); - openPopover(); + it('should switch operations when selecting a field that requires another operation', () => { + wrapper = mount(); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - it('should keep the field when switching to another operation compatible for this field', () => { - wrapper = mount( - { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); + it('should not set the state if selecting the currently active operation', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); - - it('should update label on label input changes', () => { - wrapper = mount(); - - openPopover(); + it('should update label on label input changes', () => { + wrapper = mount(); - act(() => { - wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') - .simulate('change', { target: { value: 'New Label' } }); - }); + act(() => { + wrapper + .find('input[data-test-subj="indexPattern-label-edit"]') + .simulate('change', { target: { value: 'New Label' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'New Label', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'New Label', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); + describe('transient invalid state', () => { + it('should not set the state if selecting an operation incompatible with the current field', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); + it('should show error message in invalid state', () => { + wrapper = mount(); - it('should show error message in invalid state', () => { - wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - openPopover(); + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength( + 0 + ); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(setState).not.toHaveBeenCalled(); + }); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength(0); + it('should leave error state if a compatible operation is selected', () => { + wrapper = mount(); - expect(setState).not.toHaveBeenCalled(); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); - openPopover(); + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + it('should indicate fields compatible with selected operation', () => { + wrapper = mount(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should leave error state if the popover gets closed', () => { - wrapper = mount(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - openPopover(); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + it('should select compatible operation if field not compatible with selected operation', () => { + wrapper = mount( + + ); - act(() => { - wrapper.find(EuiPopover).prop('closePopover')!(); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]'); + const options = comboBox.prop('options'); + + // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation + act(() => { + comboBox.prop('onChange')!([options![1].options![2]]); + }); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); }); - openPopover(); + it('should select the Records field when count is selected', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'bytes', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') + .simulate('click'); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + expect(newColumnState.operationType).toEqual('count'); + expect(newColumnState.sourceField).toEqual('Records'); + }); + + it('should indicate document and field compatibility with selected document operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'count', + sourceField: 'Records', + }, + }, + }, + }, + }; + wrapper = mount( + + ); - it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - openPopover(); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + it('should set datasource state if compatible field is selected for operation', () => { + wrapper = mount(); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); - it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount(); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox + .prop('options')![1] + .options!.find(({ label }) => label === 'source')!; - openPopover(); + act(() => { + comboBox.prop('onChange')!([option]); + }); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, + }, + }, + }); + }); + }); + + it('should support selecting the operation before the field', () => { + wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); @@ -635,9 +734,8 @@ describe('IndexPatternDimensionEditorPanel', () => { .filter('[data-test-subj="indexPattern-dimension-field"]'); const options = comboBox.prop('options'); - // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation act(() => { - comboBox.prop('onChange')!([options![1].options![2]]); + comboBox.prop('onChange')!([options![1].options![0]]); }); expect(setState).toHaveBeenCalledWith({ @@ -648,8 +746,8 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + sourceField: 'bytes', + operationType: 'avg', // Other parts of this don't matter for this test }), }, @@ -659,41 +757,93 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); - it('should select the Records field when count is selected', () => { - const initialState: IndexPatternPrivateState = { + it('should select operation directly if only one field is possible', () => { + const initialState = { + ...state, + indexPatterns: { + 1: { + ...state.indexPatterns['1'], + fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), + }, + }, + }; + + wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); + + it('should select operation directly if only document is possible', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ ...state, layers: { first: { ...state.layers.first, columns: { ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'avg', - sourceField: 'bytes', - }, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - }; - wrapper = mount( - - ); + }); + }); + + it('should indicate compatible fields when selecting the operation first', () => { + wrapper = mount(); - openPopover(); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') - .simulate('click'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; - expect(newColumnState.operationType).toEqual('count'); - expect(newColumnState.sourceField).toEqual('Records'); + expect(options![0]['data-test-subj']).toContain('Incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); }); - it('should indicate document and field compatibility with selected document operation', () => { + it('should indicate document compatibility when document operation is selected', () => { const initialState: IndexPatternPrivateState = { ...state, layers: { @@ -713,45 +863,56 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }; wrapper = mount( - + ); - openPopover(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - const options = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]') .prop('options'); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); + options![1].options!.map(operation => + expect(operation['data-test-subj']).toContain('Incompatible') + ); }); - it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); + it('should show all operations that are not filtered out', () => { + wrapper = mount( + !op.isBucketed && op.dataType === 'number'} + /> + ); - openPopover(); + interface ItemType { + name: React.ReactNode; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; + + expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ + 'Unique count', + 'Average', + 'Count', + 'Maximum', + 'Minimum', + 'Sum', + ]); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - }); + it('should add a column on selection of a field', () => { + wrapper = mount(); const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const option = comboBox.prop('options')![1].options![0]; act(() => { comboBox.prop('onChange')!([option]); @@ -764,456 +925,237 @@ describe('IndexPatternDimensionEditorPanel', () => { ...state.layers.first, columns: { ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + col2: expect.objectContaining({ + sourceField: 'bytes', + // Other parts of this don't matter for this test }), }, + columnOrder: ['col1', 'col2'], }, }, }); }); - }); - - it('should support selecting the operation before the field', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]'); - const options = comboBox.prop('options'); - - act(() => { - comboBox.prop('onChange')!([options![1].options![0]]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only one field is possible', () => { - const initialState = { - ...state, - indexPatterns: { - 1: { - ...state.indexPatterns['1'], - fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), - }, - }, - }; - - wrapper = mount( - - ); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only document is possible', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should indicate document compatibility when document operation is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; - wrapper = mount( - - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - - options![1].options!.map(operation => - expect(operation['data-test-subj']).toContain('Incompatible') - ); - }); - - it('should show all operations that are not filtered out', () => { - wrapper = mount( - !op.isBucketed && op.dataType === 'number'} - /> - ); - - openPopover(); - - interface ItemType { - name: React.ReactNode; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; - - expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ - 'Unique count', - 'Average', - 'Count', - 'Maximum', - 'Minimum', - 'Sum', - ]); - }); - - it('should add a column on selection of a field', () => { - wrapper = mount(); - - openPopover(); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options![0]; - act(() => { - comboBox.prop('onChange')!([option]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should use helper function when changing the function', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', + // Private + operationType: 'max', + sourceField: 'bytes', + }, }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); + }; + wrapper = mount( + + ); - act(() => { - wrapper - .find('[data-test-subj="lns-indexPatternDimension-min"]') - .first() - .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); - }); + act(() => { + wrapper + .find('[data-test-subj="lns-indexPatternDimension-min"]') + .first() + .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); + }); - expect(changeColumn).toHaveBeenCalledWith({ - state: initialState, - columnId: 'col1', - layerId: 'first', - newColumn: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'min', - }), + expect(changeColumn).toHaveBeenCalledWith({ + state: initialState, + columnId: 'col1', + layerId: 'first', + newColumn: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'min', + }), + }); }); - }); - it('should clear the dimension when removing the selection in field combobox', () => { - wrapper = mount(); + it('should clear the dimension when removing the selection in field combobox', () => { + wrapper = mount(); - openPopover(); - - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('onChange')!([]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('onChange')!([]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, }, - }, + }); }); - }); - it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', + it('allows custom format', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, + }), + }, }, }, - }, + }); }); - }); - it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, + it('keeps decimal places while switching', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, }, }, }, }, - }, - }; + }; - wrapper = mount(); + wrapper = mount( + + ); - openPopover(); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: '', label: 'Default' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: '', label: 'Default' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'number', label: 'Number' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'number', label: 'Number' }]); + expect( + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('value') + ).toEqual(0); }); - expect( - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('value') - ).toEqual(0); - }); - - it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, + it('allows custom format with number of decimal places', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('onChange')!({ target: { value: '0' } }); - }); + act(() => { + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('onChange')!({ target: { value: '0' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, + }), + }, }, }, - }, + }); }); }); - describe('drag and drop', () => { + describe('Drag and drop', () => { function dragDropState(): IndexPatternPrivateState { return { indexPatternRefs: [], @@ -1264,112 +1206,80 @@ describe('IndexPatternDimensionEditorPanel', () => { } it('is not droppable if no drag is happening', () => { - wrapper = mount( - - ); - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + canHandleDrop({ + ...defaultProps, + dragDropContext, + state: dragDropState(), + layerId: 'myLayer', + }) + ).toBe(false); }); it('is not droppable if the dragged item has no field', () => { - wrapper = shallow( - - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + }) + ).toBe(false); }); it('is not droppable if field is not supported by filterOperations', () => { - wrapper = shallow( - false} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: () => false, + layerId: 'myLayer', + }) + ).toBe(false); }); it('is droppable if the field is supported by filterOperations', () => { - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeTruthy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(true); }); - it('is notdroppable if the field belongs to another index pattern', () => { - wrapper = shallow( - { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { ...dragDropContext, dragging: { field: { type: 'number', name: 'bar', aggregatable: true }, indexPatternId: 'foo2', }, - }} - state={dragDropState()} - filterOperations={op => op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); }); it('appends the dropped column when a field is dropped', () => { @@ -1378,27 +1288,18 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1426,27 +1327,17 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.isBucketed} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1474,26 +1365,16 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index f5d38e4d715de..329fb47722f18 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -15,7 +15,6 @@ import { DatasourceDimensionDropHandlerProps, } from '../../types'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; -// import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor, PopoverTrigger } from './popover_editor'; @@ -53,44 +52,44 @@ type Props = Pick< DatasourceDimensionDropProps, 'layerId' | 'columnId' | 'state' | 'filterOperations' >; -const getOperationFieldSupportMatrix = _.memoize( - (props: Props): OperationFieldSupportMatrix => { - const layerId = props.layerId; - const currentIndexPattern = - props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter(operation => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach(operation => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - }); +const getOperationFieldSupportMatrix = /* _.memoize( */ ( + props: Props +): OperationFieldSupportMatrix => { + const layerId = props.layerId; + const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; + + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter(operation => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach(operation => { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; - }, + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; +}; /* , (props: Props) => { return props.layerId + ' ' + props.columnId; } -); +);*/ export function canHandleDrop(props: DatasourceDimensionDropProps) { const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 8c8d9f2ea0557..b8321b0a74de5 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -366,6 +366,7 @@ export interface FrameDimensionPanelProps { // Visualizations can hint at the role this dimension would play, which // affects the default ordering of the query suggestedPriority?: DimensionPriority; + onRemove?: (accessor: string) => void; // Some dimension editors will allow users to change the operation grouping diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 04ff720309d62..da49d4594c087 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -123,7 +123,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar_stacked", - "splitAccessor": "aaa", + "splitAccessor": undefined, "x": "date", "y": Array [ "bytes", @@ -472,17 +472,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "quantity", - "y": Array [ - "price", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] + `); }); test('handles ip', () => { @@ -509,17 +509,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "myip", - "y": Array [ - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "myip", + "y": Array [ + "quantity", + ], + }, + ] + `); }); test('handles unbucketed suggestions', () => { @@ -545,16 +545,16 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "eee", - "x": "mybool", - "y": Array [ - "num votes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] + `); }); }); From 9178b13a864d14c36a7fcc028fb013f94a966fd3 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 6 Mar 2020 13:52:55 -0500 Subject: [PATCH 05/16] More updates --- .../editor_frame/editor_frame.test.tsx | 57 ++++++++++--------- .../public/editor_frame_service/mocks.tsx | 25 +++++++- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 4736dd75831e4..d0866417d9706 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -94,14 +94,15 @@ describe('editor_frame', () => { mockVisualization.getLayerIds.mockReturnValue(['first']); mockVisualization2.getLayerIds.mockReturnValue(['second']); - mockDatasource = createMockDatasource(); - mockDatasource2 = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); + mockDatasource2 = createMockDatasource('testDatasource2'); expressionRendererMock = createExpressionRendererMock(); }); describe('initialization', () => { it('should initialize initial datasource', async () => { + mockVisualization.getLayerIds.mockReturnValue([]); act(() => { mount( { }); it('should initialize all datasources with state from doc', async () => { - const mockDatasource3 = createMockDatasource(); + const mockDatasource3 = createMockDatasource('testDatasource3'); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; @@ -209,7 +210,7 @@ describe('editor_frame', () => { ); }); - expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled(); + expect(mockVisualization.getLayerOptions).not.toHaveBeenCalled(); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); await waitForPromises(); }); @@ -308,6 +309,7 @@ describe('editor_frame', () => { mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); mockDatasource2.getLayers.mockReturnValue(['abc', 'def']); mockDatasource2.removeLayer.mockReturnValue({ removed: true }); + mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']); act(() => { mount( { await waitForPromises(); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) ); }); @@ -633,15 +634,15 @@ describe('editor_frame', () => { await waitForPromises(); const updatedState = {}; - const setVisualizationState = (mockVisualization.renderLayerConfigPanel as jest.Mock).mock + const setVisualizationState = (mockVisualization.getLayerOptions as jest.Mock).mock .calls[0][1].setState; act(() => { setVisualizationState(updatedState); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(2); + expect(mockVisualization.getLayerOptions).toHaveBeenLastCalledWith( + // expect.any(Element), expect.objectContaining({ state: updatedState, }) @@ -705,8 +706,9 @@ describe('editor_frame', () => { await waitForPromises(); const updatedPublicAPI: DatasourcePublicAPI = { + datasourceId: 'testDatasource', renderLayerPanel: jest.fn(), - renderDimensionPanel: jest.fn(), + // renderDimensionPanel: jest.fn(), getOperationForColumnId: jest.fn(), getTableSpec: jest.fn(), }; @@ -718,9 +720,9 @@ describe('editor_frame', () => { setDatasourceState({}); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(2); + expect(mockVisualization.getLayerOptions).toHaveBeenLastCalledWith( + // expect.any(Element), expect.objectContaining({ frame: expect.objectContaining({ datasourceLayers: { @@ -772,10 +774,9 @@ describe('editor_frame', () => { await waitForPromises(); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalled(); + expect(mockVisualization.getLayerOptions).toHaveBeenCalled(); - const datasourceLayers = - mockVisualization.renderLayerConfigPanel.mock.calls[0][1].frame.datasourceLayers; + const datasourceLayers = mockVisualization.initialize.mock.calls[0][0].datasourceLayers; expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); @@ -1037,8 +1038,8 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( + // expect.any(Element), expect.objectContaining({ state: { initial: true } }) ); }); @@ -1055,7 +1056,7 @@ describe('editor_frame', () => { datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }), }) ); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( + expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ state: { initial: true } }) ); @@ -1251,9 +1252,9 @@ describe('editor_frame', () => { .simulate('click'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(1); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(1); + expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( + // expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) @@ -1317,8 +1318,8 @@ describe('editor_frame', () => { .simulate('drop'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( + // expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) @@ -1391,7 +1392,7 @@ describe('editor_frame', () => { }); }); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( + expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ state: suggestionVisState, @@ -1487,8 +1488,8 @@ describe('editor_frame', () => { }); }); - expect(mockVisualization3.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization3.getLayerOptions).toHaveBeenCalledWith( + // expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index e606c69c8c386..3981128cbc889 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -33,9 +33,23 @@ export function createMockVisualization(): jest.Mocked { getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_frame, _state?) => ({})), - renderLayerConfigPanel: jest.fn(), + getLayerOptions: jest.fn(() => ({ + dimensions: [ + // { + // layerId: 'layer1', + // dimensionId: 'a', + // dimensionLabel: 'a', + // supportsMoreColumns: true, + // accessors: [], + // filterOperations: jest.fn(() => true), + // }, + ], + })), toExpression: jest.fn((_state, _frame) => null), toPreviewExpression: jest.fn((_state, _frame) => null), + + setDimension: jest.fn(), + removeDimension: jest.fn(), }; } @@ -43,11 +57,11 @@ export type DatasourceMock = jest.Mocked & { publicAPIMock: jest.Mocked; }; -export function createMockDatasource(): DatasourceMock { +export function createMockDatasource(id: string): DatasourceMock { const publicAPIMock: jest.Mocked = { + datasourceId: id, getTableSpec: jest.fn(() => []), getOperationForColumnId: jest.fn(), - renderDimensionPanel: jest.fn(), renderLayerPanel: jest.fn(), }; @@ -66,6 +80,11 @@ export function createMockDatasource(): DatasourceMock { getLayers: jest.fn(_state => []), getMetaData: jest.fn(_state => ({ filterableIndexPatterns: [] })), + renderDimensionTrigger: jest.fn(), + renderDimensionEditor: jest.fn(), + canHandleDrop: jest.fn(), + onDrop: jest.fn(), + // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, From 0e22ae57e2df4453c529c6e2e2e758885c000218 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 6 Mar 2020 14:28:27 -0500 Subject: [PATCH 06/16] Fix all editor frame tests --- .../editor_frame/config_panel_wrapper.tsx | 7 +++++-- .../editor_frame/editor_frame.test.tsx | 14 ++++---------- x-pack/legacy/plugins/lens/public/types.ts | 1 - 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 5182581ce4190..06fbc8fa76d4c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -98,7 +98,7 @@ function LayerPanels( type: 'UPDATE_VISUALIZATION_STATE', visualizationId: activeVisualization.id, newState, - clearStagedPreview: true, + clearStagedPreview: false, }); }, [props.dispatch, activeVisualization] @@ -109,7 +109,7 @@ function LayerPanels( type: 'UPDATE_DATASOURCE_STATE', updater: () => newState, datasourceId, - clearStagedPreview: true, + clearStagedPreview: false, }); }, [props.dispatch] @@ -204,6 +204,9 @@ function LayerPanel( const dragDropContext = useContext(DragContext); const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + if (!datasourcePublicAPI) { + return <>; + } const layerVisualizationConfigProps = { layerId, dragDropContext, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index d0866417d9706..404b03ce88993 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -635,14 +635,13 @@ describe('editor_frame', () => { const updatedState = {}; const setVisualizationState = (mockVisualization.getLayerOptions as jest.Mock).mock - .calls[0][1].setState; + .calls[0][0].setState; act(() => { setVisualizationState(updatedState); }); expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(2); expect(mockVisualization.getLayerOptions).toHaveBeenLastCalledWith( - // expect.any(Element), expect.objectContaining({ state: updatedState, }) @@ -722,7 +721,6 @@ describe('editor_frame', () => { expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(2); expect(mockVisualization.getLayerOptions).toHaveBeenLastCalledWith( - // expect.any(Element), expect.objectContaining({ frame: expect.objectContaining({ datasourceLayers: { @@ -738,6 +736,7 @@ describe('editor_frame', () => { it('should pass the datasource api for each layer to the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']); mount( { expect(mockVisualization.getLayerOptions).toHaveBeenCalled(); - const datasourceLayers = mockVisualization.initialize.mock.calls[0][0].datasourceLayers; + const datasourceLayers = + mockVisualization.getLayerOptions.mock.calls[0][0].frame.datasourceLayers; expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); @@ -1039,7 +1039,6 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState); expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( - // expect.any(Element), expect.objectContaining({ state: { initial: true } }) ); }); @@ -1057,7 +1056,6 @@ describe('editor_frame', () => { }) ); expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( - expect.any(Element), expect.objectContaining({ state: { initial: true } }) ); }); @@ -1254,7 +1252,6 @@ describe('editor_frame', () => { expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(1); expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( - // expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) @@ -1319,7 +1316,6 @@ describe('editor_frame', () => { }); expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( - // expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) @@ -1393,7 +1389,6 @@ describe('editor_frame', () => { }); expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( - expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) @@ -1489,7 +1484,6 @@ describe('editor_frame', () => { }); expect(mockVisualization3.getLayerOptions).toHaveBeenCalledWith( - // expect.any(Element), expect.objectContaining({ state: suggestionVisState, }) diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b8321b0a74de5..ad47795c0309e 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -257,7 +257,6 @@ export interface LensMultiTable { export interface VisualizationLayerConfigProps { layerId: string; - // dragDropContext: DragContextState; frame: FramePublicAPI; state: T; setState: (newState: T) => void; From e8b63fa09114ab41ff1c7068b2c180a6ba6586e1 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 6 Mar 2020 17:55:40 -0500 Subject: [PATCH 07/16] Fix jest tests --- .../visualization.test.tsx | 167 ++++++++---------- .../datatable_visualization/visualization.tsx | 14 +- .../editor_frame/chart_switch.test.tsx | 2 +- .../metric_expression.tsx | 5 + .../metric_visualization.test.ts | 44 ++++- .../xy_visualization/xy_config_panel.test.tsx | 2 +- .../xy_visualization/xy_suggestions.test.ts | 3 +- .../xy_visualization/xy_visualization.test.ts | 27 +-- 8 files changed, 144 insertions(+), 120 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index 972bfe5007b54..28bfae29976bb 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -4,14 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { createMockDatasource } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization } from './visualization'; -import { mount } from 'enzyme'; import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; -import { generateId } from '../id_generator'; - -jest.mock('../id_generator'); function mockFrame(): FramePublicAPI { return { @@ -30,12 +25,11 @@ function mockFrame(): FramePublicAPI { describe('Datatable Visualization', () => { describe('#initialize', () => { it('should initialize from the empty state', () => { - (generateId as jest.Mock).mockReturnValueOnce('id'); expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({ layers: [ { layerId: 'aaa', - columns: ['id'], + columns: [], }, ], }); @@ -84,7 +78,6 @@ describe('Datatable Visualization', () => { describe('#clearLayer', () => { it('should reset the layer', () => { - (generateId as jest.Mock).mockReturnValueOnce('testid'); const state: DatatableVisualizationState = { layers: [ { @@ -97,7 +90,7 @@ describe('Datatable Visualization', () => { layers: [ { layerId: 'baz', - columns: ['testid'], + columns: [], }, ], }); @@ -210,10 +203,30 @@ describe('Datatable Visualization', () => { }); }); - describe('DataTableLayer', () => { + describe('#getLayerOptions', () => { + it('returns a single layer option', () => { + const setState = jest.fn(); + const datasource = createMockDatasource('test'); + const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; + + expect( + datatableVisualization.getLayerOptions({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + setState, + frame, + }).dimensions + ).toHaveLength(1); + }); + it('allows all kinds of operations', () => { const setState = jest.fn(); + const datasource = createMockDatasource('test'); const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; const filterOperations = datatableVisualization.getLayerOptions({ layerId: 'first', @@ -239,108 +252,82 @@ describe('Datatable Visualization', () => { ); }); - it('allows columns to be removed', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); + it('reorders the rendered colums based on the order from the datasource', () => { + const datasource = createMockDatasource('test'); const layer = { layerId: 'a', columns: ['b', 'c'] }; const frame = mockFrame(); frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onRemove = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onRemove') as (k: string) => {}; - - onRemove('b'); + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + + expect( + datatableVisualization.getLayerOptions({ + layerId: 'a', + state: { layers: [layer] }, + setState: jest.fn(), + frame, + }).dimensions[0].accessors + ).toEqual(['c', 'b']); + }); + }); - expect(setState).toHaveBeenCalledWith({ + describe('#removeDimension', () => { + it('allows columns to be removed', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.removeDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + dimensionId: '', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['c'], }, ], }); }); + }); + describe('#setDimension', () => { it('allows columns to be added', () => { - (generateId as jest.Mock).mockReturnValueOnce('d'); - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onAdd = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledWith({ + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'd', + dimensionId: '', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['b', 'c', 'd'], }, ], }); }); - it('reorders the rendered colums based on the order from the datasource', () => { - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={jest.fn()} - state={{ layers: [layer] }} - /> - ); - - const accessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(accessors).toEqual(['b', 'c']); - - component.setProps({ - layer: { layerId: 'a', columns: ['c', 'b'] }, + it('does not set a duplicate dimension', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + dimensionId: '', + }) + ).toEqual({ + layers: [ + { + layerId: 'layer1', + columns: ['b', 'c'], + }, + ], }); - - const newAccessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(newAccessors).toEqual(['c', 'b']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 7dd14e27f397d..1f066e6f5103a 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -128,12 +128,22 @@ export const datatableVisualization: Visualization< ]; }, - getLayerOptions({ state }) { + getLayerOptions({ state, frame, layerId }) { + const layer = state.layers.find(l => l.layerId === layerId); + if (!layer) { + return { dimensions: [] }; + } + + const datasource = frame.datasourceLayers[layer.layerId]; + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + return { dimensions: [ { layerId: state.layers[0].layerId, - accessors: state.layers[0].columns, + accessors: sortedColumns, dimensionId: 'columns', dimensionLabel: i18n.translate('xpack.lens.datatable.columns', { defaultMessage: 'Columns', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx index 1b60098fd45ad..6698c9e68b98c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx @@ -84,7 +84,7 @@ describe('chart_switch', () => { } function mockDatasourceMap() { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('testDatasource'); datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ { state: {}, diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx index 66ed963002f59..4d979a766cd2b 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx @@ -91,6 +91,11 @@ export function MetricChart({ const { title, accessor, mode } = args; let value = '-'; const firstTable = Object.values(data.tables)[0]; + if (!accessor) { + return ( + + ); + } if (firstTable) { const column = firstTable.columns[0]; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 571b578b876d4..237e1c4069898 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -24,8 +24,8 @@ function mockFrame(): FramePublicAPI { ...createMockFramePublicAPI(), addNewLayer: () => 'l42', datasourceLayers: { - l1: createMockDatasource().publicAPIMock, - l42: createMockDatasource().publicAPIMock, + l1: createMockDatasource('l1').publicAPIMock, + l42: createMockDatasource('l42').publicAPIMock, }, }; } @@ -36,10 +36,10 @@ describe('metric_visualization', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); const initialState = metricVisualization.initialize(mockFrame()); - expect(initialState.accessor).toBeDefined(); + expect(initialState.accessor).not.toBeDefined(); expect(initialState).toMatchInlineSnapshot(` Object { - "accessor": "test-id1", + "accessor": undefined, "layerId": "l42", } `); @@ -60,7 +60,7 @@ describe('metric_visualization', () => { it('returns a clean layer', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({ - accessor: 'test-id1', + accessor: undefined, layerId: 'l1', }); }); @@ -73,15 +73,47 @@ describe('metric_visualization', () => { }); describe('#setDimension', () => { + it('sets the accessor', () => { + expect( + metricVisualization.setDimension({ + prevState: { + accessor: undefined, + layerId: 'l1', + }, + layerId: 'l1', + dimensionId: '', + columnId: 'newDimension', + }) + ).toEqual({ + accessor: 'newDimension', + layerId: 'l1', + }); + }); }); describe('#removeDimension', () => { + it('removes the accessor', () => { + expect( + metricVisualization.removeDimension({ + prevState: { + accessor: 'a', + layerId: 'l1', + }, + layerId: 'l1', + dimensionId: '', + columnId: 'a', + }) + ).toEqual({ + accessor: undefined, + layerId: 'l1', + }); + }); }); describe('#toExpression', () => { it('should map to a valid AST', () => { const datasource: DatasourcePublicAPI = { - ...createMockDatasource().publicAPIMock, + ...createMockDatasource('l1').publicAPIMock, getOperationForColumnId(_: string) { return { id: 'a', diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index d0ea5d9040eb2..7544ed0f87b7d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -35,7 +35,7 @@ describe('LayerContextMenu', () => { beforeEach(() => { frame = createMockFramePublicAPI(); frame.datasourceLayers = { - first: createMockDatasource().publicAPIMock, + first: createMockDatasource('test').publicAPIMock, }; }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index da49d4594c087..ddbd9d11b5fad 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -240,7 +240,6 @@ describe('xy_suggestions', () => { }); test('only makes a seriesType suggestion for unchanged table without split', () => { - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', @@ -249,7 +248,7 @@ describe('xy_suggestions', () => { accessors: ['price'], layerId: 'first', seriesType: 'bar', - splitAccessor: 'dummyCol', + splitAccessor: undefined, xAccessor: 'date', }, ], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index e6e9f1f9950ff..1dd1074978de7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -9,7 +9,6 @@ import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -import { generateId } from '../id_generator'; import { Ast } from '@kbn/interpreter/target/common'; jest.mock('../id_generator'); @@ -87,31 +86,22 @@ describe('xy_visualization', () => { describe('#initialize', () => { it('loads default state', () => { - (generateId as jest.Mock) - .mockReturnValueOnce('test-id1') - .mockReturnValueOnce('test-id2') - .mockReturnValue('test-id3'); const mockFrame = createMockFramePublicAPI(); const initialState = xyVisualization.initialize(mockFrame); expect(initialState.layers).toHaveLength(1); - expect(initialState.layers[0].xAccessor).toBeDefined(); - expect(initialState.layers[0].accessors[0]).toBeDefined(); - expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); + expect(initialState.layers[0].xAccessor).not.toBeDefined(); + expect(initialState.layers[0].accessors).toHaveLength(0); expect(initialState).toMatchInlineSnapshot(` Object { "layers": Array [ Object { - "accessors": Array [ - "test-id1", - ], + "accessors": Array [], "layerId": "", "position": "top", "seriesType": "bar_stacked", "showGridlines": false, - "splitAccessor": "test-id2", - "xAccessor": "test-id3", }, ], "legend": Object { @@ -167,14 +157,11 @@ describe('xy_visualization', () => { 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'], + accessors: [], layerId: 'first', seriesType: 'bar', - splitAccessor: 'test_empty_id', - xAccessor: 'test_empty_id', }); }); }); @@ -185,13 +172,17 @@ describe('xy_visualization', () => { }); }); + describe('#setDimension', () => {}); + + describe('#removeDimension', () => {}); + describe('#toExpression', () => { let mockDatasource: ReturnType; let frame: ReturnType; beforeEach(() => { frame = createMockFramePublicAPI(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ { columnId: 'd' }, From ef403a6116fb05c00f4472d4e9d222278b66079c Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 9 Mar 2020 17:04:12 -0400 Subject: [PATCH 08/16] Fix bug with removing dimension --- .../plugins/lens/public/_config_panel.scss | 10 + .../editor_frame/config_panel_wrapper.tsx | 19 +- .../dimension_panel/_dimension_panel.scss | 9 - .../indexpattern_datasource/indexpattern.tsx | 9 + x-pack/legacy/plugins/lens/public/types.ts | 82 ++--- .../public/xy_visualization/to_expression.ts | 41 ++- .../xy_visualization/xy_visualization.test.ts | 312 ++++++++++-------- 7 files changed, 259 insertions(+), 223 deletions(-) delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/_config_panel.scss b/x-pack/legacy/plugins/lens/public/_config_panel.scss index 5c6d25bf10818..c706893550d55 100644 --- a/x-pack/legacy/plugins/lens/public/_config_panel.scss +++ b/x-pack/legacy/plugins/lens/public/_config_panel.scss @@ -19,3 +19,13 @@ box-shadow: none !important; border: 1px dashed currentColor; } + +.lnsConfigPanel__dimension { + @include euiFontSizeS; + background-color: $euiColorEmptyShade; + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 06fbc8fa76d4c..178da1eb8915f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiButtonEmpty, EuiToolTip, - EuiText, EuiButton, EuiForm, EuiFormRow, @@ -129,7 +128,7 @@ function LayerPanels( updateDatasource={updateDatasource} frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} - onRemove={() => { + onRemoveLayer={() => { dispatch({ type: 'UPDATE_STATE', subType: 'REMOVE_OR_CLEAR_LAYER', @@ -198,11 +197,11 @@ function LayerPanel( visualizationState: unknown; updateVisualization: StateSetter; updateDatasource: (datasourceId: string, newState: unknown) => void; - onRemove: () => void; + onRemoveLayer: () => void; } ) { const dragDropContext = useContext(DragContext); - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props; + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; if (!datasourcePublicAPI) { return <>; @@ -308,7 +307,7 @@ function LayerPanel( {dimension.accessors.map(accessor => ( { trackUiEvent('indexpattern_dimension_removed'); + props.updateDatasource( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }) + ); props.updateVisualization( props.activeVisualization.removeDimension({ layerId, @@ -498,7 +505,7 @@ function LayerPanel( el.blur(); } - onRemove(); + onRemoveLayer(); }} > {isOnlyLayer diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss deleted file mode 100644 index ddb37505f9985..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss +++ /dev/null @@ -1,9 +0,0 @@ -.lnsIndexPatternDimensionPanel { - @include euiFontSizeS; - background-color: $euiColorEmptyShade; - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index db72887872aad..79ba8b839ae1b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -44,6 +44,7 @@ import { } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Plugin as DataPlugin } from '../../../../../../src/plugins/data/public'; +import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '..'; export { OperationType, IndexPatternColumn } from './operations'; @@ -165,6 +166,14 @@ export function getIndexPatternDatasource({ return Object.keys(state.layers); }, + removeColumn({ prevState, layerId, columnId }) { + return deleteColumn({ + state: prevState, + layerId, + columnId, + }); + }, + toExpression, getMetaData(state: IndexPatternPrivateState) { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index ad47795c0309e..9873997909d2f 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -14,9 +14,6 @@ import { Document } from './persistence'; import { DateRange } from '../../../../plugins/lens/common'; import { Query, Filter } from '../../../../../src/plugins/data/public'; -// eslint-disable-next-line -export interface EditorFrameOptions {} - export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { @@ -55,7 +52,7 @@ export interface EditorFrameSetup { } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => Promise; + createInstance: () => Promise; } // Hints the default nesting to the data source. 0 is the highest priority @@ -140,6 +137,7 @@ export interface Datasource { removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; + removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; @@ -162,22 +160,13 @@ export interface Datasource { */ export interface DatasourcePublicAPI { datasourceId: string; - getTableSpec: () => TableSpec; + getTableSpec: () => Array<{ columnId: string }>; getOperationForColumnId: (columnId: string) => Operation | null; // Render can be called many times - // renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; } -export interface TableSpecColumn { - // Column IDs are the keys for internal state in data sources and visualizations - columnId: string; -} - -// TableSpec is managed by visualizations -export type TableSpec = TableSpecColumn[]; - export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; @@ -188,16 +177,35 @@ export interface DatasourceDataPanelProps { filters: Filter[]; } -// The only way a visualization has to restrict the query building -export type DatasourceDimensionEditorProps = FrameDimensionPanelProps & { +export interface DatasourceDimensionProps { + layerId: string; + columnId: string; + + // Visualizations can restrict operations based on their own rules + filterOperations: (operation: OperationMetadata) => boolean; + + // Visualizations can hint at the role this dimension would play, which + // affects the default ordering of the query + suggestedPriority?: DimensionPriority; + + onRemove?: (accessor: string) => void; + + // Some dimension editors will allow users to change the operation grouping + // from the panel, and this lets the visualization hint that it doesn't want + // users to have that level of control + hideGrouping?: boolean; + state: T; +} + +// The only way a visualization has to restrict the query building +export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { setState: StateSetter; core: Pick; dateRange: DateRange; }; -export type DatasourceDimensionTriggerProps = FrameDimensionPanelProps & { - state: T; +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { dragDropContext: DragContextState; togglePopover: () => void; }; @@ -286,18 +294,6 @@ interface VisualizationDimensionConfig { required?: boolean; } -export interface VisualizationLayerConfigResult { - dimensions: VisualizationDimensionConfig[]; -} - -export interface VisualizationDimensionPanelProps { - layerId: string; - // dragDropContext: DragContextState; - frame: FramePublicAPI; - state: T; - setState: (newState: T) => void; -} - /** * Object passed to `getSuggestions` of a visualization. * It contains a possible table the current datasource could @@ -355,25 +351,6 @@ export interface VisualizationSuggestion { previewIcon: IconType; } -export interface FrameDimensionPanelProps { - layerId: string; - columnId: string; - - // Visualizations can restrict operations based on their own rules - filterOperations: (operation: OperationMetadata) => boolean; - - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query - suggestedPriority?: DimensionPriority; - - onRemove?: (accessor: string) => void; - - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control - hideGrouping?: boolean; -} - export interface FramePublicAPI { datasourceLayers: Record; @@ -381,9 +358,6 @@ export interface FramePublicAPI { query: Query; filters: Filter[]; - // Render can be called many times - // renderDimensionPanel: (domElement: Element, props: FrameDimensionPanelProps) => void; - // Adds a new layer. This has a side effect of updating the datasource state addNewLayer: () => string; removeLayers: (layerIds: string[]) => void; @@ -411,7 +385,9 @@ export interface Visualization { getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerConfigProps) => void; - getLayerOptions: (props: VisualizationLayerConfigProps) => VisualizationLayerConfigResult; + getLayerOptions: ( + props: VisualizationLayerConfigProps + ) => { dimensions: VisualizationDimensionConfig[] }; getDescription: ( state: T diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index 8e1902a646267..750ee7d5c4e21 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -36,25 +36,25 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return null; } - // const stateWithValidAccessors = { - // ...state, - // layers: state.layers.map(layer => { - // const datasource = frame.datasourceLayers[layer.layerId]; - - // const newLayer = { ...layer }; - - // if (!datasource.getOperationForColumnId(layer.splitAccessor)) { - // delete newLayer.splitAccessor; - // } - - // return { - // ...newLayer, - // accessors: layer.accessors.filter(accessor => - // Boolean(datasource.getOperationForColumnId(accessor)) - // ), - // }; - // }), - // }; + const stateWithValidAccessors = { + ...state, + layers: state.layers.map(layer => { + const datasource = frame.datasourceLayers[layer.layerId]; + + const newLayer = { ...layer }; + + if (!datasource.getOperationForColumnId(layer.splitAccessor)) { + delete newLayer.splitAccessor; + } + + return { + ...newLayer, + accessors: layer.accessors.filter(accessor => + Boolean(datasource.getOperationForColumnId(accessor)) + ), + }; + }), + }; const metadata: Record> = {}; state.layers.forEach(layer => { @@ -69,8 +69,7 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); return buildExpression( - // stateWithValidAccessors, - state, + stateWithValidAccessors, metadata, frame, xyTitles(state.layers[0], frame) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 1dd1074978de7..f7619e3b41957 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -172,9 +172,87 @@ describe('xy_visualization', () => { }); }); - describe('#setDimension', () => {}); + describe('#setDimension', () => { + it('sets the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }, + ], + }, + layerId: 'first', + dimensionId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + + it('replaces the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + dimensionId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + }); - describe('#removeDimension', () => {}); + describe('#removeDimension', () => { + it('removes the x axis', () => { + expect( + xyVisualization.removeDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + dimensionId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }); + }); + }); describe('#toExpression', () => { let mockDatasource: ReturnType; @@ -223,136 +301,102 @@ describe('xy_visualization', () => { ]); }); }); -}); -// test('the x dimension panel accepts only bucketed operations', () => { -// // TODO: this should eventually also accept raw operation -// const state = testState(); -// const component = mount( -// -// ); - -// const panel = testSubj(component, 'lnsXY_xDimensionPanel'); -// const nativeProps = (panel as NativeRendererProps).nativeProps; -// const { columnId, filterOperations } = nativeProps; -// const exampleOperation: Operation = { -// dataType: 'number', -// isBucketed: false, -// label: 'bar', -// }; -// const bucketedOps: Operation[] = [ -// { ...exampleOperation, isBucketed: true, dataType: 'number' }, -// { ...exampleOperation, isBucketed: true, dataType: 'string' }, -// { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, -// { ...exampleOperation, isBucketed: true, dataType: 'date' }, -// ]; -// const ops: Operation[] = [ -// ...bucketedOps, -// { ...exampleOperation, dataType: 'number' }, -// { ...exampleOperation, dataType: 'string' }, -// { ...exampleOperation, dataType: 'boolean' }, -// { ...exampleOperation, dataType: 'date' }, -// ]; -// expect(columnId).toEqual('shazm'); -// expect(ops.filter(filterOperations)).toEqual(bucketedOps); -// }); - -// test('the y dimension panel accepts numeric operations', () => { -// const state = testState(); -// const component = mount( -// -// ); - -// const filterOperations = component -// .find('[data-test-subj="lensXY_yDimensionPanel"]') -// .first() -// .prop('filterOperations') as (op: Operation) => boolean; - -// const exampleOperation: Operation = { -// dataType: 'number', -// isBucketed: false, -// label: 'bar', -// }; -// const ops: Operation[] = [ -// { ...exampleOperation, dataType: 'number' }, -// { ...exampleOperation, dataType: 'string' }, -// { ...exampleOperation, dataType: 'boolean' }, -// { ...exampleOperation, dataType: 'date' }, -// ]; -// expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); -// }); - -// test('allows removal of y dimensions', () => { -// const setState = jest.fn(); -// const state = testState(); -// const component = mount( -// -// ); - -// const onRemove = component -// .find('[data-test-subj="lensXY_yDimensionPanel"]') -// .first() -// .prop('onRemove') as (accessor: string) => {}; - -// onRemove('b'); - -// expect(setState).toHaveBeenCalledTimes(1); -// expect(setState.mock.calls[0][0]).toMatchObject({ -// layers: [ -// { -// ...state.layers[0], -// accessors: ['a', 'c'], -// }, -// ], -// }); -// }); - -// test('allows adding a y axis dimension', () => { -// (generateId as jest.Mock).mockReturnValueOnce('zed'); -// const setState = jest.fn(); -// const state = testState(); -// const component = mount( -// -// ); - -// const onAdd = component -// .find('[data-test-subj="lensXY_yDimensionPanel"]') -// .first() -// .prop('onAdd') as () => {}; - -// onAdd(); - -// expect(setState).toHaveBeenCalledTimes(1); -// expect(setState.mock.calls[0][0]).toMatchObject({ -// layers: [ -// { -// ...state.layers[0], -// accessors: ['a', 'b', 'c', 'zed'], -// }, -// ], -// }); -// }); + describe('#getLayerOptions', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + // mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { + // return { label: `col_${col}`, dataType: 'number' } as Operation; + // }); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + it('should return options for 3 dimensions', () => { + const options = xyVisualization.getLayerOptions({ + state: exampleState(), + frame, + layerId: 'first', + setState: jest.fn, + }).dimensions; + expect(options).toHaveLength(3); + expect(options.map(o => o.dimensionId)).toEqual(['x', 'y', 'breakdown']); + }); + + it('should only accept bucketed operations for x', () => { + const options = xyVisualization.getLayerOptions({ + state: exampleState(), + frame, + layerId: 'first', + setState: jest.fn, + }).dimensions; + const filterOperations = options.find(o => o.dimensionId === 'x')!.filterOperations; + + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const bucketedOps: Operation[] = [ + { ...exampleOperation, isBucketed: true, dataType: 'number' }, + { ...exampleOperation, isBucketed: true, dataType: 'string' }, + { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, + { ...exampleOperation, isBucketed: true, dataType: 'date' }, + ]; + const ops: Operation[] = [ + ...bucketedOps, + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations)).toEqual(bucketedOps); + }); + + it('should not allow anything to be added to x', () => { + const options = xyVisualization.getLayerOptions({ + state: exampleState(), + frame, + layerId: 'first', + setState: jest.fn, + }).dimensions; + expect(options.find(o => o.dimensionId === 'x')?.supportsMoreColumns).toBe(false); + }); + + it('should allow number operations on y', () => { + const options = xyVisualization.getLayerOptions({ + state: exampleState(), + frame, + layerId: 'first', + setState: jest.fn, + }).dimensions; + const filterOperations = options.find(o => o.dimensionId === 'y')!.filterOperations; + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const ops: Operation[] = [ + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); + }); + }); +}); From a2d5e148c44b0c434a3484418fb24df27f25b57a Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 10 Mar 2020 10:36:16 -0400 Subject: [PATCH 09/16] Update tests --- .../editor_frame/save.test.ts | 4 +- .../editor_frame/suggestion_helpers.test.ts | 8 +- .../editor_frame/suggestion_panel.test.tsx | 2 +- .../editor_frame/workspace_panel.test.tsx | 4 +- .../public/editor_frame_service/mocks.tsx | 17 +-- .../editor_frame_service/service.test.tsx | 4 +- x-pack/legacy/plugins/lens/public/plugin.tsx | 2 +- ...est.ts.snap => to_expression.test.ts.snap} | 2 +- .../xy_visualization/to_expression.test.ts | 133 ++++++++++++++++++ .../public/xy_visualization/to_expression.ts | 31 +--- .../public/xy_visualization/xy_expression.tsx | 2 +- .../xy_visualization/xy_visualization.test.ts | 53 ------- 12 files changed, 159 insertions(+), 103 deletions(-) rename x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/{xy_visualization.test.ts.snap => to_expression.test.ts.snap} (96%) create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index 158a6cb8c979a..60bfbc493f61c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -11,7 +11,7 @@ import { esFilters, IIndexPattern, IFieldType } from '../../../../../../../src/p describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); mockVisualization.getPersistableState.mockImplementation(x => x); - const mockDatasource = createMockDatasource(); + const mockDatasource = createMockDatasource('a'); const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern; const mockField = ({ name: '@timestamp' } as unknown) as IFieldType; @@ -45,7 +45,7 @@ describe('save editor frame state', () => { }; it('transforms from internal state to persisted doc format', async () => { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('a'); datasource.getPersistableState.mockImplementation(state => ({ stuff: `${state}_datasource_persisted`, })); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 487a91c22b5d5..63b8b1f048296 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -30,7 +30,7 @@ let datasourceStates: Record< beforeEach(() => { datasourceMap = { - mock: createMockDatasource(), + mock: createMockDatasource('a'), }; datasourceStates = { @@ -147,9 +147,9 @@ describe('suggestion helpers', () => { }, }; const multiDatasourceMap = { - mock: createMockDatasource(), - mock2: createMockDatasource(), - mock3: createMockDatasource(), + mock: createMockDatasource('a'), + mock2: createMockDatasource('a'), + mock3: createMockDatasource('a'), }; const droppedField = {}; getSuggestions({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 9729d6259f84a..b146f2467c46c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -39,7 +39,7 @@ describe('suggestion_panel', () => { beforeEach(() => { mockVisualization = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); dispatchMock = jest.fn(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 92a14963ff0b6..201f13d421994 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -41,7 +41,7 @@ describe('workspace_panel', () => { mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); }); @@ -204,7 +204,7 @@ describe('workspace_panel', () => { }); it('should include data fetching for each layer in the expression', () => { - const mockDatasource2 = createMockDatasource(); + const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index 3981128cbc889..da8bda166f1df 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -35,14 +35,14 @@ export function createMockVisualization(): jest.Mocked { initialize: jest.fn((_frame, _state?) => ({})), getLayerOptions: jest.fn(() => ({ dimensions: [ - // { - // layerId: 'layer1', - // dimensionId: 'a', - // dimensionLabel: 'a', - // supportsMoreColumns: true, - // accessors: [], - // filterOperations: jest.fn(() => true), - // }, + { + layerId: 'layer1', + dimensionId: 'a', + dimensionLabel: 'a', + supportsMoreColumns: true, + accessors: [], + filterOperations: jest.fn(() => true), + }, ], })), toExpression: jest.fn((_state, _frame) => null), @@ -77,6 +77,7 @@ export function createMockDatasource(id: string): DatasourceMock { toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => {}), removeLayer: jest.fn((_state, _layerId) => {}), + removeColumn: jest.fn(props => {}), getLayers: jest.fn(_state => []), getMetaData: jest.fn(_state => ({ filterableIndexPatterns: [] })), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 2e1645c816140..b1c0b3c8b4c2c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -43,7 +43,7 @@ describe('editor_frame service', () => { (async () => { pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), @@ -59,7 +59,7 @@ describe('editor_frame service', () => { it('should not have child nodes after unmount', async () => { pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 7afe6d7abedc0..75b969ab028e6 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -114,7 +114,7 @@ export class LensPlugin { const savedObjectsClient = coreStart.savedObjects.client; addHelpMenuToAppChrome(coreStart.chrome); - const instance = await this.createEditorFrame!({}); + const instance = await this.createEditorFrame!(); setReportManager( new LensReportManager({ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap similarity index 96% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 76af8328673ad..6b68679bfd4ec 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`xy_visualization #toExpression should map to a valid AST 1`] = ` +exports[`#toExpression should map to a valid AST 1`] = ` Object { "chain": Array [ Object { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts new file mode 100644 index 0000000000000..6bc379ea33bca --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { Ast } from '@kbn/interpreter/target/common'; +import { Position } from '@elastic/charts'; +import { xyVisualization } from './xy_visualization'; +import { Operation } from '../types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; + +describe('#toExpression', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { + return { label: `col_${col}`, dataType: 'number' } as Operation; + }); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + it('should map to a valid AST', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + ) + ).toMatchSnapshot(); + }); + + it('should not generate an expression when missing x', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should not generate an expression when missing y', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: 'a', + accessors: [], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should default to labeling all columns with their column label', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + )! as Ast; + + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); + expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); + expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); + expect( + (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel + ).toEqual([ + JSON.stringify({ + b: 'col_b', + c: 'col_c', + d: 'col_d', + }), + ]); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index 750ee7d5c4e21..23ab3090bbe85 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -22,8 +22,8 @@ function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { if (!datasource) { return defaults; } - const x = datasource.getOperationForColumnId(layer.xAccessor); - const y = datasource.getOperationForColumnId(layer.accessors[0]); + const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null; + const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null; return { xTitle: x ? x.label : defaults.xTitle, @@ -36,26 +36,6 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return null; } - const stateWithValidAccessors = { - ...state, - layers: state.layers.map(layer => { - const datasource = frame.datasourceLayers[layer.layerId]; - - const newLayer = { ...layer }; - - if (!datasource.getOperationForColumnId(layer.splitAccessor)) { - delete newLayer.splitAccessor; - } - - return { - ...newLayer, - accessors: layer.accessors.filter(accessor => - Boolean(datasource.getOperationForColumnId(accessor)) - ), - }; - }), - }; - const metadata: Record> = {}; state.layers.forEach(layer => { metadata[layer.layerId] = {}; @@ -68,12 +48,7 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); }); - return buildExpression( - stateWithValidAccessors, - metadata, - frame, - xyTitles(state.layers[0], frame) - ); + return buildExpression(state, metadata, frame, xyTitles(state.layers[0], frame)); }; export function toPreviewExpression(state: State, frame: FramePublicAPI) { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index 80fef47ed7a14..15aaf289eebf9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -248,7 +248,7 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC } const columnToLabelMap = columnToLabel ? JSON.parse(columnToLabel) : {}; - const splitAccessorLabel = columnToLabelMap[splitAccessor]; + const splitAccessorLabel = splitAccessor ? columnToLabelMap[splitAccessor] : ''; const yAccessors = accessors.map(accessor => columnToLabelMap[accessor] || accessor); const idForLegend = splitAccessorLabel || yAccessors; const sanitized = sanitizeRows({ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index f7619e3b41957..57d9be0e866da 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -9,7 +9,6 @@ import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -import { Ast } from '@kbn/interpreter/target/common'; jest.mock('../id_generator'); @@ -254,54 +253,6 @@ describe('xy_visualization', () => { }); }); - describe('#toExpression', () => { - let mockDatasource: ReturnType; - let frame: ReturnType; - - beforeEach(() => { - frame = createMockFramePublicAPI(); - mockDatasource = createMockDatasource('testDatasource'); - - mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ - { columnId: 'd' }, - { columnId: 'a' }, - { columnId: 'b' }, - { columnId: 'c' }, - ]); - - mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { - return { label: `col_${col}`, dataType: 'number' } as Operation; - }); - - frame.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - }); - - it('should map to a valid AST', () => { - expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); - }); - - it('should default to labeling all columns with their column label', () => { - const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; - - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); - expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); - expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); - expect( - (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel - ).toEqual([ - JSON.stringify({ - b: 'col_b', - c: 'col_c', - d: 'col_d', - }), - ]); - }); - }); - describe('#getLayerOptions', () => { let mockDatasource: ReturnType; let frame: ReturnType; @@ -317,10 +268,6 @@ describe('xy_visualization', () => { { columnId: 'c' }, ]); - // mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { - // return { label: `col_${col}`, dataType: 'number' } as Operation; - // }); - frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; From 6365ab6c855e624e7df8c3369d8bed19dc94b394 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 10 Mar 2020 12:51:19 -0400 Subject: [PATCH 10/16] Fix frame tests --- .../editor_frame/editor_frame.test.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 404b03ce88993..9d9ede4e98f76 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -1382,7 +1382,10 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="indexPattern-dropTarget"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); @@ -1477,7 +1480,10 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="lnsWorkspace"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); From 8eb8fac68e3a01abd464595e408e6ce7eca50dfe Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 10 Mar 2020 16:28:40 -0400 Subject: [PATCH 11/16] Fix all tests I could find --- .../editor_frame/config_panel_wrapper.tsx | 43 ++++++++++++++---- .../editor_frame/editor_frame.test.tsx | 2 +- .../public/editor_frame_service/mocks.tsx | 3 +- .../dimension_panel/dimension_panel.tsx | 39 +++++++++++----- .../dimension_panel/popover_editor.tsx | 45 +------------------ x-pack/legacy/plugins/lens/public/types.ts | 2 + .../public/xy_visualization/to_expression.ts | 4 +- .../xy_visualization/xy_visualization.tsx | 3 ++ .../test/functional/apps/lens/smokescreen.ts | 6 +-- 9 files changed, 77 insertions(+), 70 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 178da1eb8915f..85eec9f2a753f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -158,7 +158,7 @@ function LayerPanels( className="lnsConfigPanel__addLayerBtn" fullWidth size="s" - data-test-subj={`lnsXY_layer_add`} + data-test-subj="lnsXY_layer_add" aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { defaultMessage: 'Add layer', })} @@ -236,20 +236,32 @@ function LayerPanel( const [popoverState, setPopoverState] = useState<{ isOpen: boolean; openId: string | null; + addingToDimensionId: string | null; }>({ isOpen: false, openId: null, + addingToDimensionId: null, }); const { dimensions } = activeVisualization.getLayerOptions(layerVisualizationConfigProps); const isEmptyLayer = !dimensions.some(d => d.accessors.length > 0); - function wrapInPopover(id: string, trigger: React.ReactElement, panel: React.ReactElement) { + function wrapInPopover( + id: string, + dimensionId: string, + trigger: React.ReactElement, + panel: React.ReactElement + ) { + const noMatch = popoverState.isOpen ? !dimensions.some(d => d.accessors.includes(id)) : false; return ( { - setPopoverState({ isOpen: false, openId: null }); + setPopoverState({ isOpen: false, openId: null, addingToDimensionId: null }); }} button={trigger} display="block" @@ -308,7 +320,7 @@ function LayerPanel( {wrapInPopover( accessor, + dimension.dimensionId, { if (popoverState.isOpen) { setPopoverState({ isOpen: false, openId: null, + addingToDimensionId: null, }); } else { setPopoverState({ isOpen: true, openId: accessor, + addingToDimensionId: null, // not set for existing dimension }); } }, @@ -397,7 +413,7 @@ function LayerPanel( {dimension.supportsMoreColumns ? ( {wrapInPopover( - dimension.dimensionLabel, + newId, + dimension.dimensionId, { props.updateVisualization( @@ -474,6 +494,11 @@ function LayerPanel( }) ); props.updateDatasource(datasourceId, newState); + setPopoverState({ + isOpen: true, + openId: newId, + addingToDimensionId: null, // clear now that dimension exists + }); }, }} /> diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 9d9ede4e98f76..1e3879d5bf749 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -1384,7 +1384,7 @@ describe('editor_frame', () => { act(() => { instance .find(DragDrop) - .filter('[data-test-subj="indexPattern-dropTarget"]') + .filter('[data-test-subj="mockVisA"]') .prop('onDrop')!({ indexPatternId: '1', field: {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index da8bda166f1df..30999bd44c247 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -33,7 +33,7 @@ export function createMockVisualization(): jest.Mocked { getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_frame, _state?) => ({})), - getLayerOptions: jest.fn(() => ({ + getLayerOptions: jest.fn(props => ({ dimensions: [ { layerId: 'layer1', @@ -42,6 +42,7 @@ export function createMockVisualization(): jest.Mocked { supportsMoreColumns: true, accessors: [], filterOperations: jest.fn(() => true), + dataTestSubj: 'mockVisA', }, ], })), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 329fb47722f18..702320baa8f47 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -6,6 +6,8 @@ import _ from 'lodash'; import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -17,7 +19,7 @@ import { import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; -import { PopoverEditor, PopoverTrigger } from './popover_editor'; +import { PopoverEditor } from './popover_editor'; import { changeColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; @@ -179,20 +181,37 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens props: IndexPatternDimensionTriggerProps ) { const layerId = props.layerId; - const currentIndexPattern = - props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; - const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); const selectedColumn: IndexPatternColumn | null = props.state.layers[layerId].columns[props.columnId] || null; + const { columnId, uniqueLabel } = props; return ( - +
+ {selectedColumn ? ( + { + props.togglePopover(); + }} + data-test-subj="lns-dimensionTrigger" + aria-label={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + title={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + > + {uniqueLabel} + + ) : null} +
); }; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 8d240a4fa0243..6769841946a7c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -14,16 +14,11 @@ import { EuiCallOut, EuiFormRow, EuiFieldText, - EuiLink, EuiSpacer, } from '@elastic/eui'; import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { - IndexPatternDimensionTriggerProps, - IndexPatternDimensionEditorProps, - OperationFieldSupportMatrix, -} from './dimension_panel'; +import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -40,12 +35,6 @@ import { FormatSelector } from './format_selector'; const operationPanels = getOperationDisplay(); -export interface PopoverTriggerProps extends IndexPatternDimensionTriggerProps { - selectedColumn?: IndexPatternColumn; - operationFieldSupportMatrix: OperationFieldSupportMatrix; - currentIndexPattern: IndexPattern; -} - export interface PopoverEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; operationFieldSupportMatrix: OperationFieldSupportMatrix; @@ -380,38 +369,6 @@ export function PopoverEditor(props: PopoverEditorProps) { - ) - - ); -} - -export function PopoverTrigger(props: PopoverTriggerProps) { - const { selectedColumn, columnId, uniqueLabel } = props; - return ( -
- {selectedColumn ? ( - { - props.togglePopover(); - }} - data-test-subj="indexPattern-configure-dimension" - aria-label={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - title={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - > - {uniqueLabel} - - ) : null}
); } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 9873997909d2f..8d12a82228bfe 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -292,6 +292,8 @@ interface VisualizationDimensionConfig { hideGrouping?: boolean; required?: boolean; + + dataTestSubj?: string; } /** diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index 23ab3090bbe85..21f7c662098dc 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -164,7 +164,7 @@ export const buildExpression = ( hide: [Boolean(layer.hide)], - xAccessor: [layer.xAccessor], + xAccessor: [layer.xAccessor!], yScaleType: [ getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), ], @@ -172,7 +172,7 @@ export const buildExpression = ( getScaleType(metadata[layer.layerId][layer.xAccessor!], ScaleType.Linear), ], isHistogram: [isHistogramDimension], - splitAccessor: [layer.splitAccessor], + splitAccessor: [layer.splitAccessor!], seriesType: [layer.seriesType], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index b1ba1fa4e826c..a7587e331b788 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -160,6 +160,7 @@ export const xyVisualization: Visualization = { suggestedPriority: 1, supportsMoreColumns: !layer.xAccessor, required: true, + dataTestSubj: 'lnsXY_xDimensionPanel', }, { dimensionId: 'y', @@ -170,6 +171,7 @@ export const xyVisualization: Visualization = { filterOperations: isNumericMetric, supportsMoreColumns: true, required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', }, { dimensionId: 'breakdown', @@ -180,6 +182,7 @@ export const xyVisualization: Visualization = { filterOperations: isBucketed, suggestedPriority: 0, supportsMoreColumns: !layer.splitAccessor, + dataTestSubj: 'lnsXY_splitDimensionPanel', }, ], }; diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 3346f2ff77036..317bb0b27e972 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -77,21 +77,21 @@ export default function({ getService, getPageObjects, ...rest }: FtrProviderCont await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'date_histogram', field: '@timestamp', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'avg', field: 'bytes', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'terms', field: 'ip', }); From 5877a7f0a74e71343aa8132e487c957ac4888971 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 11 Mar 2020 15:09:23 -0400 Subject: [PATCH 12/16] Remove debugger --- .../legacy/plugins/lens/public/indexpattern_datasource/loader.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index fa6866f9238d4..ed3d8a91b366d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -134,7 +134,6 @@ export async function changeIndexPattern({ patterns: [id], }); - debugger; setState(s => ({ ...s, layers: isSingleEmptyLayer(state.layers) From 1160987ba03f048cc845baf81e0cd554823384ee Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 11 Mar 2020 17:19:57 -0400 Subject: [PATCH 13/16] Style config panels --- .../editor_frame/_config_panel_wrapper.scss} | 22 +++++- .../editor_frame/config_panel_wrapper.tsx | 71 ++++++++++--------- .../editor_frame/editor_frame.test.tsx | 4 +- .../editor_frame/index.scss | 1 + x-pack/legacy/plugins/lens/public/index.scss | 2 - .../dimension_panel/_index.scss | 1 - .../dimension_panel/_popover.scss | 29 +++----- .../dimension_panel/dimension_panel.tsx | 40 +++++------ .../dimension_panel/popover_editor.tsx | 12 ++-- 9 files changed, 90 insertions(+), 92 deletions(-) rename x-pack/legacy/plugins/lens/public/{_config_panel.scss => editor_frame_service/editor_frame/_config_panel_wrapper.scss} (62%) diff --git a/x-pack/legacy/plugins/lens/public/_config_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss similarity index 62% rename from x-pack/legacy/plugins/lens/public/_config_panel.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss index c706893550d55..1e28e11baeb3a 100644 --- a/x-pack/legacy/plugins/lens/public/_config_panel.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss @@ -2,7 +2,7 @@ margin-bottom: $euiSizeS; } -.lnsConfigPanel__axis { +.lnsConfigPanel__row { background: $euiColorLightestShade; padding: $euiSizeS; border-radius: $euiBorderRadius; @@ -22,10 +22,28 @@ .lnsConfigPanel__dimension { @include euiFontSizeS; - background-color: $euiColorEmptyShade; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); border-radius: $euiBorderRadius; display: flex; align-items: center; margin-top: $euiSizeXS; overflow: hidden; } + +.lnsConfigPanel__trigger { + max-width: 100%; + display: block; +} + +.lnsConfigPanel__triggerLink { + padding: $euiSizeS; + width: 100%; + display: flex; + align-items: center; + min-height: $euiSizeXXL; +} + +.lnsConfigPanel__popover { + line-height: 0; + flex-grow: 1; +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 85eec9f2a753f..5cba4517fedbb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -255,6 +255,8 @@ function LayerPanel( const noMatch = popoverState.isOpen ? !dimensions.some(d => d.accessors.includes(id)) : false; return ( { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToDimensionId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: newId, - addingToDimensionId: dimension.dimensionId, - }); - } - }} - size="xs" - > - -
, +
+ { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToDimensionId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToDimensionId: dimension.dimensionId, + }); + } + }} + size="xs" + > + + +
, { ExpressionRenderer={expressionRendererMock} /> ); - expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled(); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); }); - expect(mockVisualization.getLayerOptions).not.toHaveBeenCalled(); - expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); + expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); }); it('should not initialize visualization before datasource is initialized', async () => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss index fee28c374ef7e..6c6a63c8c7eb6 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,4 +1,5 @@ @import './chart_switch'; +@import './config_panel_wrapper'; @import './data_panel_wrapper'; @import './expression_renderer'; @import './frame_layout'; diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 496573f6a1c9a..2f91d14c397c7 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -4,8 +4,6 @@ @import './variables'; @import './mixins'; -@import './config_panel'; - @import './app_plugin/index'; @import 'datatable_visualization/index'; @import './drag_drop/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss index 2ce3e11171fc9..26f805fe735f0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss @@ -1,3 +1,2 @@ -@import './dimension_panel'; @import './field_select'; @import './popover'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss index 8f26ab91e0f16..07a72ee1f66fc 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss @@ -1,37 +1,24 @@ -.lnsPopoverEditor { +.lnsIndexPatternDimensionEditor { flex-grow: 1; line-height: 0; overflow: hidden; } -.lnsPopoverEditor__anchor { - max-width: 100%; - display: block; -} - -.lnsPopoverEditor__link { - width: 100%; - display: flex; - align-items: center; - padding: $euiSizeS; - min-height: $euiSizeXXL; -} - -.lnsPopoverEditor__left, -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__left, +.lnsIndexPatternDimensionEditor__right { padding: $euiSizeS; } -.lnsPopoverEditor__left { +.lnsIndexPatternDimensionEditor__left { padding-top: 0; background-color: $euiPageBackgroundColor; } -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__right { width: $euiSize * 20; } -.lnsPopoverEditor__operation { +.lnsIndexPatternDimensionEditor__operation { @include euiFontSizeS; color: $euiColorPrimary; @@ -41,11 +28,11 @@ } } -.lnsPopoverEditor__operation--selected { +.lnsIndexPatternDimensionEditor__operation--selected { font-weight: bold; color: $euiTextColor; } -.lnsPopoverEditor__operation--incompatible { +.lnsIndexPatternDimensionEditor__operation--incompatible { color: $euiColorMediumShade; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 702320baa8f47..9bf50fd6b13b9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -186,32 +186,26 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens props.state.layers[layerId].columns[props.columnId] || null; const { columnId, uniqueLabel } = props; + if (!selectedColumn) { + return null; + } return ( -
{ + props.togglePopover(); + }} + data-test-subj="lns-dimensionTrigger" + aria-label={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + title={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} > - {selectedColumn ? ( - { - props.togglePopover(); - }} - data-test-subj="lns-dimensionTrigger" - aria-label={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - title={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - > - {uniqueLabel} - - ) : null} -
+ {uniqueLabel} + ); }; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 6769841946a7c..e26c338b6e240 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -109,14 +109,14 @@ export function PopoverEditor(props: PopoverEditorProps) { items: getOperationTypes().map(({ operationType, compatibleWithCurrentField }) => ({ name: operationPanels[operationType].displayName, id: operationType as string, - className: classNames('lnsPopoverEditor__operation', { - 'lnsPopoverEditor__operation--selected': Boolean( + className: classNames('lnsIndexPatternDimensionEditor__operation', { + 'lnsIndexPatternDimensionEditor__operation--selected': Boolean( incompatibleSelectedOperationType === operationType || (!incompatibleSelectedOperationType && selectedColumn && selectedColumn.operationType === operationType) ), - 'lnsPopoverEditor__operation--incompatible': !compatibleWithCurrentField, + 'lnsIndexPatternDimensionEditor__operation--incompatible': !compatibleWithCurrentField, }), 'data-test-subj': `lns-indexPatternDimension${ compatibleWithCurrentField ? '' : 'Incompatible' @@ -182,7 +182,7 @@ export function PopoverEditor(props: PopoverEditorProps) { } return ( -
+
- + - + {incompatibleSelectedOperationType && selectedColumn && ( Date: Wed, 11 Mar 2020 18:33:17 -0400 Subject: [PATCH 14/16] Update i18n --- .../lens/public/metric_visualization/metric_visualization.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx index 11f2aa7e8d248..8da335ead6c06 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -90,7 +90,7 @@ export const metricVisualization: Visualization = { return { dimensions: [ { - dimensionId: '', + dimensionId: 'metric', dimensionLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), layerId: props.state.layerId, accessors: props.state.accessor ? [props.state.accessor] : [], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e1627bc273db9..09df20f326baf 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6835,7 +6835,6 @@ "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示", "xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション", "xpack.lens.metric.label": "メトリック", - "xpack.lens.metric.valueLabel": "値", "xpack.lens.sugegstion.refreshSuggestionLabel": "更新", "xpack.lens.suggestion.refreshSuggestionTooltip": "選択したビジュアライゼーションに基づいて、候補を更新します。", "xpack.lens.suggestions.currentVisLabel": "現在", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 65f67d5be5e56..d4f9e7d583f07 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6835,7 +6835,6 @@ "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}", "xpack.lens.lensSavedObjectLabel": "Lens 可视化", "xpack.lens.metric.label": "指标", - "xpack.lens.metric.valueLabel": "值", "xpack.lens.sugegstion.refreshSuggestionLabel": "刷新", "xpack.lens.suggestion.refreshSuggestionTooltip": "基于选定可视化刷新建议。", "xpack.lens.suggestions.currentVisLabel": "当前", From d3077cdd263ba7be3121e2dac2dd0dbb8333f685 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 11 Mar 2020 19:06:26 -0400 Subject: [PATCH 15/16] Fix dashboard test --- .../apps/dashboard_mode/dashboard_empty_screen.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index c90a0ae6d19fc..19eebb3ba501c 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -34,21 +34,21 @@ export default function({ getPageObjects, getService }) { await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_xDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'date_histogram', field: '@timestamp', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_yDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'avg', field: 'bytes', }); await PageObjects.lens.configureDimension({ dimension: - '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="indexPattern-configure-dimension"]', + '[data-test-subj="lnsXY_splitDimensionPanel"] [data-test-subj="lns-empty-dimension"]', operation: 'terms', field: 'ip', }); From 07531df5d0e284481eda8e3297cac4e73d0c51db Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 16 Mar 2020 15:19:24 -0400 Subject: [PATCH 16/16] Fix bug when switching index patterns --- .../visualization.test.tsx | 24 +-- .../datatable_visualization/visualization.tsx | 18 +- .../editor_frame/_config_panel_wrapper.scss | 1 + .../editor_frame/config_panel_wrapper.tsx | 161 ++++++++++++------ .../editor_frame/editor_frame.test.tsx | 75 ++------ .../editor_frame/editor_frame.tsx | 12 +- .../editor_frame/suggestion_panel.tsx | 1 - .../public/editor_frame_service/mocks.tsx | 10 +- .../dimension_panel/dimension_panel.tsx | 14 +- .../indexpattern.test.ts | 1 - .../indexpattern_datasource/indexpattern.tsx | 47 ++--- .../layerpanel.test.tsx | 1 + .../indexpattern_datasource/layerpanel.tsx | 3 +- .../metric_visualization.test.ts | 3 +- .../metric_visualization.tsx | 8 +- .../lens/public/metric_visualization/types.ts | 2 +- x-pack/legacy/plugins/lens/public/types.ts | 112 ++++++------ .../public/xy_visualization/to_expression.ts | 16 +- .../xy_visualization/xy_config_panel.tsx | 4 +- .../public/xy_visualization/xy_suggestions.ts | 2 +- .../xy_visualization/xy_visualization.test.ts | 39 ++--- .../xy_visualization/xy_visualization.tsx | 36 ++-- 22 files changed, 289 insertions(+), 301 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index 28bfae29976bb..e18190b6c2d69 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -203,39 +203,35 @@ describe('Datatable Visualization', () => { }); }); - describe('#getLayerOptions', () => { + describe('#getConfiguration', () => { it('returns a single layer option', () => { - const setState = jest.fn(); const datasource = createMockDatasource('test'); const frame = mockFrame(); frame.datasourceLayers = { first: datasource.publicAPIMock }; expect( - datatableVisualization.getLayerOptions({ + datatableVisualization.getConfiguration({ layerId: 'first', state: { layers: [{ layerId: 'first', columns: [] }], }, - setState, frame, - }).dimensions + }).groups ).toHaveLength(1); }); it('allows all kinds of operations', () => { - const setState = jest.fn(); const datasource = createMockDatasource('test'); const frame = mockFrame(); frame.datasourceLayers = { first: datasource.publicAPIMock }; - const filterOperations = datatableVisualization.getLayerOptions({ + const filterOperations = datatableVisualization.getConfiguration({ layerId: 'first', state: { layers: [{ layerId: 'first', columns: [] }], }, - setState, frame, - }).dimensions[0].filterOperations; + }).groups[0].filterOperations; const baseOperation: Operation = { dataType: 'string', @@ -260,12 +256,11 @@ describe('Datatable Visualization', () => { datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); expect( - datatableVisualization.getLayerOptions({ + datatableVisualization.getConfiguration({ layerId: 'a', state: { layers: [layer] }, - setState: jest.fn(), frame, - }).dimensions[0].accessors + }).groups[0].accessors ).toEqual(['c', 'b']); }); }); @@ -278,7 +273,6 @@ describe('Datatable Visualization', () => { prevState: { layers: [layer] }, layerId: 'layer1', columnId: 'b', - dimensionId: '', }) ).toEqual({ layers: [ @@ -299,7 +293,7 @@ describe('Datatable Visualization', () => { prevState: { layers: [layer] }, layerId: 'layer1', columnId: 'd', - dimensionId: '', + groupId: '', }) ).toEqual({ layers: [ @@ -318,7 +312,7 @@ describe('Datatable Visualization', () => { prevState: { layers: [layer] }, layerId: 'layer1', columnId: 'b', - dimensionId: '', + groupId: '', }) ).toEqual({ layers: [ diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 1f066e6f5103a..4248d722d5540 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -128,10 +128,10 @@ export const datatableVisualization: Visualization< ]; }, - getLayerOptions({ state, frame, layerId }) { + getConfiguration({ state, frame, layerId }) { const layer = state.layers.find(l => l.layerId === layerId); if (!layer) { - return { dimensions: [] }; + return { groups: [] }; } const datasource = frame.datasourceLayers[layer.layerId]; @@ -140,14 +140,14 @@ export const datatableVisualization: Visualization< const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); return { - dimensions: [ + groups: [ { - layerId: state.layers[0].layerId, - accessors: sortedColumns, - dimensionId: 'columns', - dimensionLabel: i18n.translate('xpack.lens.datatable.columns', { + groupId: 'columns', + groupLabel: i18n.translate('xpack.lens.datatable.columns', { defaultMessage: 'Columns', }), + layerId: state.layers[0].layerId, + accessors: sortedColumns, supportsMoreColumns: true, filterOperations: () => true, }, @@ -155,7 +155,7 @@ export const datatableVisualization: Visualization< }; }, - setDimension({ layerId, columnId, prevState }) { + setDimension({ prevState, layerId, columnId }) { return { ...prevState, layers: prevState.layers.map(l => { @@ -166,7 +166,7 @@ export const datatableVisualization: Visualization< }), }; }, - removeDimension({ prevState, columnId, layerId }) { + removeDimension({ prevState, layerId, columnId }) { return { ...prevState, layers: prevState.layers.map(l => diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss index 1e28e11baeb3a..62a7f6b023f31 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss @@ -15,6 +15,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; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 5cba4517fedbb..c2cd0485de67e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -26,7 +26,7 @@ import { Visualization, FramePublicAPI, Datasource, - VisualizationLayerConfigProps, + VisualizationLayerWidgetProps, DatasourceDimensionEditorProps, StateSetter, } from '../../types'; @@ -113,6 +113,32 @@ function LayerPanels( }, [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: { + activeId: activeVisualization.id, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); const layerIds = activeVisualization.getLayerIds(visualizationState); return ( @@ -126,6 +152,7 @@ function LayerPanels( visualizationState={visualizationState} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} + updateAll={updateAll} frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} onRemoveLayer={() => { @@ -197,6 +224,11 @@ function LayerPanel( visualizationState: unknown; updateVisualization: StateSetter; updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; onRemoveLayer: () => void; } ) { @@ -204,13 +236,12 @@ function LayerPanel( const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; if (!datasourcePublicAPI) { - return <>; + return null; } const layerVisualizationConfigProps = { layerId, dragDropContext, state: props.visualizationState, - setState: props.updateVisualization, frame: props.framePublicAPI, dateRange: props.framePublicAPI.dateRange, }; @@ -236,34 +267,33 @@ function LayerPanel( const [popoverState, setPopoverState] = useState<{ isOpen: boolean; openId: string | null; - addingToDimensionId: string | null; + addingToGroupId: string | null; }>({ isOpen: false, openId: null, - addingToDimensionId: null, + addingToGroupId: null, }); - const { dimensions } = activeVisualization.getLayerOptions(layerVisualizationConfigProps); - const isEmptyLayer = !dimensions.some(d => d.accessors.length > 0); + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); function wrapInPopover( id: string, - dimensionId: string, + groupId: string, trigger: React.ReactElement, panel: React.ReactElement ) { - const noMatch = popoverState.isOpen ? !dimensions.some(d => d.accessors.includes(id)) : false; + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; return ( { - setPopoverState({ isOpen: false, openId: null, addingToDimensionId: null }); + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); }} button={trigger} anchorPosition="leftUp" @@ -282,16 +312,49 @@ function LayerPanel( - {datasourcePublicAPI && ( + {layerDatasource && ( { + 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); + }, + }} /> )} @@ -299,13 +362,13 @@ function LayerPanel( - {dimensions.map((dimension, index) => { + {groups.map((group, index) => { const newId = generateId(); - const isMissing = !isEmptyLayer && dimension.required && dimension.accessors.length === 0; + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; return ( <> - {dimension.accessors.map(accessor => ( + {group.accessors.map(accessor => ( { @@ -335,32 +398,32 @@ function LayerPanel( ...layerDatasourceDropProps, droppedItem, columnId: accessor, - filterOperations: dimension.filterOperations, + filterOperations: group.filterOperations, }); }} > {wrapInPopover( accessor, - dimension.dimensionId, + group.groupId, { if (popoverState.isOpen) { setPopoverState({ isOpen: false, openId: null, - addingToDimensionId: null, + addingToGroupId: null, }); } else { setPopoverState({ isOpen: true, openId: accessor, - addingToDimensionId: null, // not set for existing dimension + addingToGroupId: null, // not set for existing dimension }); } }, @@ -372,7 +435,7 @@ function LayerPanel( ...layerDatasourceConfigProps, core: props.core, columnId: accessor, - filterOperations: dimension.filterOperations, + filterOperations: group.filterOperations, }} /> )} @@ -391,18 +454,15 @@ function LayerPanel( })} onClick={() => { trackUiEvent('indexpattern_dimension_removed'); - props.updateDatasource( + props.updateAll( datasourceId, layerDatasource.removeColumn({ layerId, columnId: accessor, prevState: layerDatasourceState, - }) - ); - props.updateVisualization( + }), props.activeVisualization.removeDimension({ layerId, - dimensionId: dimension.dimensionId, columnId: accessor, prevState: props.visualizationState, }) @@ -411,16 +471,16 @@ function LayerPanel( /> ))} - {dimension.supportsMoreColumns ? ( + {group.supportsMoreColumns ? ( { @@ -428,13 +488,13 @@ function LayerPanel( ...layerDatasourceDropProps, droppedItem, columnId: newId, - filterOperations: dimension.filterOperations, + filterOperations: group.filterOperations, }); if (dropSuccess) { props.updateVisualization( activeVisualization.setDimension({ layerId, - dimensionId: dimension.dimensionId, + groupId: group.groupId, columnId: newId, prevState: props.visualizationState, }) @@ -444,7 +504,7 @@ function LayerPanel( > {wrapInPopover( newId, - dimension.dimensionId, + group.groupId,
{ - props.updateVisualization( + props.updateAll( + datasourceId, + newState, activeVisualization.setDimension({ layerId, - dimensionId: dimension.dimensionId, + groupId: group.groupId, columnId: newId, prevState: props.visualizationState, }) ); - props.updateDatasource(datasourceId, newState); setPopoverState({ isOpen: true, openId: newId, - addingToDimensionId: null, // clear now that dimension exists + addingToGroupId: null, // clear now that dimension exists }); }, }} @@ -529,7 +590,7 @@ function LayerPanel( // 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) { + if (el?.blur) { el.blur(); } @@ -558,7 +619,7 @@ function LayerSettings({ }: { layerId: string; activeVisualization: Visualization; - layerConfigProps: VisualizationLayerConfigProps; + layerConfigProps: VisualizationLayerWidgetProps; }) { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index e75c5f4137b13..8d8d38944e18a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -376,7 +376,7 @@ describe('editor_frame', () => { ); }); - expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) ); }); @@ -615,14 +615,14 @@ describe('editor_frame', () => { ); }); const updatedState = {}; - const setVisualizationState = (mockVisualization.getLayerOptions as jest.Mock).mock - .calls[0][0].setState; + const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] + .setState; act(() => { - setVisualizationState(updatedState); + setDatasourceState(updatedState); }); - expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(2); - expect(mockVisualization.getLayerOptions).toHaveBeenLastCalledWith( + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, }) @@ -689,8 +689,6 @@ describe('editor_frame', () => { const updatedPublicAPI: DatasourcePublicAPI = { datasourceId: 'testDatasource', - renderLayerPanel: jest.fn(), - // renderDimensionPanel: jest.fn(), getOperationForColumnId: jest.fn(), getTableSpec: jest.fn(), }; @@ -702,8 +700,8 @@ describe('editor_frame', () => { setDatasourceState({}); }); - expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(2); - expect(mockVisualization.getLayerOptions).toHaveBeenLastCalledWith( + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ datasourceLayers: { @@ -756,10 +754,10 @@ describe('editor_frame', () => { ); }); - expect(mockVisualization.getLayerOptions).toHaveBeenCalled(); + expect(mockVisualization.getConfiguration).toHaveBeenCalled(); const datasourceLayers = - mockVisualization.getLayerOptions.mock.calls[0][0].frame.datasourceLayers; + mockVisualization.getConfiguration.mock.calls[0][0].frame.datasourceLayers; expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); @@ -812,21 +810,18 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource1State, - setState: expect.anything(), layerId: 'first', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'second', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'third', }) ); @@ -859,45 +854,9 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ dateRange, state: datasourceState, - setState: expect.any(Function), layerId: 'first', }); }); - - it('should re-create the public api after state has been set', async () => { - mockDatasource.getLayers.mockReturnValue(['first']); - - await act(async () => { - mount( - - ); - }); - - const updatedState = {}; - const setDatasourceState = mockDatasource.getPublicAPI.mock.calls[0][0].setState; - act(() => { - setDatasourceState(updatedState); - }); - - expect(mockDatasource.getPublicAPI).toHaveBeenLastCalledWith( - expect.objectContaining({ - state: updatedState, - setState: expect.any(Function), - layerId: 'first', - }) - ); - }); }); describe('switching', () => { @@ -1022,7 +981,7 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState); - expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1039,7 +998,7 @@ describe('editor_frame', () => { datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }), }) ); - expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1238,8 +1197,8 @@ describe('editor_frame', () => { .simulate('click'); }); - expect(mockVisualization.getLayerOptions).toHaveBeenCalledTimes(1); - expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1); + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1304,7 +1263,7 @@ describe('editor_frame', () => { .simulate('drop'); }); - expect(mockVisualization.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1381,7 +1340,7 @@ describe('editor_frame', () => { }); }); - expect(mockVisualization2.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1480,7 +1439,7 @@ describe('editor_frame', () => { }); }); - expect(mockVisualization3.getLayerOptions).toHaveBeenCalledWith( + expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 93f58938ec625..082519d9a8feb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -91,21 +91,11 @@ export function EditorFrame(props: EditorFrameProps) { const layers = datasource.getLayers(datasourceState); layers.forEach(layer => { - const publicAPI = props.datasourceMap[id].getPublicAPI({ + datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ state: datasourceState, - setState: (newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - datasourceId: id, - updater: newState, - clearStagedPreview: true, - }); - }, layerId: layer, dateRange: props.dateRange, }); - - datasourceLayers[layer] = publicAPI; }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 1115126792c86..93f6ea6ea67ac 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -373,7 +373,6 @@ function getPreviewExpression( layerId, dateRange: frame.dateRange, state: datasourceState, - setState: () => {}, }); } }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index 30999bd44c247..5d2f68a5567eb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -33,12 +33,12 @@ export function createMockVisualization(): jest.Mocked { getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_frame, _state?) => ({})), - getLayerOptions: jest.fn(props => ({ - dimensions: [ + getConfiguration: jest.fn(props => ({ + groups: [ { + groupId: 'a', + groupLabel: 'a', layerId: 'layer1', - dimensionId: 'a', - dimensionLabel: 'a', supportsMoreColumns: true, accessors: [], filterOperations: jest.fn(() => true), @@ -63,7 +63,6 @@ export function createMockDatasource(id: string): DatasourceMock { datasourceId: id, getTableSpec: jest.fn(() => []), getOperationForColumnId: jest.fn(), - renderLayerPanel: jest.fn(), }; return { @@ -75,6 +74,7 @@ export function createMockDatasource(id: string): DatasourceMock { getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), + renderLayerPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => {}), removeLayer: jest.fn((_state, _layerId) => {}), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 9bf50fd6b13b9..5d87137db3d39 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -54,9 +54,10 @@ type Props = Pick< DatasourceDimensionDropProps, 'layerId' | 'columnId' | 'state' | 'filterOperations' >; -const getOperationFieldSupportMatrix = /* _.memoize( */ ( - props: Props -): OperationFieldSupportMatrix => { + +// TODO: This code has historically been memoized, as a potentially performance +// sensitive task. If we can add memoization without breaking the behavior, we should. +const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; @@ -86,12 +87,7 @@ const getOperationFieldSupportMatrix = /* _.memoize( */ ( operationByField: _.mapValues(supportedOperationsByField, _.uniq), fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), }; -}; /* , - - (props: Props) => { - return props.layerId + ' ' + props.columnId; - } -);*/ +}; export function canHandleDrop(props: DatasourceDimensionDropProps) { const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 25121eec30f2a..76e59a170a9e9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -408,7 +408,6 @@ describe('IndexPattern Data Source', () => { const initialState = stateFromPersistedState(persistedState); publicAPI = indexPatternDatasource.getPublicAPI({ state: initialState, - setState: () => {}, layerId: 'first', dateRange: { fromDate: 'now-30d', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 79ba8b839ae1b..9c2a9c9bf4a09 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -274,10 +274,34 @@ export function getIndexPatternDatasource({ ); }, + renderLayerPanel: ( + domElement: Element, + props: DatasourceLayerPanelProps + ) => { + render( + { + changeLayerIndexPattern({ + savedObjectsClient, + indexPatternId, + setState: props.setState, + state: props.state, + layerId: props.layerId, + onError: onIndexPatternLoadError, + replaceIfPossible: true, + }); + }} + {...props} + />, + domElement + ); + }, + canHandleDrop, onDrop, - getPublicAPI({ state, setState, layerId }: PublicAPIProps) { + getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = uniqueLabels(state.layers); return { @@ -294,27 +318,6 @@ export function getIndexPatternDatasource({ } return null; }, - - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => { - render( - { - changeLayerIndexPattern({ - savedObjectsClient, - indexPatternId, - setState, - state, - layerId: props.layerId, - onError: onIndexPatternLoadError, - replaceIfPossible: true, - }); - }} - {...props} - />, - domElement - ); - }, }; }, getDatasourceSuggestionsForField(state, draggedField) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index af7afb9cf9342..219a6d935e436 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -178,6 +178,7 @@ describe('Layer Data Panel', () => { defaultProps = { layerId: 'first', state: initialState, + setState: jest.fn(), onChangeIndexPattern: jest.fn(async () => {}), }; }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index ae346ecc72cbc..eea00d52a77f9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -11,7 +11,8 @@ import { DatasourceLayerPanelProps } from '../types'; import { IndexPatternPrivateState } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; -export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { +export interface IndexPatternLayerPanelProps + extends DatasourceLayerPanelProps { state: IndexPatternPrivateState; onChangeIndexPattern: (newId: string) => void; } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 237e1c4069898..276f24433c670 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -81,7 +81,7 @@ describe('metric_visualization', () => { layerId: 'l1', }, layerId: 'l1', - dimensionId: '', + groupId: '', columnId: 'newDimension', }) ).toEqual({ @@ -100,7 +100,6 @@ describe('metric_visualization', () => { layerId: 'l1', }, layerId: 'l1', - dimensionId: '', columnId: 'a', }) ).toEqual({ diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx index 8da335ead6c06..44256df5aed6d 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -86,12 +86,12 @@ export const metricVisualization: Visualization = { getPersistableState: state => state, - getLayerOptions(props) { + getConfiguration(props) { return { - dimensions: [ + groups: [ { - dimensionId: 'metric', - dimensionLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), + groupId: 'metric', + groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), layerId: props.state.layerId, accessors: props.state.accessor ? [props.state.accessor] : [], supportsMoreColumns: false, diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts index d0792afe2f030..53fc103934255 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts @@ -6,7 +6,7 @@ export interface State { layerId: string; - accessor: string | undefined; + accessor?: string; } export interface MetricConfig extends State { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 492af6231f7bb..c897979b06cfb 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -17,7 +17,6 @@ export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { state: T; - setState: StateSetter; layerId: string; dateRange: DateRange; } @@ -141,6 +140,7 @@ export interface Datasource { renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; @@ -161,9 +161,6 @@ export interface DatasourcePublicAPI { datasourceId: string; getTableSpec: () => Array<{ columnId: string }>; getOperationForColumnId: (columnId: string) => Operation | null; - - // Render can be called many times - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; } export interface DatasourceDataPanelProps { @@ -176,26 +173,30 @@ export interface DatasourceDataPanelProps { filters: Filter[]; } -export interface DatasourceDimensionProps { - layerId: string; - columnId: string; - - // Visualizations can restrict operations based on their own rules +interface SharedDimensionProps { + /** Visualizations can restrict operations based on their own rules. + * For example, limiting to only bucketed or only numeric operations. + */ filterOperations: (operation: OperationMetadata) => boolean; - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query + /** Visualizations can hint at the role this dimension would play, which + * affects the default ordering of the query + */ suggestedPriority?: DimensionPriority; - onRemove?: (accessor: string) => void; - - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control + /** Some dimension editors will allow users to change the operation grouping + * from the panel, and this lets the visualization hint that it doesn't want + * users to have that level of control + */ hideGrouping?: boolean; +} +export type DatasourceDimensionProps = SharedDimensionProps & { + layerId: string; + columnId: string; + onRemove?: (accessor: string) => void; state: T; -} +}; // The only way a visualization has to restrict the query building export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { @@ -209,21 +210,19 @@ export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { togglePopover: () => void; }; -export interface DatasourceLayerPanelProps { +export interface DatasourceLayerPanelProps { layerId: string; + state: T; + setState: StateSetter; } -export interface DatasourceDimensionDropProps { +export type DatasourceDimensionDropProps = SharedDimensionProps & { layerId: string; columnId: string; - // Visualizations can restrict operations based on their own rules - filterOperations: (operation: OperationMetadata) => boolean; - suggestedPriority?: DimensionPriority; - state: T; setState: StateSetter; dragDropContext: DragContextState; -} +}; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { droppedItem: unknown; @@ -262,37 +261,32 @@ export interface LensMultiTable { }; } -export interface VisualizationLayerConfigProps { +export interface VisualizationConfigProps { layerId: string; frame: FramePublicAPI; state: T; - setState: (newState: T) => void; } -interface VisualizationDimensionConfig { - dimensionId: string; - // Displayed to user - dimensionLabel: string; - - supportsMoreColumns: boolean; - accessors: string[]; - - // Visualizations can restrict operations based on their own rules - filterOperations: (operation: OperationMetadata) => boolean; - - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query - suggestedPriority?: DimensionPriority; - onRemove?: (accessor: string) => void; +export type VisualizationLayerWidgetProps = VisualizationConfigProps & { + setState: (newState: T) => void; +}; - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control - hideGrouping?: boolean; +type VisualizationDimensionGroupConfig = SharedDimensionProps & { + groupLabel: string; + /** ID is passed back to visualization. For example, `x` */ + groupId: string; + accessors: string[]; + supportsMoreColumns: boolean; + /** If required, a warning will appear if accessors are empty */ required?: boolean; - dataTestSubj?: string; +}; + +interface VisualizationDimensionChangeProps { + layerId: string; + columnId: string; + prevState: T; } /** @@ -384,11 +378,11 @@ export interface Visualization { // 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: VisualizationLayerConfigProps) => void; + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - getLayerOptions: ( - props: VisualizationLayerConfigProps - ) => { dimensions: VisualizationDimensionConfig[] }; + getConfiguration: ( + props: VisualizationConfigProps + ) => { groups: VisualizationDimensionGroupConfig[] }; getDescription: ( state: T @@ -405,18 +399,12 @@ export interface Visualization { getPersistableState: (state: T) => P; // Actions triggered by the frame which tell the datasource that a dimension is being changed - setDimension: (props: { - layerId: string; - dimensionId: string; - columnId: string; - prevState: T; - }) => T; - removeDimension: (props: { - layerId: string; - dimensionId: string; - columnId: string; - prevState: T; - }) => T; + setDimension: ( + props: VisualizationDimensionChangeProps & { + groupId: string; + } + ) => T; + removeDimension: (props: VisualizationDimensionChangeProps) => T; toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index 21f7c662098dc..9b068b0ca5ef0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -9,6 +9,10 @@ import { ScaleType } from '@elastic/charts'; import { State, LayerConfig } from './types'; import { FramePublicAPI, OperationMetadata } from '../types'; +interface ValidLayer extends LayerConfig { + xAccessor: NonNullable; +} + function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { const defaults = { xTitle: 'x', @@ -98,7 +102,9 @@ export const buildExpression = ( frame?: FramePublicAPI, { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } ): Ast | null => { - const validLayers = state.layers.filter(layer => layer.xAccessor && layer.accessors.length); + const validLayers = state.layers.filter((layer): layer is ValidLayer => + Boolean(layer.xAccessor && layer.accessors.length) + ); if (!validLayers.length) { return null; } @@ -144,7 +150,7 @@ export const buildExpression = ( const xAxisOperation = frame && - frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor!); + frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); const isHistogramDimension = Boolean( xAxisOperation && @@ -164,15 +170,15 @@ export const buildExpression = ( hide: [Boolean(layer.hide)], - xAccessor: [layer.xAccessor!], + xAccessor: [layer.xAccessor], yScaleType: [ getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), ], xScaleType: [ - getScaleType(metadata[layer.layerId][layer.xAccessor!], ScaleType.Linear), + getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), ], isHistogram: [isHistogramDimension], - splitAccessor: [layer.splitAccessor!], + splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [], seriesType: [layer.seriesType], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 4faa5251b6baf..5e85680cc2b2c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import { State, SeriesType, visualizationTypes } from './types'; -import { VisualizationLayerConfigProps } from '../types'; +import { VisualizationLayerWidgetProps } from '../types'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -25,7 +25,7 @@ function updateLayer(state: State, layer: UnwrapArray, index: n }; } -export function LayerContextMenu(props: VisualizationLayerConfigProps) { +export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); const index = state.layers.findIndex(l => l.layerId === layerId); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts index 8924d9f5c9b8a..5e9311bb1e928 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -355,7 +355,7 @@ function buildSuggestion({ layerId, seriesType, xAccessor: xValue.columnId, - splitAccessor: splitBy ? splitBy.columnId : undefined, + splitAccessor: splitBy?.columnId, accessors: yValues.map(col => col.columnId), }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 57d9be0e866da..beccf0dc46eb4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -10,8 +10,6 @@ import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -jest.mock('../id_generator'); - function exampleState(): State { return { legend: { position: Position.Bottom, isVisible: true }, @@ -187,7 +185,7 @@ describe('xy_visualization', () => { ], }, layerId: 'first', - dimensionId: 'x', + groupId: 'x', columnId: 'newCol', }).layers[0] ).toEqual({ @@ -213,7 +211,7 @@ describe('xy_visualization', () => { ], }, layerId: 'first', - dimensionId: 'x', + groupId: 'x', columnId: 'newCol', }).layers[0] ).toEqual({ @@ -241,8 +239,7 @@ describe('xy_visualization', () => { ], }, layerId: 'first', - dimensionId: 'x', - columnId: 'newCol', + columnId: 'a', }).layers[0] ).toEqual({ layerId: 'first', @@ -253,7 +250,7 @@ describe('xy_visualization', () => { }); }); - describe('#getLayerOptions', () => { + describe('#getConfiguration', () => { let mockDatasource: ReturnType; let frame: ReturnType; @@ -274,24 +271,22 @@ describe('xy_visualization', () => { }); it('should return options for 3 dimensions', () => { - const options = xyVisualization.getLayerOptions({ + const options = xyVisualization.getConfiguration({ state: exampleState(), frame, layerId: 'first', - setState: jest.fn, - }).dimensions; + }).groups; expect(options).toHaveLength(3); - expect(options.map(o => o.dimensionId)).toEqual(['x', 'y', 'breakdown']); + expect(options.map(o => o.groupId)).toEqual(['x', 'y', 'breakdown']); }); it('should only accept bucketed operations for x', () => { - const options = xyVisualization.getLayerOptions({ + const options = xyVisualization.getConfiguration({ state: exampleState(), frame, layerId: 'first', - setState: jest.fn, - }).dimensions; - const filterOperations = options.find(o => o.dimensionId === 'x')!.filterOperations; + }).groups; + const filterOperations = options.find(o => o.groupId === 'x')!.filterOperations; const exampleOperation: Operation = { dataType: 'number', @@ -315,23 +310,21 @@ describe('xy_visualization', () => { }); it('should not allow anything to be added to x', () => { - const options = xyVisualization.getLayerOptions({ + const options = xyVisualization.getConfiguration({ state: exampleState(), frame, layerId: 'first', - setState: jest.fn, - }).dimensions; - expect(options.find(o => o.dimensionId === 'x')?.supportsMoreColumns).toBe(false); + }).groups; + expect(options.find(o => o.groupId === 'x')?.supportsMoreColumns).toBe(false); }); it('should allow number operations on y', () => { - const options = xyVisualization.getLayerOptions({ + const options = xyVisualization.getConfiguration({ state: exampleState(), frame, layerId: 'first', - setState: jest.fn, - }).dimensions; - const filterOperations = options.find(o => o.dimensionId === 'y')!.filterOperations; + }).groups; + const filterOperations = options.find(o => o.groupId === 'y')!.filterOperations; const exampleOperation: Operation = { dataType: 'number', isBucketed: false, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index a7587e331b788..c72fa0fec24d7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -146,13 +146,13 @@ export const xyVisualization: Visualization = { getPersistableState: state => state, - getLayerOptions(props) { + getConfiguration(props) { const layer = props.state.layers.find(l => l.layerId === props.layerId)!; return { - dimensions: [ + groups: [ { - dimensionId: 'x', - dimensionLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { + groupId: 'x', + groupLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { defaultMessage: 'X-axis', }), accessors: layer.xAccessor ? [layer.xAccessor] : [], @@ -163,8 +163,8 @@ export const xyVisualization: Visualization = { dataTestSubj: 'lnsXY_xDimensionPanel', }, { - dimensionId: 'y', - dimensionLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { + groupId: 'y', + groupLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { defaultMessage: 'Y-axis', }), accessors: layer.accessors, @@ -174,8 +174,8 @@ export const xyVisualization: Visualization = { dataTestSubj: 'lnsXY_yDimensionPanel', }, { - dimensionId: 'breakdown', - dimensionLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { + groupId: 'breakdown', + groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { defaultMessage: 'Break down by', }), accessors: layer.splitAccessor ? [layer.splitAccessor] : [], @@ -188,19 +188,19 @@ export const xyVisualization: Visualization = { }; }, - setDimension({ prevState, layerId, columnId, dimensionId }) { + setDimension({ prevState, layerId, columnId, groupId }) { const newLayer = prevState.layers.find(l => l.layerId === layerId); if (!newLayer) { return prevState; } - if (dimensionId === 'x') { + if (groupId === 'x') { newLayer.xAccessor = columnId; } - if (dimensionId === 'y') { + if (groupId === 'y') { newLayer.accessors = [...newLayer.accessors.filter(a => a !== columnId), columnId]; } - if (dimensionId === 'breakdown') { + if (groupId === 'breakdown') { newLayer.splitAccessor = columnId; } @@ -210,20 +210,18 @@ export const xyVisualization: Visualization = { }; }, - removeDimension({ prevState, layerId, columnId, dimensionId }) { + removeDimension({ prevState, layerId, columnId }) { const newLayer = prevState.layers.find(l => l.layerId === layerId); if (!newLayer) { return prevState; } - if (dimensionId === 'x') { + if (newLayer.xAccessor === columnId) { delete newLayer.xAccessor; - } - if (dimensionId === 'y') { - newLayer.accessors = newLayer.accessors.filter(a => a !== columnId); - } - if (dimensionId === 'breakdown') { + } else if (newLayer.splitAccessor === columnId) { delete newLayer.splitAccessor; + } else if (newLayer.accessors.includes(columnId)) { + newLayer.accessors = newLayer.accessors.filter(a => a !== columnId); } return {