diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
index 32785d2d95753..904c11d2f29de 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
@@ -5,35 +5,18 @@
*/
import React from 'react';
-import { mount, ReactWrapper } from 'enzyme';
+import { ReactWrapper } from 'enzyme';
+import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { EditorFrame } from './editor_frame';
import { Visualization, Datasource, DatasourcePublicAPI } from '../../types';
import { act } from 'react-dom/test-utils';
+import { createMockVisualization, createMockDatasource } from '../mock_extensions';
// calling this function will wait for all pending Promises from mock
// datasources to be processed by its callers.
-const waitForPromises = () => new Promise(resolve => setImmediate(resolve));
+const waitForPromises = () => new Promise(resolve => setTimeout(resolve));
describe('editor_frame', () => {
- const getMockVisualization = () => ({
- getMappingOfTableToRoles: jest.fn(),
- getPersistableState: jest.fn(),
- getSuggestions: jest.fn(),
- initialize: jest.fn(),
- renderConfigPanel: jest.fn(),
- toExpression: jest.fn(),
- });
-
- const getMockDatasource = () => ({
- getDatasourceSuggestionsForField: jest.fn(),
- getDatasourceSuggestionsFromCurrentState: jest.fn(),
- getPersistableState: jest.fn(),
- getPublicAPI: jest.fn(),
- initialize: jest.fn(() => Promise.resolve()),
- renderDataPanel: jest.fn(),
- toExpression: jest.fn(),
- });
-
let mockVisualization: Visualization;
let mockDatasource: Datasource;
@@ -41,11 +24,11 @@ describe('editor_frame', () => {
let mockDatasource2: Datasource;
beforeEach(() => {
- mockVisualization = getMockVisualization();
- mockVisualization2 = getMockVisualization();
+ mockVisualization = createMockVisualization();
+ mockVisualization2 = createMockVisualization();
- mockDatasource = getMockDatasource();
- mockDatasource2 = getMockDatasource();
+ mockDatasource = createMockDatasource();
+ mockDatasource2 = createMockDatasource();
});
describe('initialization', () => {
@@ -439,4 +422,284 @@ describe('editor_frame', () => {
);
});
});
+
+ describe('suggestions', () => {
+ it('should fetch suggestions of currently active datasource', async () => {
+ mount(
+
+ );
+
+ await waitForPromises();
+
+ expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
+ expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled();
+ });
+
+ it('should fetch suggestions of all visualizations', async () => {
+ mount(
+
+ );
+
+ await waitForPromises();
+
+ expect(mockVisualization.getSuggestions).toHaveBeenCalled();
+ expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
+ });
+
+ it('should display suggestions in descending order', async () => {
+ const instance = mount(
+ [
+ {
+ tableIndex: 0,
+ score: 0.5,
+ state: {},
+ title: 'Suggestion2',
+ },
+ {
+ tableIndex: 0,
+ score: 0.8,
+ state: {},
+ title: 'Suggestion1',
+ },
+ ],
+ },
+ testVis2: {
+ ...mockVisualization,
+ getSuggestions: () => [
+ {
+ tableIndex: 0,
+ score: 0.4,
+ state: {},
+ title: 'Suggestion4',
+ },
+ {
+ tableIndex: 0,
+ score: 0.45,
+ state: {},
+ title: 'Suggestion3',
+ },
+ ],
+ },
+ }}
+ datasourceMap={{
+ testDatasource: {
+ ...mockDatasource,
+ getDatasourceSuggestionsFromCurrentState: () => [{ state: {}, tableColumns: [] }],
+ },
+ }}
+ initialDatasourceId="testDatasource"
+ initialVisualizationId="testVis"
+ />
+ );
+
+ await waitForPromises();
+
+ // TODO why is this necessary?
+ instance.update();
+ const suggestions = instance.find('[data-test-subj="suggestion"]');
+ expect(suggestions.map(el => el.text())).toEqual([
+ 'Suggestion1',
+ 'Suggestion2',
+ 'Suggestion3',
+ 'Suggestion4',
+ ]);
+ });
+
+ it('should switch to suggested visualization', async () => {
+ const newDatasourceState = {};
+ const suggestionVisState = {};
+ const instance = mount(
+ [
+ {
+ tableIndex: 0,
+ score: 0.8,
+ state: suggestionVisState,
+ title: 'Suggestion1',
+ },
+ ],
+ },
+ testVis2: mockVisualization2,
+ }}
+ datasourceMap={{
+ testDatasource: {
+ ...mockDatasource,
+ getDatasourceSuggestionsFromCurrentState: () => [
+ { state: newDatasourceState, tableColumns: [] },
+ ],
+ },
+ }}
+ initialDatasourceId="testDatasource"
+ initialVisualizationId="testVis2"
+ />
+ );
+
+ await waitForPromises();
+
+ // TODO why is this necessary?
+ instance.update();
+
+ act(() => {
+ instance.find('[data-test-subj="suggestion"]').simulate('click');
+ });
+
+ expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(1);
+ expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
+ expect.any(Element),
+ expect.objectContaining({
+ state: suggestionVisState,
+ })
+ );
+ expect(mockDatasource.renderDataPanel).toHaveBeenLastCalledWith(
+ expect.any(Element),
+ expect.objectContaining({
+ state: newDatasourceState,
+ })
+ );
+ });
+
+ it('should switch to best suggested visualization on field drop', async () => {
+ const suggestionVisState = {};
+ const instance = mount(
+ [
+ {
+ tableIndex: 0,
+ score: 0.2,
+ state: {},
+ title: 'Suggestion1',
+ },
+ {
+ tableIndex: 0,
+ score: 0.8,
+ state: suggestionVisState,
+ title: 'Suggestion2',
+ },
+ ],
+ },
+ testVis2: mockVisualization2,
+ }}
+ datasourceMap={{
+ testDatasource: {
+ ...mockDatasource,
+ getDatasourceSuggestionsForField: () => [{ state: {}, tableColumns: [] }],
+ getDatasourceSuggestionsFromCurrentState: () => [{ state: {}, tableColumns: [] }],
+ },
+ }}
+ initialDatasourceId="testDatasource"
+ initialVisualizationId="testVis"
+ />
+ );
+
+ await waitForPromises();
+
+ // TODO why is this necessary?
+ instance.update();
+
+ act(() => {
+ instance.find('[data-test-subj="lnsDragDrop"]').simulate('drop');
+ });
+
+ expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith(
+ expect.any(Element),
+ expect.objectContaining({
+ state: suggestionVisState,
+ })
+ );
+ });
+
+ it('should switch to best suggested visualization regardless extension on field drop', async () => {
+ const suggestionVisState = {};
+ const instance = mount(
+ [
+ {
+ tableIndex: 0,
+ score: 0.2,
+ state: {},
+ title: 'Suggestion1',
+ },
+ {
+ tableIndex: 0,
+ score: 0.6,
+ state: {},
+ title: 'Suggestion2',
+ },
+ ],
+ },
+ testVis2: {
+ ...mockVisualization2,
+ getSuggestions: () => [
+ {
+ tableIndex: 0,
+ score: 0.8,
+ state: suggestionVisState,
+ title: 'Suggestion3',
+ },
+ ],
+ },
+ }}
+ datasourceMap={{
+ testDatasource: {
+ ...mockDatasource,
+ getDatasourceSuggestionsForField: () => [{ state: {}, tableColumns: [] }],
+ getDatasourceSuggestionsFromCurrentState: () => [{ state: {}, tableColumns: [] }],
+ },
+ }}
+ initialDatasourceId="testDatasource"
+ initialVisualizationId="testVis"
+ />
+ );
+
+ await waitForPromises();
+
+ // TODO why is this necessary?
+ instance.update();
+
+ act(() => {
+ instance.find('[data-test-subj="lnsDragDrop"]').simulate('drop');
+ });
+
+ expect(mockVisualization2.renderConfigPanel).toHaveBeenCalledWith(
+ expect.any(Element),
+ expect.objectContaining({
+ state: suggestionVisState,
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
index ee5023c06f3f7..fb0bb662915ba 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
@@ -10,6 +10,8 @@ import { reducer, getInitialState } from './state_management';
import { DataPanelWrapper } from './data_panel_wrapper';
import { ConfigPanelWrapper } from './config_panel_wrapper';
import { FrameLayout } from './frame_layout';
+import { SuggestionPanel } from './suggestion_panel';
+import { WorkspacePanel } from './workspace_panel';
export interface EditorFrameProps {
datasourceMap: Record;
@@ -67,6 +69,52 @@ export function EditorFrame(props: EditorFrameProps) {
]
);
+ if (state.datasource.activeId && !state.datasource.isLoading) {
+ return (
+
+ }
+ configPanel={
+
+ }
+ workspacePanel={
+
+ }
+ suggestionsPanel={
+
+ }
+ />
+ );
+ }
+
return (
}
- configPanel={
- state.datasource.activeId &&
- !state.datasource.isLoading && (
-
- )
- }
/>
);
}
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx
index 182d0c9f0b592..f62722bf71b85 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx
@@ -9,7 +9,9 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
export interface FrameLayoutProps {
dataPanel: React.ReactNode;
- configPanel: React.ReactNode;
+ configPanel?: React.ReactNode;
+ suggestionsPanel?: React.ReactNode;
+ workspacePanel?: React.ReactNode;
}
export function FrameLayout(props: FrameLayoutProps) {
@@ -17,7 +19,11 @@ export function FrameLayout(props: FrameLayoutProps) {
{/* TODO style this and add workspace prop and loading flags */}
{props.dataPanel}
- {props.configPanel}
+ {props.workspacePanel}
+
+ {props.configPanel}
+ {props.suggestionsPanel}
+
);
}
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
index 373b321309586..615c9607877ed 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
@@ -117,6 +117,34 @@ describe('editor_frame state management', () => {
expect(newState.visualization.state).toBe(newVisState);
});
+ it('should should switch active visualization and update datasource state', () => {
+ const testVisState = {};
+ const newVisState = {};
+ const newDatasourceState = {};
+ const newState = reducer(
+ {
+ datasource: {
+ activeId: 'testDatasource',
+ state: {},
+ isLoading: false,
+ },
+ visualization: {
+ activeId: 'testVis',
+ state: testVisState,
+ },
+ },
+ {
+ type: 'SWITCH_VISUALIZATION',
+ newVisualizationId: 'testVis2',
+ initialState: newVisState,
+ datasourceState: newDatasourceState,
+ }
+ );
+
+ expect(newState.visualization.state).toBe(newVisState);
+ expect(newState.datasource.state).toBe(newDatasourceState);
+ });
+
it('should should switch active datasource and purge visualization state', () => {
const newState = reducer(
{
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
index 2358da104378b..ec24a0269c58c 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
@@ -31,6 +31,7 @@ export type Action =
type: 'SWITCH_VISUALIZATION';
newVisualizationId: string;
initialState: unknown;
+ datasourceState?: unknown;
}
| {
type: 'SWITCH_DATASOURCE';
@@ -79,6 +80,10 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
activeId: action.newVisualizationId,
state: action.initialState,
},
+ datasource: {
+ ...state.datasource,
+ state: action.datasourceState ? action.datasourceState : state.datasource.state,
+ },
};
case 'UPDATE_DATASOURCE_STATE':
return {
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts
new file mode 100644
index 0000000000000..f79a4b1000991
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts
@@ -0,0 +1,154 @@
+/*
+ * 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 { getSuggestions } from './suggestion_helpers';
+import { createMockVisualization } from '../mock_extensions';
+import { TableColumn } from '../../types';
+
+describe('suggestion helpers', () => {
+ it('should return suggestions array', () => {
+ const mockVisualization = createMockVisualization();
+ const suggestedState = {};
+ const suggestions = getSuggestions(
+ [{ state: {}, tableColumns: [] }],
+ {
+ vis1: {
+ ...mockVisualization,
+ getSuggestions: () => [
+ { tableIndex: 0, score: 0.5, title: 'Test', state: suggestedState },
+ ],
+ },
+ },
+ 'vis1',
+ {}
+ );
+ expect(suggestions.length).toBe(1);
+ expect(suggestions[0].state).toBe(suggestedState);
+ });
+
+ it('should concatenate suggestions from all visualizations', () => {
+ const mockVisualization1 = createMockVisualization();
+ const mockVisualization2 = createMockVisualization();
+ const suggestions = getSuggestions(
+ [{ state: {}, tableColumns: [] }],
+ {
+ vis1: {
+ ...mockVisualization1,
+ getSuggestions: () => [
+ { tableIndex: 0, score: 0.5, title: 'Test', state: {} },
+ { tableIndex: 0, score: 0.5, title: 'Test2', state: {} },
+ ],
+ },
+ vis2: {
+ ...mockVisualization2,
+ getSuggestions: () => [{ tableIndex: 0, score: 0.5, title: 'Test3', state: {} }],
+ },
+ },
+ 'vis1',
+ {}
+ );
+ expect(suggestions.length).toBe(3);
+ });
+
+ it('should rank the visualizations by score', () => {
+ const mockVisualization1 = createMockVisualization();
+ const mockVisualization2 = createMockVisualization();
+ const suggestions = getSuggestions(
+ [{ state: {}, tableColumns: [] }],
+ {
+ vis1: {
+ ...mockVisualization1,
+ getSuggestions: () => [
+ { tableIndex: 0, score: 0.2, title: 'Test', state: {} },
+ { tableIndex: 0, score: 0.8, title: 'Test2', state: {} },
+ ],
+ },
+ vis2: {
+ ...mockVisualization2,
+ getSuggestions: () => [{ tableIndex: 0, score: 0.6, title: 'Test3', state: {} }],
+ },
+ },
+ 'vis1',
+ {}
+ );
+ expect(suggestions[0].score).toBe(0.8);
+ expect(suggestions[1].score).toBe(0.6);
+ expect(suggestions[2].score).toBe(0.2);
+ });
+
+ it('should call all suggestion getters with all available data tables', () => {
+ const mockVisualization1 = createMockVisualization();
+ const mockVisualization2 = createMockVisualization();
+ const table1: TableColumn[] = [];
+ const table2: TableColumn[] = [];
+ getSuggestions(
+ [{ state: {}, tableColumns: table1 }, { state: {}, tableColumns: table2 }],
+ {
+ vis1: mockVisualization1,
+ vis2: mockVisualization2,
+ },
+ 'vis1',
+ {}
+ );
+ expect(mockVisualization1.getSuggestions.mock.calls[0][0].tables[0]).toBe(table1);
+ expect(mockVisualization1.getSuggestions.mock.calls[0][0].tables[1]).toBe(table2);
+ expect(mockVisualization2.getSuggestions.mock.calls[0][0].tables[0]).toBe(table1);
+ expect(mockVisualization2.getSuggestions.mock.calls[0][0].tables[1]).toBe(table2);
+ });
+
+ it('should map the suggestion ids back to the correct datasource states', () => {
+ const mockVisualization1 = createMockVisualization();
+ const mockVisualization2 = createMockVisualization();
+ const tableState1 = {};
+ const tableState2 = {};
+ const suggestions = getSuggestions(
+ [{ state: tableState1, tableColumns: [] }, { state: tableState2, tableColumns: [] }],
+ {
+ vis1: {
+ ...mockVisualization1,
+ getSuggestions: () => [
+ { tableIndex: 0, score: 0.3, title: 'Test', state: {} },
+ { tableIndex: 1, score: 0.2, title: 'Test2', state: {} },
+ ],
+ },
+ vis2: {
+ ...mockVisualization2,
+ getSuggestions: () => [{ tableIndex: 1, score: 0.1, title: 'Test3', state: {} }],
+ },
+ },
+ 'vis1',
+ {}
+ );
+ expect(suggestions[0].datasourceState).toBe(tableState1);
+ expect(suggestions[1].datasourceState).toBe(tableState2);
+ expect(suggestions[1].datasourceState).toBe(tableState2);
+ });
+
+ it('should pass the state of the currently active visualization to getSuggestions', () => {
+ const mockVisualization1 = createMockVisualization();
+ const mockVisualization2 = createMockVisualization();
+ const currentState = {};
+ getSuggestions(
+ [{ state: {}, tableColumns: [] }, { state: {}, tableColumns: [] }],
+ {
+ vis1: mockVisualization1,
+ vis2: mockVisualization2,
+ },
+ 'vis1',
+ currentState
+ );
+ expect(mockVisualization1.getSuggestions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ state: currentState,
+ })
+ );
+ expect(mockVisualization2.getSuggestions).not.toHaveBeenCalledWith(
+ expect.objectContaining({
+ state: currentState,
+ })
+ );
+ });
+});
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts
new file mode 100644
index 0000000000000..c6f5d4d4538f3
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { Visualization, DatasourceSuggestion } from '../../types';
+import { Action } from './state_management';
+
+export interface Suggestion {
+ visualizationId: string;
+ datasourceState: unknown;
+ score: number;
+ title: string;
+ state: unknown;
+}
+
+/**
+ * This function takes a list of available data tables and a list of visualization
+ * extensions and creates a ranked list of suggestions which contain a pair of a data table
+ * and a visualization.
+ *
+ * Each suggestion represents a valid state of the editor and can be applied by creating an
+ * action with `toSwitchAction` and dispatching it
+ */
+export function getSuggestions(
+ datasourceTableSuggestions: DatasourceSuggestion[],
+ visualizationMap: Record,
+ activeVisualizationId: string | null,
+ visualizationState: unknown
+): Suggestion[] {
+ const datasourceTables = datasourceTableSuggestions.map(({ tableColumns }) => tableColumns);
+
+ return (
+ Object.entries(visualizationMap)
+ .map(([visualizationId, visualization]) => {
+ return visualization
+ .getSuggestions({
+ tables: datasourceTables,
+ state: visualizationId === activeVisualizationId ? visualizationState : undefined,
+ })
+ .map(({ tableIndex: datasourceSuggestionId, ...suggestion }) => ({
+ ...suggestion,
+ visualizationId,
+ datasourceState: datasourceTableSuggestions[datasourceSuggestionId].state,
+ }));
+ })
+ // TODO why is flatMap not available here?
+ .reduce((globalList, currentList) => [...globalList, ...currentList], [])
+ .sort(({ score: scoreA }, { score: scoreB }) => scoreB - scoreA)
+ );
+}
+
+export function toSwitchAction(suggestion: Suggestion): Action {
+ return {
+ type: 'SWITCH_VISUALIZATION',
+ newVisualizationId: suggestion.visualizationId,
+ initialState: suggestion.state,
+ datasourceState: suggestion.datasourceState,
+ };
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx
new file mode 100644
index 0000000000000..9d9730db37651
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n/react';
+
+import { Action } from './state_management';
+import { Datasource, Visualization } from '../../types';
+import { getSuggestions, toSwitchAction } from './suggestion_helpers';
+
+export interface SuggestionPanelProps {
+ activeDatasource: Datasource;
+ datasourceState: unknown;
+ activeVisualizationId: string | null;
+ visualizationMap: Record;
+ visualizationState: unknown;
+ dispatch: (action: Action) => void;
+}
+
+export function SuggestionPanel({
+ activeDatasource,
+ datasourceState,
+ activeVisualizationId,
+ visualizationMap,
+ visualizationState,
+ dispatch,
+}: SuggestionPanelProps) {
+ const datasourceSuggestions = activeDatasource.getDatasourceSuggestionsFromCurrentState(
+ datasourceState
+ );
+
+ const suggestions = getSuggestions(
+ datasourceSuggestions,
+ visualizationMap,
+ activeVisualizationId,
+ visualizationState
+ );
+
+ return (
+ <>
+
+
+
+ {suggestions.map((suggestion, index) => {
+ return (
+
+ );
+ })}
+ >
+ );
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx
new file mode 100644
index 0000000000000..761b77757df62
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n/react';
+
+import { Action } from './state_management';
+import { Datasource, Visualization, DatasourcePublicAPI } from '../../types';
+import { DragDrop } from '../../drag_drop';
+import { getSuggestions, toSwitchAction } from './suggestion_helpers';
+
+export interface WorkspacePanelProps {
+ activeDatasource: Datasource;
+ datasourceState: unknown;
+ activeVisualizationId: string | null;
+ visualizationMap: Record;
+ visualizationState: unknown;
+ datasourcePublicAPI: DatasourcePublicAPI;
+ dispatch: (action: Action) => void;
+}
+
+interface ExpressionRendererProps {
+ expression: string;
+}
+
+function ExpressionRenderer(props: ExpressionRendererProps) {
+ // TODO: actually render the expression and move this to a generic folder as it can be re-used for
+ // suggestion rendering
+ return {props.expression};
+}
+
+export function WorkspacePanel({
+ activeDatasource,
+ activeVisualizationId,
+ datasourceState,
+ visualizationMap,
+ visualizationState,
+ datasourcePublicAPI,
+ dispatch,
+}: WorkspacePanelProps) {
+ function onDrop() {
+ const datasourceSuggestions = activeDatasource.getDatasourceSuggestionsForField(
+ datasourceState
+ );
+
+ const suggestions = getSuggestions(
+ datasourceSuggestions,
+ visualizationMap,
+ activeVisualizationId,
+ visualizationState
+ );
+
+ if (suggestions.length === 0) {
+ // TODO specify and implement behavior in case of no valid suggestions
+ return;
+ }
+
+ const suggestion = suggestions[0];
+
+ // TODO heuristically present the suggestions in a modal instead of just picking the first one
+ dispatch(toSwitchAction(suggestion));
+ }
+
+ function renderEmptyWorkspace() {
+ return (
+
+
+
+ );
+ }
+
+ function renderVisualization() {
+ if (activeVisualizationId === null) {
+ return renderEmptyWorkspace();
+ }
+
+ const activeVisualization = visualizationMap[activeVisualizationId];
+ const datasourceExpression = activeDatasource.toExpression(datasourceState);
+ const visualizationExpression = activeVisualization.toExpression(
+ visualizationState,
+ datasourcePublicAPI
+ );
+ const expression = `${datasourceExpression} | ${visualizationExpression}`;
+
+ return ;
+ }
+
+ return (
+
+ {renderVisualization()}
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/mock_extensions.ts b/x-pack/plugins/lens/public/editor_frame_plugin/mock_extensions.ts
new file mode 100644
index 0000000000000..45162d7c07960
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/mock_extensions.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { DatasourcePublicAPI, Visualization, Datasource } from '../types';
+
+export function createMockVisualization(): jest.Mocked {
+ return {
+ getPersistableState: jest.fn(_state => ({})),
+ getSuggestions: jest.fn(_options => []),
+ initialize: jest.fn(_state => ({})),
+ renderConfigPanel: jest.fn(),
+ toExpression: jest.fn((_state, _datasource) => ''),
+ };
+}
+
+export type DatasourceMock = jest.Mocked & {
+ publicAPIMock: jest.Mocked;
+};
+
+export function createMockDatasource(): DatasourceMock {
+ const publicAPIMock: jest.Mocked = {
+ getTableSpec: jest.fn(() => []),
+ getOperationForColumnId: jest.fn(),
+ renderDimensionPanel: jest.fn(),
+ removeColumnInTableSpec: jest.fn(),
+ moveColumnTo: jest.fn(),
+ duplicateColumn: jest.fn(),
+ };
+
+ return {
+ getDatasourceSuggestionsForField: jest.fn(_state => []),
+ getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []),
+ getPersistableState: jest.fn(),
+ getPublicAPI: jest.fn((_state, _setState) => publicAPIMock),
+ initialize: jest.fn(_state => Promise.resolve()),
+ renderDataPanel: jest.fn(),
+ toExpression: jest.fn(_state => ''),
+
+ // 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,
+ };
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx
index 1ca641b2e6e37..1d81f315bf525 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/plugin.test.tsx
@@ -5,9 +5,11 @@
*/
import { EditorFramePlugin } from './plugin';
-import { Visualization, Datasource } from '../types';
+import { createMockDatasource, createMockVisualization } from './mock_extensions';
-const nextTick = () => new Promise(resolve => setTimeout(resolve));
+// calling this function will wait for all pending Promises from mock
+// datasources to be processed by its callers.
+const waitForPromises = () => new Promise(resolve => setTimeout(resolve));
describe('editor_frame plugin', () => {
let pluginInstance: EditorFramePlugin;
@@ -51,23 +53,14 @@ describe('editor_frame plugin', () => {
});
it('should initialize and render provided datasource', async () => {
+ const mockDatasource = createMockDatasource();
const publicAPI = pluginInstance.setup();
- const mockDatasource = {
- getDatasourceSuggestionsForField: jest.fn(),
- getDatasourceSuggestionsFromCurrentState: jest.fn(),
- getPersistableState: jest.fn(),
- getPublicAPI: jest.fn(),
- initialize: jest.fn(() => Promise.resolve()),
- renderDataPanel: jest.fn(),
- toExpression: jest.fn(),
- };
-
publicAPI.registerDatasource('test', mockDatasource);
const instance = publicAPI.createInstance({});
instance.mount(mountpoint);
- await nextTick();
+ await waitForPromises();
expect(mockDatasource.initialize).toHaveBeenCalled();
expect(mockDatasource.renderDataPanel).toHaveBeenCalled();
@@ -76,25 +69,9 @@ describe('editor_frame plugin', () => {
});
it('should initialize visualization and render config panel', async () => {
+ const mockDatasource = createMockDatasource();
+ const mockVisualization = createMockVisualization();
const publicAPI = pluginInstance.setup();
- const mockDatasource: Datasource = {
- getDatasourceSuggestionsForField: jest.fn(),
- getDatasourceSuggestionsFromCurrentState: jest.fn(),
- getPersistableState: jest.fn(),
- getPublicAPI: jest.fn(),
- initialize: jest.fn(() => Promise.resolve()),
- renderDataPanel: jest.fn(),
- toExpression: jest.fn(),
- };
-
- const mockVisualization: Visualization = {
- getMappingOfTableToRoles: jest.fn(),
- getPersistableState: jest.fn(),
- getSuggestions: jest.fn(),
- initialize: jest.fn(),
- renderConfigPanel: jest.fn(),
- toExpression: jest.fn(),
- };
publicAPI.registerDatasource('test', mockDatasource);
publicAPI.registerVisualization('test', mockVisualization);
@@ -102,7 +79,7 @@ describe('editor_frame plugin', () => {
const instance = publicAPI.createInstance({});
instance.mount(mountpoint);
- await nextTick();
+ await waitForPromises();
expect(mockVisualization.initialize).toHaveBeenCalled();
expect(mockVisualization.renderConfigPanel).toHaveBeenCalled();
diff --git a/x-pack/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/plugins/lens/public/editor_frame_plugin/plugin.tsx
index 07c1841601140..7c12d95cb69c1 100644
--- a/x-pack/plugins/lens/public/editor_frame_plugin/plugin.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_plugin/plugin.tsx
@@ -6,8 +6,9 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
-import { Datasource, Visualization, EditorFrameSetup, EditorFrameInstance } from '../types';
+import { I18nProvider } from '@kbn/i18n/react';
+import { Datasource, Visualization, EditorFrameSetup, EditorFrameInstance } from '../types';
import { EditorFrame } from './editor_frame';
export class EditorFramePlugin {
@@ -29,13 +30,19 @@ export class EditorFramePlugin {
mount: element => {
unmount();
domElement = element;
+
+ const firstDatasourceId = Object.keys(this.datasources)[0];
+ const firstVisualizationId = Object.keys(this.visualizations)[0];
+
render(
- ,
+
+
+ ,
domElement
);
},
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index a0328cb3bd988..4e2c623416c9a 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -21,24 +21,14 @@ export interface EditorFrameSetup {
// Hints the default nesting to the data source. 0 is the highest priority
export type DimensionPriority = 0 | 1 | 2;
-// For switching between visualizations and correctly matching columns
-export type DimensionRole =
- | 'splitChart'
- | 'series'
- | 'primary'
- | 'secondary'
- | 'color'
- | 'size'
- | string; // Some visualizations will use custom names that have other meaning
-
-export interface TableColumns {
+export interface TableColumn {
columnId: string;
operation: Operation;
}
export interface DatasourceSuggestion {
state: T;
- tableColumns: TableColumns[];
+ tableColumns: TableColumn[];
}
/**
@@ -131,10 +121,8 @@ export interface VisualizationProps {
}
export interface SuggestionRequest {
- // Roles currently being used
- roles: DimensionRole[];
// It is up to the Visualization to rank these tables
- tableColumns: { [datasourceSuggestionId: string]: TableColumns };
+ tables: TableColumn[][];
state?: T; // State is only passed if the visualization is active
}
@@ -142,7 +130,7 @@ export interface VisualizationSuggestion {
score: number;
title: string;
state: T;
- datasourceSuggestionId: string;
+ tableIndex: number;
}
export interface Visualization {
@@ -155,9 +143,6 @@ export interface Visualization {
toExpression: (state: T, datasource: DatasourcePublicAPI) => string;
- // Frame will request the list of roles currently being used when calling `getInitialStateFromOtherVisualization`
- getMappingOfTableToRoles: (state: T, datasource: DatasourcePublicAPI) => DimensionRole[];
-
// The frame will call this function on all visualizations when the table changes, or when
// rendering additional ways of using the data
getSuggestions: (options: SuggestionRequest) => Array>;
diff --git a/x-pack/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
index 7ec2994161216..c5d69175e90cd 100644
--- a/x-pack/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
@@ -6,10 +6,10 @@
import React from 'react';
import { render } from 'react-dom';
-import { Visualization, DimensionRole } from '../types';
+import { Visualization } from '../types';
export interface XyVisualizationState {
- roles: DimensionRole[];
+ roles: string[];
}
export type XyVisualizationPersistedState = XyVisualizationState;
@@ -31,7 +31,5 @@ export const xyVisualization: Visualization [],
- getMappingOfTableToRoles: (state, datasource) => [],
-
toExpression: state => '',
};