[Lens] (Accessibility) Added button to execute drag and drop to workspace (#85960) (#88853)

This commit is contained in:
Joe Reuter 2021-01-20 18:56:23 +01:00 committed by GitHub
parent ee5b6d22a3
commit d476b42857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 447 additions and 231 deletions

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
import { NativeRenderer } from '../../native_renderer';
import { Action } from './state_management';
import { DragContext } from '../../drag_drop';
import { DragContext, Dragging } from '../../drag_drop';
import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types';
import { Query, Filter } from '../../../../../../src/plugins/data/public';
@ -26,6 +26,8 @@ interface DataPanelWrapperProps {
query: Query;
dateRange: FramePublicAPI['dateRange'];
filters: Filter[];
dropOntoWorkspace: (field: Dragging) => void;
hasSuggestionForField: (field: Dragging) => boolean;
}
export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
@ -51,6 +53,8 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
dateRange: props.dateRange,
filters: props.filters,
showNoDataPopover: props.showNoDataPopover,
dropOntoWorkspace: props.dropOntoWorkspace,
hasSuggestionForField: props.hasSuggestionForField,
};
const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false);

View file

@ -632,16 +632,19 @@ describe('editor_frame', () => {
);
});
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
mockDatasource.renderDataPanel.mockClear();
const updatedState = {
title: 'shazm',
};
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
act(() => {
setDatasourceState(updatedState);
});
expect(mockDatasource.renderDataPanel).toHaveBeenCalledTimes(2);
expect(mockDatasource.renderDataPanel).toHaveBeenCalledTimes(1);
expect(mockDatasource.renderDataPanel).toHaveBeenLastCalledWith(
expect.any(Element),
expect.objectContaining({

View file

@ -16,14 +16,19 @@ 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 { Dragging, RootDragDropProvider } from '../../drag_drop';
import { getSavedObjectFormat } from './save';
import { generateId } from '../../id_generator';
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
import { EditorFrameStartPlugins } from '../service';
import { initializeDatasources, createDatasourceLayers } from './state_helpers';
import { applyVisualizeFieldSuggestions } from './suggestion_helpers';
import {
applyVisualizeFieldSuggestions,
getTopSuggestionForField,
switchToSuggestion,
} from './suggestion_helpers';
import { trackUiEvent } from '../../lens_ui_telemetry';
export interface EditorFrameProps {
doc?: Document;
@ -254,6 +259,53 @@ export function EditorFrame(props: EditorFrameProps) {
]
);
const getSuggestionForField = React.useCallback(
(field: Dragging) => {
const { activeDatasourceId, datasourceStates } = state;
const activeVisualizationId = state.visualization.activeId;
const visualizationState = state.visualization.state;
const { visualizationMap, datasourceMap } = props;
if (!field || !activeDatasourceId) {
return;
}
return getTopSuggestionForField(
datasourceLayers,
activeVisualizationId,
visualizationMap,
visualizationState,
datasourceMap[activeDatasourceId],
datasourceStates,
field
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
state.visualization.state,
props.datasourceMap,
props.visualizationMap,
state.activeDatasourceId,
state.datasourceStates,
]
);
const hasSuggestionForField = React.useCallback(
(field: Dragging) => getSuggestionForField(field) !== undefined,
[getSuggestionForField]
);
const dropOntoWorkspace = React.useCallback(
(field) => {
const suggestion = getSuggestionForField(field);
if (suggestion) {
trackUiEvent('drop_onto_workspace');
switchToSuggestion(dispatch, suggestion, 'SWITCH_VISUALIZATION');
}
},
[getSuggestionForField]
);
return (
<RootDragDropProvider>
<FrameLayout
@ -277,6 +329,8 @@ export function EditorFrame(props: EditorFrameProps) {
dateRange={props.dateRange}
filters={props.filters}
showNoDataPopover={props.showNoDataPopover}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
/>
}
configPanel={
@ -310,6 +364,7 @@ export function EditorFrame(props: EditorFrameProps) {
core={props.core}
plugins={props.plugins}
visualizeTriggerFieldContext={visualizeTriggerFieldContext}
getSuggestionForField={getSuggestionForField}
/>
)
}

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getSuggestions } from './suggestion_helpers';
import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers';
import { createMockVisualization, createMockDatasource, DatasourceMock } from '../mocks';
import { TableSuggestion, DatasourceSuggestion } from '../../types';
import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types';
import { PaletteOutput } from 'src/plugins/charts/public';
const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({
@ -472,4 +472,133 @@ describe('suggestion helpers', () => {
})
);
});
describe('getTopSuggestionForField', () => {
let mockVisualization1: jest.Mocked<Visualization>;
let mockVisualization2: jest.Mocked<Visualization>;
let mockDatasourceState: unknown;
let defaultParams: Parameters<typeof getTopSuggestionForField>;
beforeEach(() => {
datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([
{
state: {},
table: {
isMultiRow: true,
layerId: '1',
columns: [],
changeType: 'unchanged',
},
keptLayerIds: [],
},
]);
mockVisualization1 = createMockVisualization();
mockVisualization1.getSuggestions.mockReturnValue([
{
score: 0.3,
title: 'second suggestion',
state: { second: true },
previewIcon: 'empty',
},
{
score: 0.5,
title: 'top suggestion',
state: { first: true },
previewIcon: 'empty',
},
]);
mockVisualization2 = createMockVisualization();
mockVisualization2.getSuggestions.mockReturnValue([
{
score: 0.8,
title: 'other vis suggestion',
state: {},
previewIcon: 'empty',
},
]);
mockDatasourceState = { myDatasourceState: true };
defaultParams = [
{
'1': {
getTableSpec: () => [{ columnId: 'col1' }],
datasourceId: '',
getOperationForColumnId: jest.fn(),
},
},
'vis1',
{ vis1: mockVisualization1 },
{},
datasourceMap.mock,
{
mockindexpattern: { state: mockDatasourceState, isLoading: false },
},
{ id: 'myfield' },
];
});
it('should return top suggestion for field', () => {
const result = getTopSuggestionForField(...defaultParams);
expect(result!.title).toEqual('top suggestion');
expect(datasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith(
mockDatasourceState,
{
id: 'myfield',
}
);
});
it('should return nothing if visualization does not produce suggestions', () => {
mockVisualization1.getSuggestions.mockReturnValue([]);
const result = getTopSuggestionForField(...defaultParams);
expect(result).toEqual(undefined);
});
it('should return nothing if datasource does not produce suggestions', () => {
datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]);
defaultParams[2] = {
vis1: { ...mockVisualization1, getSuggestions: () => [] },
vis2: mockVisualization2,
};
const result = getTopSuggestionForField(...defaultParams);
expect(result).toEqual(undefined);
});
it('should not consider suggestion from other visualization if there is data', () => {
defaultParams[2] = {
vis1: { ...mockVisualization1, getSuggestions: () => [] },
vis2: mockVisualization2,
};
const result = getTopSuggestionForField(...defaultParams);
expect(result).toBeUndefined();
});
it('should consider top suggestion from other visualization if there is no data', () => {
const mockVisualization3 = createMockVisualization();
defaultParams[0] = {
'1': {
getTableSpec: () => [],
datasourceId: '',
getOperationForColumnId: jest.fn(),
},
};
mockVisualization1.getSuggestions.mockReturnValue([]);
mockVisualization3.getSuggestions.mockReturnValue([
{
score: 0.1,
title: 'low ranking suggestion',
state: {},
previewIcon: 'empty',
},
]);
defaultParams[2] = {
vis1: mockVisualization1,
vis2: mockVisualization2,
vis3: mockVisualization3,
};
const result = getTopSuggestionForField(...defaultParams);
expect(result!.title).toEqual('other vis suggestion');
expect(mockVisualization1.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
expect(mockVisualization3.getSuggestions).toHaveBeenCalled();
});
});
});

View file

@ -16,8 +16,10 @@ import {
TableChangeType,
TableSuggestion,
DatasourceSuggestion,
DatasourcePublicAPI,
} from '../../types';
import { Action } from './state_management';
import { Dragging } from '../../drag_drop';
export interface Suggestion {
visualizationId: string;
@ -221,3 +223,35 @@ export function switchToSuggestion(
dispatch(action);
}
export function getTopSuggestionForField(
datasourceLayers: Record<string, DatasourcePublicAPI>,
activeVisualizationId: string | null,
visualizationMap: Record<string, Visualization<unknown>>,
visualizationState: unknown,
datasource: Datasource,
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
field: Dragging
) {
const hasData = Object.values(datasourceLayers).some(
(datasourceLayer) => datasourceLayer.getTableSpec().length > 0
);
const mainPalette =
activeVisualizationId && visualizationMap[activeVisualizationId]?.getMainPalette
? visualizationMap[activeVisualizationId].getMainPalette?.(visualizationState)
: undefined;
const suggestions = getSuggestions({
datasourceMap: { [datasource.id]: datasource },
datasourceStates,
visualizationMap:
hasData && activeVisualizationId
? { [activeVisualizationId]: visualizationMap[activeVisualizationId] }
: visualizationMap,
activeVisualizationId,
visualizationState,
field,
mainPalette,
});
return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
}

View file

@ -7,7 +7,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public';
import { FramePublicAPI, TableSuggestion, Visualization } from '../../../types';
import { FramePublicAPI, Visualization } from '../../../types';
import {
createMockVisualization,
createMockDatasource,
@ -85,6 +85,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -108,6 +109,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -131,6 +133,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -168,6 +171,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -241,6 +245,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -284,6 +289,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -335,6 +341,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -415,6 +422,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
});
@ -471,6 +479,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
});
@ -528,6 +537,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -569,6 +579,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -612,6 +623,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -652,6 +664,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
@ -690,6 +703,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
});
@ -734,6 +748,7 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={() => undefined}
/>
);
});
@ -756,6 +771,7 @@ describe('workspace_panel', () => {
describe('suggestions from dropping in workspace panel', () => {
let mockDispatch: jest.Mock;
let mockGetSuggestionForField: jest.Mock;
let frame: jest.Mocked<FramePublicAPI>;
const draggedField = { id: 'field' };
@ -763,6 +779,7 @@ describe('workspace_panel', () => {
beforeEach(() => {
frame = createMockFramePublicAPI();
mockDispatch = jest.fn();
mockGetSuggestionForField = jest.fn();
});
function initComponent(draggingContext = draggedField) {
@ -790,43 +807,23 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
core={coreMock.createSetup()}
plugins={{ uiActions: uiActionsMock, data: dataMock }}
getSuggestionForField={mockGetSuggestionForField}
/>
</ChildDragDropProvider>
);
}
it('should immediately transition if exactly one suggestion is returned', () => {
const expectedTable: TableSuggestion = {
isMultiRow: true,
layerId: '1',
columns: [],
changeType: 'unchanged',
};
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: expectedTable,
keptLayerIds: [],
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.5,
title: 'my title',
state: {},
previewIcon: 'empty',
},
]);
mockGetSuggestionForField.mockReturnValue({
visualizationId: 'vis',
datasourceState: {},
datasourceId: 'mock',
visualizationState: {},
});
initComponent();
instance.find(DragDrop).prop('onDrop')!(draggedField);
expect(mockDatasource.getDatasourceSuggestionsForField).toHaveBeenCalledTimes(1);
expect(mockVisualization.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
table: expectedTable,
})
);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SWITCH_VISUALIZATION',
newVisualizationId: 'vis',
@ -837,80 +834,12 @@ describe('workspace_panel', () => {
});
it('should allow to drop if there are suggestions', () => {
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: {
isMultiRow: true,
layerId: '1',
columns: [],
changeType: 'unchanged',
},
keptLayerIds: [],
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.5,
title: 'my title',
state: {},
previewIcon: 'empty',
},
]);
initComponent();
expect(instance.find(DragDrop).prop('droppable')).toBeTruthy();
});
it('should refuse to drop if there only suggestions from other visualizations if there are data tables', () => {
frame.datasourceLayers.a = mockDatasource.publicAPIMock;
mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]);
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: {
isMultiRow: true,
layerId: '1',
columns: [],
changeType: 'unchanged',
},
keptLayerIds: [],
},
]);
mockVisualization2.getSuggestions.mockReturnValueOnce([
{
score: 0.5,
title: 'my title',
state: {},
previewIcon: 'empty',
},
]);
initComponent();
expect(instance.find(DragDrop).prop('droppable')).toBeFalsy();
});
it('should allow to drop if there are suggestions from active visualization even if there are data tables', () => {
frame.datasourceLayers.a = mockDatasource.publicAPIMock;
mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]);
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: {
isMultiRow: true,
layerId: '1',
columns: [],
changeType: 'unchanged',
},
keptLayerIds: [],
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.5,
title: 'my title',
state: {},
previewIcon: 'empty',
},
]);
mockGetSuggestionForField.mockReturnValue({
visualizationId: 'vis',
datasourceState: {},
datasourceId: 'mock',
visualizationState: {},
});
initComponent();
expect(instance.find(DragDrop).prop('droppable')).toBeTruthy();
});
@ -919,61 +848,5 @@ describe('workspace_panel', () => {
initComponent();
expect(instance.find(DragDrop).prop('droppable')).toBeFalsy();
});
it('should immediately transition to the first suggestion if there are multiple', () => {
mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([
{
state: {},
table: {
isMultiRow: true,
columns: [],
layerId: '1',
changeType: 'unchanged',
},
keptLayerIds: [],
},
{
state: {},
table: {
isMultiRow: true,
columns: [],
layerId: '1',
changeType: 'unchanged',
},
keptLayerIds: [],
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.5,
title: 'second suggestion',
state: {},
previewIcon: 'empty',
},
]);
mockVisualization.getSuggestions.mockReturnValueOnce([
{
score: 0.8,
title: 'first suggestion',
state: {
isFirst: true,
},
previewIcon: 'empty',
},
]);
initComponent();
instance.find(DragDrop).prop('onDrop')!(draggedField);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SWITCH_VISUALIZATION',
newVisualizationId: 'vis',
initialState: {
isFirst: true,
},
datasourceState: {},
datasourceId: 'mock',
});
});
});
});

View file

@ -39,8 +39,8 @@ import {
isLensFilterEvent,
isLensEditEvent,
} from '../../../types';
import { DragDrop, DragContext } from '../../../drag_drop';
import { getSuggestions, switchToSuggestion } from '../suggestion_helpers';
import { DragDrop, DragContext, Dragging } from '../../../drag_drop';
import { Suggestion, switchToSuggestion } from '../suggestion_helpers';
import { buildExpression } from '../expression_helpers';
import { debouncedComponent } from '../../../debounced_component';
import { trackUiEvent } from '../../../lens_ui_telemetry';
@ -75,6 +75,7 @@ export interface WorkspacePanelProps {
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
title?: string;
visualizeTriggerFieldContext?: VisualizeFieldContext;
getSuggestionForField: (field: Dragging) => Suggestion | undefined;
}
interface WorkspaceState {
@ -97,43 +98,11 @@ export function WorkspacePanel({
ExpressionRenderer: ExpressionRendererComponent,
title,
visualizeTriggerFieldContext,
getSuggestionForField,
}: WorkspacePanelProps) {
const dragDropContext = useContext(DragContext);
const suggestionForDraggedField = useMemo(
() => {
if (!dragDropContext.dragging || !activeDatasourceId) {
return;
}
const hasData = Object.values(framePublicAPI.datasourceLayers).some(
(datasource) => datasource.getTableSpec().length > 0
);
const mainPalette =
activeVisualizationId &&
visualizationMap[activeVisualizationId] &&
visualizationMap[activeVisualizationId].getMainPalette
? visualizationMap[activeVisualizationId].getMainPalette!(visualizationState)
: undefined;
const suggestions = getSuggestions({
datasourceMap: { [activeDatasourceId]: datasourceMap[activeDatasourceId] },
datasourceStates,
visualizationMap:
hasData && activeVisualizationId
? { [activeVisualizationId]: visualizationMap[activeVisualizationId] }
: visualizationMap,
activeVisualizationId,
visualizationState,
field: dragDropContext.dragging,
mainPalette,
});
return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[dragDropContext.dragging]
);
const suggestionForDraggedField = getSuggestionForField(dragDropContext.dragging);
const [localState, setLocalState] = useState<WorkspaceState>({
expressionBuildError: undefined,

View file

@ -260,6 +260,8 @@ describe('IndexPattern Data Panel', () => {
query: { query: '', language: 'lucene' },
filters: [],
showNoDataPopover: jest.fn(),
dropOntoWorkspace: jest.fn(),
hasSuggestionForField: jest.fn(() => false),
};
});

View file

@ -100,6 +100,8 @@ export function IndexPatternDataPanel({
changeIndexPattern,
charts,
showNoDataPopover,
dropOntoWorkspace,
hasSuggestionForField,
}: Props) {
const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state;
const onChangeIndexPattern = useCallback(
@ -193,6 +195,8 @@ export function IndexPatternDataPanel({
onChangeIndexPattern={onChangeIndexPattern}
existingFields={state.existingFields}
existenceFetchFailed={state.existenceFetchFailed}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
/>
)}
</>
@ -241,6 +245,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
data,
existingFields,
charts,
dropOntoWorkspace,
hasSuggestionForField,
}: Omit<DatasourceDataPanelProps, 'state' | 'setState' | 'showNoDataPopover'> & {
data: DataPublicPluginStart;
currentIndexPatternId: string;
@ -593,6 +599,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
currentIndexPatternId={currentIndexPatternId}
existenceFetchFailed={existenceFetchFailed}
existFieldsInIndex={!!allFields.length}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -42,7 +42,6 @@
max-width: 300px;
}
.lnsFieldItem__buttonGroup {
// Enforce lowercase for buttons or else some browsers inherit all caps from flyout title
.lnsFieldItem__fieldPanelTitle {
text-transform: none;
}

View file

@ -95,6 +95,8 @@ describe('IndexPattern Field Item', () => {
},
exists: true,
chartsThemeService,
dropOntoWorkspace: () => {},
hasSuggestionForField: () => false,
};
data.fieldFormats = ({

View file

@ -6,10 +6,11 @@
import './field_item.scss';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import DateMath from '@elastic/datemath';
import {
EuiButtonGroup,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
@ -18,7 +19,9 @@ import {
EuiPopoverFooter,
EuiPopoverTitle,
EuiProgress,
EuiSpacer,
EuiText,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import {
@ -45,7 +48,7 @@ import {
import { FieldButton } from '../../../../../src/plugins/kibana_react/public';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { DraggedField } from './indexpattern';
import { DragDrop } from '../drag_drop';
import { DragDrop, Dragging } from '../drag_drop';
import { DatasourceDataPanelProps, DataType } from '../types';
import { BucketedAggregation, FieldStatsResponse } from '../../common';
import { IndexPattern, IndexPatternField } from './types';
@ -66,6 +69,8 @@ export interface FieldItemProps {
chartsThemeService: ChartsPluginSetup['theme'];
filters: Filter[];
hideDetails?: boolean;
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
}
interface State {
@ -95,10 +100,19 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
dateRange,
filters,
hideDetails,
dropOntoWorkspace,
} = props;
const [infoIsOpen, setOpen] = useState(false);
const dropOntoWorkspaceAndClose = useCallback(
(droppedField: Dragging) => {
dropOntoWorkspace(droppedField);
setOpen(false);
},
[dropOntoWorkspace, setOpen]
);
const [state, setState] = useState<State>({
isLoading: false,
});
@ -142,10 +156,6 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
}
function togglePopover() {
if (hideDetails) {
return;
}
setOpen(!infoIsOpen);
if (!infoIsOpen) {
trackUiEvent('indexpattern_field_info_click');
@ -227,8 +237,13 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
closePopover={() => setOpen(false)}
anchorPosition="rightUp"
panelClassName="lnsFieldItem__fieldPanel"
initialFocus=".lnsFieldItem__fieldPanel"
>
<FieldItemPopoverContents {...state} {...props} />
<FieldItemPopoverContents
{...state}
{...props}
dropOntoWorkspace={dropOntoWorkspaceAndClose}
/>
</EuiPopover>
</li>
);
@ -236,6 +251,40 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
export const FieldItem = debouncedComponent(InnerFieldItem);
function FieldPanelHeader({
indexPatternId,
field,
hasSuggestionForField,
dropOntoWorkspace,
}: {
field: IndexPatternField;
indexPatternId: string;
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
}) {
const draggableField = {
indexPatternId,
id: field.name,
field,
};
return (
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
<EuiFlexItem>
<EuiTitle size="xxs">
<h5 className="lnsFieldItem__fieldPanelTitle">{field.displayName}</h5>
</EuiTitle>
</EuiFlexItem>
<DragToWorkspaceButton
isEnabled={hasSuggestionForField(draggableField)}
dropOntoWorkspace={dropOntoWorkspace}
field={draggableField}
/>
</EuiFlexGroup>
);
}
function FieldItemPopoverContents(props: State & FieldItemProps) {
const {
histogram,
@ -247,6 +296,9 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
sampledValues,
chartsThemeService,
data: { fieldFormats },
dropOntoWorkspace,
hasSuggestionForField,
hideDetails,
} = props;
const chartTheme = chartsThemeService.useChartsTheme();
@ -270,6 +322,19 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
const [showingHistogram, setShowingHistogram] = useState(histogramDefault);
const panelHeader = (
<FieldPanelHeader
indexPatternId={indexPattern.id}
field={field}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
/>
);
if (hideDetails) {
return panelHeader;
}
let formatter: { convert: (data: unknown) => string };
if (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[field.name]) {
const FormatType = fieldFormats.getType(indexPattern.fieldFormatMap[field.name].id);
@ -300,12 +365,16 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
(!props.topValues || props.topValues.buckets.length === 0)
) {
return (
<EuiText size="s">
{i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', {
defaultMessage:
'This field is empty because it doesnt exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.',
})}
</EuiText>
<>
<EuiPopoverTitle>{panelHeader}</EuiPopoverTitle>
<EuiText size="s">
{i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', {
defaultMessage:
'This field is empty because it doesnt exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.',
})}
</EuiText>
</>
);
}
@ -340,31 +409,40 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
);
} else if (field.type === 'date') {
title = (
<>
{i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', {
defaultMessage: 'Time distribution',
})}
</>
<EuiTitle size="xxxs">
<h6>
{i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', {
defaultMessage: 'Time distribution',
})}
</h6>
</EuiTitle>
);
} else if (topValues && topValues.buckets.length) {
title = (
<>
{i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', {
defaultMessage: 'Top values',
})}
</>
<EuiTitle size="xxxs">
<h6>
{i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', {
defaultMessage: 'Top values',
})}
</h6>
</EuiTitle>
);
}
function wrapInPopover(el: React.ReactElement) {
return (
<>
{title ? <EuiPopoverTitle>{title}</EuiPopoverTitle> : <></>}
<EuiPopoverTitle>{panelHeader}</EuiPopoverTitle>
{title ? title : <></>}
<EuiSpacer size="s" />
{el}
{props.totalDocuments ? (
<EuiPopoverFooter>
<EuiText size="xs" textAlign="center">
<EuiText color="subdued" size="xs">
{props.sampledDocuments && (
<>
{i18n.translate('xpack.lens.indexPattern.percentageOfLabel', {
@ -552,3 +630,44 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
}
return <></>;
}
const DragToWorkspaceButton = ({
field,
dropOntoWorkspace,
isEnabled,
}: {
field: {
indexPatternId: string;
id: string;
field: IndexPatternField;
};
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
isEnabled: boolean;
}) => {
const buttonTitle = isEnabled
? i18n.translate('xpack.lens.indexPattern.moveToWorkspace', {
defaultMessage: 'Add {field} to workspace',
values: {
field: field.field.name,
},
})
: i18n.translate('xpack.lens.indexPattern.moveToWorkspaceDisabled', {
defaultMessage:
"This field can't be added to the workspace automatically. You can still use it directly in the configuration panel.",
});
return (
<EuiFlexItem grow={false}>
<EuiToolTip content={buttonTitle}>
<EuiButtonIcon
aria-label={buttonTitle}
isDisabled={!isEnabled}
iconType="plusInCircle"
onClick={() => {
dropOntoWorkspace(field);
}}
/>
</EuiToolTip>
</EuiFlexItem>
);
};

View file

@ -12,6 +12,7 @@ import { FieldItem } from './field_item';
import { NoFieldsCallout } from './no_fields_callout';
import { IndexPatternField } from './types';
import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion';
import { DatasourceDataPanelProps } from '../types';
const PAGINATION_SIZE = 50;
export type FieldGroups = Record<
@ -48,6 +49,8 @@ export function FieldList({
filter,
currentIndexPatternId,
existFieldsInIndex,
dropOntoWorkspace,
hasSuggestionForField,
}: {
exists: (field: IndexPatternField) => boolean;
fieldGroups: FieldGroups;
@ -60,6 +63,8 @@ export function FieldList({
};
currentIndexPatternId: string;
existFieldsInIndex: boolean;
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
}) {
const [pageSize, setPageSize] = useState(PAGINATION_SIZE);
const [scrollContainer, setScrollContainer] = useState<Element | undefined>(undefined);
@ -137,6 +142,8 @@ export function FieldList({
field={field}
hideDetails={true}
key={field.name}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
/>
))
)}
@ -147,6 +154,8 @@ export function FieldList({
.map(([key, fieldGroup]) => (
<Fragment key={key}>
<FieldsAccordion
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
initialIsOpen={Boolean(accordionState[key])}
key={key}
id={`lnsIndexPattern${key}`}

View file

@ -72,6 +72,8 @@ describe('Fields Accordion', () => {
fieldProps,
renderCallout: <div id="lens-test-callout">Callout</div>,
exists: () => true,
dropOntoWorkspace: () => {},
hasSuggestionForField: () => false,
};
});

View file

@ -50,6 +50,8 @@ export interface FieldsAccordionProps {
exists: (field: IndexPatternField) => boolean;
showExistenceFetchError?: boolean;
hideDetails?: boolean;
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
}
export const InnerFieldsAccordion = function InnerFieldsAccordion({
@ -67,6 +69,8 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
exists,
hideDetails,
showExistenceFetchError,
dropOntoWorkspace,
hasSuggestionForField,
}: FieldsAccordionProps) {
const renderField = useCallback(
(field: IndexPatternField) => (
@ -76,9 +80,11 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
field={field}
exists={exists(field)}
hideDetails={hideDetails}
dropOntoWorkspace={dropOntoWorkspace}
hasSuggestionForField={hasSuggestionForField}
/>
),
[fieldProps, exists, hideDetails]
[fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField]
);
const titleClassname = classNames({

View file

@ -16,7 +16,7 @@ import {
Datatable,
SerializedFieldFormat,
} from '../../../../src/plugins/expressions/public';
import { DragContextState } from './drag_drop';
import { DragContextState, Dragging } from './drag_drop';
import { Document } from './persistence';
import { DateRange } from '../common';
import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public';
@ -217,6 +217,8 @@ export interface DatasourceDataPanelProps<T = unknown> {
query: Query;
dateRange: DateRange;
filters: Filter[];
dropOntoWorkspace: (field: Dragging) => void;
hasSuggestionForField: (field: Dragging) => boolean;
}
interface SharedDimensionProps {