From d8ed451128bc5568afbc6b51556db3f4a235405b Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 6 Oct 2020 11:15:41 +0300 Subject: [PATCH] [Lens] Navigate from discover to lens (#77873) * Create lens action and unregister the visualize one * remove console * Implement Discover to Lens, wip, missing tests * Add unit tests * fix embed lens to empty dashboard functional tests * fix suggestions on save * Fix bug on save button, query and filters should be transferred from discover * Add functional test for the navigation from Discover to Lens * PR update after code review * unregister visualize action only if the action exists * Change the test to not be flaky * Move suggestions to editor frame and hide the emptyWorkspace for visualize field * Update ui actions docs * Add a retry to remove test flakiness * Fix bug of infinite loader when removing the y axis dimension Co-authored-by: Elastic Machine Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md # docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md # src/plugins/ui_actions/public/public.api.md --- ...ions-public.action_visualize_lens_field.md | 11 +++ ...textmapping.action_visualize_lens_field.md | 11 +++ src/plugins/ui_actions/public/index.ts | 1 + src/plugins/ui_actions/public/types.ts | 2 + .../public/actions/visualize_field_action.ts | 1 + x-pack/plugins/lens/kibana.json | 5 +- .../lens/public/app_plugin/app.test.tsx | 1 + x-pack/plugins/lens/public/app_plugin/app.tsx | 12 ++- .../lens/public/app_plugin/mounter.tsx | 9 ++- .../plugins/lens/public/app_plugin/types.ts | 10 +++ .../editor_frame/editor_frame.test.tsx | 33 +++++++- .../editor_frame/editor_frame.tsx | 34 ++++++++- .../editor_frame/state_helpers.ts | 6 +- .../editor_frame/suggestion_helpers.test.ts | 69 +++++++++++++++++ .../editor_frame/suggestion_helpers.ts | 61 ++++++++++++++- .../workspace_panel/workspace_panel.tsx | 11 ++- .../public/editor_frame_service/mocks.tsx | 1 + .../editor_frame_service/service.test.tsx | 4 + .../public/editor_frame_service/service.tsx | 13 +++- .../indexpattern_datasource/indexpattern.tsx | 7 +- .../indexpattern_suggestions.test.tsx | 65 ++++++++++++++++ .../indexpattern_suggestions.ts | 20 ++++- .../indexpattern_datasource/loader.test.ts | 28 +++++++ .../public/indexpattern_datasource/loader.ts | 7 +- x-pack/plugins/lens/public/plugin.ts | 15 +++- .../visualize_field_actions.ts | 28 +++++++ x-pack/plugins/lens/public/types.ts | 13 +++- x-pack/test/functional/apps/discover/index.ts | 1 + .../apps/discover/visualize_field.ts | 76 +++++++++++++++++++ 29 files changed, 529 insertions(+), 26 deletions(-) create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md create mode 100644 x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts create mode 100644 x-pack/test/functional/apps/discover/visualize_field.ts diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md new file mode 100644 index 00000000000000..b00618f5105105 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md) + +## ACTION\_VISUALIZE\_LENS\_FIELD variable + +Signature: + +```typescript +ACTION_VISUALIZE_LENS_FIELD = "ACTION_VISUALIZE_LENS_FIELD" +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md new file mode 100644 index 00000000000000..96370a07806d32 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) > [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md) + +## ActionContextMapping.ACTION\_VISUALIZE\_LENS\_FIELD property + +Signature: + +```typescript +[ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; +``` diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 4b2d6cae1c8e15..b9f4a4a0426bff 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -59,6 +59,7 @@ export { VisualizeFieldContext, ACTION_VISUALIZE_FIELD, ACTION_VISUALIZE_GEO_FIELD, + ACTION_VISUALIZE_LENS_FIELD, } from './types'; export { ActionByType, diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index b00f4628ffb967..0be3c19fc1c4d1 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -57,10 +57,12 @@ export interface TriggerContextMapping { const DEFAULT_ACTION = ''; export const ACTION_VISUALIZE_FIELD = 'ACTION_VISUALIZE_FIELD'; export const ACTION_VISUALIZE_GEO_FIELD = 'ACTION_VISUALIZE_GEO_FIELD'; +export const ACTION_VISUALIZE_LENS_FIELD = 'ACTION_VISUALIZE_LENS_FIELD'; export type ActionType = keyof ActionContextMapping; export interface ActionContextMapping { [DEFAULT_ACTION]: BaseContext; [ACTION_VISUALIZE_FIELD]: VisualizeFieldContext; [ACTION_VISUALIZE_GEO_FIELD]: VisualizeFieldContext; + [ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; } diff --git a/src/plugins/visualize/public/actions/visualize_field_action.ts b/src/plugins/visualize/public/actions/visualize_field_action.ts index 6671d2c9819106..e570ed5e49e6a7 100644 --- a/src/plugins/visualize/public/actions/visualize_field_action.ts +++ b/src/plugins/visualize/public/actions/visualize_field_action.ts @@ -34,6 +34,7 @@ import { AGGS_TERMS_SIZE_SETTING } from '../../common/constants'; export const visualizeFieldAction = createAction({ type: ACTION_VISUALIZE_FIELD, + id: ACTION_VISUALIZE_FIELD, getDisplayName: () => i18n.translate('visualize.discover.visualizeFieldLabel', { defaultMessage: 'Visualize field', diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index f5fba766e60eea..46a0b56a03ec57 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -11,9 +11,10 @@ "urlForwarding", "visualizations", "dashboard", - "charts" + "charts", + "uiActions" ], - "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions", "globalSearch"], + "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "globalSearch"], "configPath": ["xpack", "lens"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable"] diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 24114e2b315181..ee92c52a3cc768 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -280,6 +280,7 @@ describe('Lens App', () => { }, "doc": undefined, "filters": Array [], + "initialContext": undefined, "onChange": [Function], "onError": [Function], "query": Object { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index e4af2a33ec68be..3407ea5de49c46 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -47,6 +47,7 @@ export function App({ incomingState, redirectToOrigin, setHeaderActionMenu, + initialContext, }: LensAppProps) { const { data, @@ -67,7 +68,7 @@ export function App({ const [state, setState] = useState(() => { const currentRange = data.query.timefilter.timefilter.getTime(); return { - query: data.query.queryString.getDefaultQuery(), + query: data.query.queryString.getQuery(), filters: data.query.filterManager.getFilters(), isLoading: Boolean(initialInput), indexPatternsForTopNav: [], @@ -142,8 +143,11 @@ export function App({ useEffect(() => { // Clear app-specific filters when navigating to Lens. Necessary because Lens - // can be loaded without a full page refresh - data.query.filterManager.setAppFilters([]); + // can be loaded without a full page refresh. If the user navigates to Lens from Discover + // we keep the filters + if (!initialContext) { + data.query.filterManager.setAppFilters([]); + } const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { @@ -187,6 +191,7 @@ export function App({ uiSettings, data.query, history, + initialContext, ]); useEffect(() => { @@ -576,6 +581,7 @@ export function App({ doc: state.persistedDoc, onError, showNoDataPopover, + initialContext, onChange: ({ filterableIndexPatterns, doc, isSaveable }) => { if (isSaveable !== state.isSaveable) { setState((s) => ({ ...s, isSaveable })); diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 0d50e541d3e48e..90ba74decfd809 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -26,8 +26,9 @@ import { LensByReferenceInput, LensByValueInput, } from '../editor_frame_service/embeddable/embeddable'; +import { ACTION_VISUALIZE_LENS_FIELD } from '../../../../../src/plugins/ui_actions/public'; import { LensAttributeService } from '../lens_attribute_service'; -import { LensAppServices, RedirectToOriginProps } from './types'; +import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; export async function mountApp( @@ -46,6 +47,7 @@ export async function mountApp( const instance = await createEditorFrame(); const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(params.history); + const historyLocationState = params.history.location.state as HistoryLocationState; const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); const lensServices: LensAppServices = { @@ -132,6 +134,11 @@ export async function mountApp( onAppLeave={params.onAppLeave} setHeaderActionMenu={params.setHeaderActionMenu} history={routeProps.history} + initialContext={ + historyLocationState && historyLocationState.type === ACTION_VISUALIZE_LENS_FIELD + ? historyLocationState.payload + : undefined + } /> ); }; diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index fcdd0b20f8d276..bd5a9b5a8ed0a0 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -28,6 +28,10 @@ import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigati import { LensAttributeService } from '../lens_attribute_service'; import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; import { DashboardFeatureFlagConfig } from '../../../../../src/plugins/dashboard/public'; +import { + VisualizeFieldContext, + ACTION_VISUALIZE_LENS_FIELD, +} from '../../../../../src/plugins/ui_actions/public'; import { EmbeddableEditorState } from '../../../../../src/plugins/embeddable/public'; import { EditorFrameInstance } from '..'; @@ -75,6 +79,12 @@ export interface LensAppProps { // State passed in by the container which is used to determine the id of the Originating App. incomingState?: EmbeddableEditorState; + initialContext?: VisualizeFieldContext; +} + +export interface HistoryLocationState { + type: typeof ACTION_VISUALIZE_LENS_FIELD; + payload: VisualizeFieldContext; } export interface LensAppServices { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index e628ea0675a8d0..7328cdaf6fc9bb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -184,8 +184,8 @@ describe('editor_frame', () => { /> ); }); - expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, []); - expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, []); + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, [], undefined); + expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, [], undefined); expect(mockDatasource3.initialize).not.toHaveBeenCalled(); }); @@ -972,6 +972,32 @@ describe('editor_frame', () => { }); describe('suggestions', () => { + it('should fetch suggestions of currently active datasource when initializes from visualization trigger', async () => { + await act(async () => { + mount( + + ); + }); + + expect(mockDatasource.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalled(); + }); + it('should fetch suggestions of currently active datasource', async () => { await act(async () => { mount( @@ -1208,6 +1234,7 @@ describe('editor_frame', () => { ...mockDatasource, getDatasourceSuggestionsForField: () => [generateSuggestion()], getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], }, }} initialDatasourceId="testDatasource" @@ -1274,6 +1301,7 @@ describe('editor_frame', () => { ...mockDatasource, getDatasourceSuggestionsForField: () => [generateSuggestion()], getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { if (dragging !== 'draggedField') { setDragging('draggedField'); @@ -1370,6 +1398,7 @@ describe('editor_frame', () => { ...mockDatasource, getDatasourceSuggestionsForField: () => [generateSuggestion()], getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { if (dragging !== 'draggedField') { setDragging('draggedField'); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 72ad8e074226cc..32fd4461dfc8bc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useReducer } from 'react'; +import React, { useEffect, useReducer, useState } from 'react'; import { CoreSetup, CoreStart } from 'kibana/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Datasource, FramePublicAPI, Visualization } from '../../types'; @@ -19,8 +19,10 @@ import { 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'; export interface EditorFrameProps { doc?: Document; @@ -45,10 +47,14 @@ export interface EditorFrameProps { isSaveable: boolean; }) => void; showNoDataPopover: () => void; + initialContext?: VisualizeFieldContext; } export function EditorFrame(props: EditorFrameProps) { const [state, dispatch] = useReducer(reducer, props, getInitialState); + const [visualizeTriggerFieldContext, setVisualizeTriggerFieldContext] = useState( + props.initialContext + ); const { onError } = props; const activeVisualization = state.visualization.activeId && props.visualizationMap[state.visualization.activeId]; @@ -63,7 +69,12 @@ export function EditorFrame(props: EditorFrameProps) { // prevents executing dispatch on unmounted component let isUnmounted = false; if (!allLoaded) { - initializeDatasources(props.datasourceMap, state.datasourceStates, props.doc?.references) + initializeDatasources( + props.datasourceMap, + state.datasourceStates, + props.doc?.references, + visualizeTriggerFieldContext + ) .then((result) => { if (!isUnmounted) { Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => { @@ -84,7 +95,6 @@ export function EditorFrame(props: EditorFrameProps) { // eslint-disable-next-line react-hooks/exhaustive-deps [allLoaded, onError] ); - const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates); const framePublicAPI: FramePublicAPI = { @@ -180,6 +190,23 @@ export function EditorFrame(props: EditorFrameProps) { [allLoaded, activeVisualization, state.visualization.state] ); + // Get suggestions for visualize field when all datasources are ready + useEffect(() => { + if (allLoaded && visualizeTriggerFieldContext && !props.doc) { + applyVisualizeFieldSuggestions({ + datasourceMap: props.datasourceMap, + datasourceStates: state.datasourceStates, + visualizationMap: props.visualizationMap, + activeVisualizationId: state.visualization.activeId, + visualizationState: state.visualization.state, + visualizeTriggerFieldContext, + dispatch, + }); + setVisualizeTriggerFieldContext(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allLoaded]); + // The frame needs to call onChange every time its internal state changes useEffect( () => { @@ -275,6 +302,7 @@ export function EditorFrame(props: EditorFrameProps) { ExpressionRenderer={props.ExpressionRenderer} core={props.core} plugins={props.plugins} + visualizeTriggerFieldContext={visualizeTriggerFieldContext} /> ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 1fe5224d0b1b4e..8b0334ab98c146 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -9,18 +9,20 @@ import { Ast } from '@kbn/interpreter/common'; import { Datasource, DatasourcePublicAPI, Visualization } from '../../types'; import { buildExpression } from './expression_helpers'; import { Document } from '../../persistence/saved_object_store'; +import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; export async function initializeDatasources( datasourceMap: Record, datasourceStates: Record, - references?: SavedObjectReference[] + references?: SavedObjectReference[], + initialContext?: VisualizeFieldContext ) { const states: Record = {}; await Promise.all( Object.entries(datasourceMap).map(([datasourceId, datasource]) => { if (datasourceStates[datasourceId]) { return datasource - .initialize(datasourceStates[datasourceId].state || undefined, references) + .initialize(datasourceStates[datasourceId].state || undefined, references, initialContext) .then((datasourceState) => { states[datasourceId] = { isLoading: false, state: datasourceState }; }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 63b8b1f0482968..c5c66c1c820e86 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -173,6 +173,75 @@ describe('suggestion helpers', () => { expect(multiDatasourceMap.mock3.getDatasourceSuggestionsForField).not.toHaveBeenCalled(); }); + it('should call getDatasourceSuggestionsForVisualizeField when a visualizeTriggerField is passed', () => { + datasourceMap.mock.getDatasourceSuggestionsForVisualizeField.mockReturnValue([ + generateSuggestion(), + ]); + getSuggestions({ + visualizationMap: { + vis1: createMockVisualization(), + }, + activeVisualizationId: 'vis1', + visualizationState: {}, + datasourceMap, + datasourceStates, + visualizeTriggerFieldContext: { + indexPatternId: '1', + fieldName: 'test', + }, + }); + expect(datasourceMap.mock.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( + datasourceStates.mock.state, + '1', + 'test' + ); + }); + + it('should call getDatasourceSuggestionsForVisualizeField from all datasources with a state', () => { + const multiDatasourceStates = { + mock: { + isLoading: false, + state: {}, + }, + mock2: { + isLoading: false, + state: {}, + }, + }; + const multiDatasourceMap = { + mock: createMockDatasource('a'), + mock2: createMockDatasource('a'), + mock3: createMockDatasource('a'), + }; + const visualizeTriggerField = { + indexPatternId: '1', + fieldName: 'test', + }; + getSuggestions({ + visualizationMap: { + vis1: createMockVisualization(), + }, + activeVisualizationId: 'vis1', + visualizationState: {}, + datasourceMap: multiDatasourceMap, + datasourceStates: multiDatasourceStates, + visualizeTriggerFieldContext: visualizeTriggerField, + }); + expect(multiDatasourceMap.mock.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( + multiDatasourceStates.mock.state, + '1', + 'test' + ); + expect(multiDatasourceMap.mock2.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( + multiDatasourceStates.mock2.state, + '1', + 'test' + ); + expect( + multiDatasourceMap.mock3.getDatasourceSuggestionsForVisualizeField + ).not.toHaveBeenCalled(); + }); + it('should rank the visualizations by score', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 2bb1baf9d54f25..c4a92dde6187ce 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -7,6 +7,7 @@ import _ from 'lodash'; import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; +import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; import { Visualization, Datasource, @@ -47,6 +48,7 @@ export function getSuggestions({ subVisualizationId, visualizationState, field, + visualizeTriggerFieldContext, }: { datasourceMap: Record; datasourceStates: Record< @@ -61,6 +63,7 @@ export function getSuggestions({ subVisualizationId?: string; visualizationState: unknown; field?: unknown; + visualizeTriggerFieldContext?: VisualizeFieldContext; }): Suggestion[] { const datasources = Object.entries(datasourceMap).filter( ([datasourceId]) => datasourceStates[datasourceId] && !datasourceStates[datasourceId].isLoading @@ -70,10 +73,21 @@ export function getSuggestions({ const datasourceTableSuggestions = _.flatten( datasources.map(([datasourceId, datasource]) => { const datasourceState = datasourceStates[datasourceId].state; - return (field - ? datasource.getDatasourceSuggestionsForField(datasourceState, field) - : datasource.getDatasourceSuggestionsFromCurrentState(datasourceState) - ).map((suggestion) => ({ ...suggestion, datasourceId })); + let dataSourceSuggestions; + if (visualizeTriggerFieldContext) { + dataSourceSuggestions = datasource.getDatasourceSuggestionsForVisualizeField( + datasourceState, + visualizeTriggerFieldContext.indexPatternId, + visualizeTriggerFieldContext.fieldName + ); + } else if (field) { + dataSourceSuggestions = datasource.getDatasourceSuggestionsForField(datasourceState, field); + } else { + dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState( + datasourceState + ); + } + return dataSourceSuggestions.map((suggestion) => ({ ...suggestion, datasourceId })); }) ); @@ -100,6 +114,45 @@ export function getSuggestions({ ).sort((a, b) => b.score - a.score); } +export function applyVisualizeFieldSuggestions({ + datasourceMap, + datasourceStates, + visualizationMap, + activeVisualizationId, + visualizationState, + visualizeTriggerFieldContext, + dispatch, +}: { + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + visualizationMap: Record; + activeVisualizationId: string | null; + subVisualizationId?: string; + visualizationState: unknown; + visualizeTriggerFieldContext?: VisualizeFieldContext; + dispatch: (action: Action) => void; +}): void { + const suggestions = getSuggestions({ + datasourceMap, + datasourceStates, + visualizationMap, + activeVisualizationId, + visualizationState, + visualizeTriggerFieldContext, + }); + if (suggestions.length) { + const selectedSuggestion = + suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0]; + switchToSuggestion(dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION'); + } +} + /** * Queries a single visualization extensions for a single datasource suggestion and * creates an array of complete suggestions containing both the target datasource diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 3993b4ffc02b0c..34979083645c36 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -28,7 +28,10 @@ import { getSuggestions, switchToSuggestion } from '../suggestion_helpers'; import { buildExpression } from '../expression_helpers'; import { debouncedComponent } from '../../../debounced_component'; import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; +import { + UiActionsStart, + VisualizeFieldContext, +} from '../../../../../../../src/plugins/ui_actions/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; @@ -53,6 +56,7 @@ export interface WorkspacePanelProps { core: CoreStart | CoreSetup; plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; title?: string; + visualizeTriggerFieldContext?: VisualizeFieldContext; } export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel); @@ -71,6 +75,7 @@ export function InnerWorkspacePanel({ plugins, ExpressionRenderer: ExpressionRendererComponent, title, + visualizeTriggerFieldContext, }: WorkspacePanelProps) { const dragDropContext = useContext(DragContext); @@ -245,7 +250,9 @@ export function InnerWorkspacePanel({ } function renderVisualization() { - if (expression === null) { + // we don't want to render the emptyWorkspace on visualizing field from Discover + // as it is specific for the drag and drop functionality and can confuse the users + if (expression === null && !visualizeTriggerFieldContext) { return renderEmptyWorkspace(); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 86b137851d9bdd..93898ef1d43a87 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -69,6 +69,7 @@ export function createMockDatasource(id: string): DatasourceMock { id: 'mockindexpattern', clearLayer: jest.fn((state, _layerId) => state), getDatasourceSuggestionsForField: jest.fn((_state, _item) => []), + getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })), getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index c1b6d74bb49c00..e9f8013ef7e2d1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -53,6 +53,10 @@ describe('editor_frame service', () => { query: { query: '', language: 'lucene' }, filters: [], showNoDataPopover: jest.fn(), + initialContext: { + indexPatternId: '1', + fieldName: 'test', + }, }); instance.unmount(); })() diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index e6d7f78f5ad071..54250c3bd9300b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -127,7 +127,17 @@ export class EditorFrameService { return { mount: async ( element, - { doc, onError, dateRange, query, filters, savedQuery, onChange, showNoDataPopover } + { + doc, + onError, + dateRange, + query, + filters, + savedQuery, + onChange, + showNoDataPopover, + initialContext, + } ) => { domElement = element; const firstDatasourceId = Object.keys(resolvedDatasources)[0]; @@ -156,6 +166,7 @@ export class EditorFrameService { savedQuery={savedQuery} onChange={onChange} showNoDataPopover={showNoDataPopover} + initialContext={initialContext} /> , domElement diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 7f7eb0bc0fdac3..28aeac223e4a6d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -36,6 +36,7 @@ import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, getDatasourceSuggestionsFromCurrentState, + getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; import { isDraggedField, normalizeOperationDataType } from './utils'; @@ -49,6 +50,7 @@ import { } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '../index'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; @@ -134,7 +136,8 @@ export function getIndexPatternDatasource({ async initialize( persistedState?: IndexPatternPersistedState, - references?: SavedObjectReference[] + references?: SavedObjectReference[], + initialContext?: VisualizeFieldContext ) { return loadInitialState({ persistedState, @@ -143,6 +146,7 @@ export function getIndexPatternDatasource({ defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, indexPatternsService, + initialContext, }); }, @@ -335,6 +339,7 @@ export function getIndexPatternDatasource({ : []; }, getDatasourceSuggestionsFromCurrentState, + getDatasourceSuggestionsForVisualizeField, }; return indexPatternDatasource; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 80765627c1fc29..a480cfe408982b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -10,6 +10,7 @@ import { IndexPatternPrivateState } from './types'; import { getDatasourceSuggestionsForField, getDatasourceSuggestionsFromCurrentState, + getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; jest.mock('./loader'); @@ -1077,6 +1078,70 @@ describe('IndexPattern Data Source suggestions', () => { }); }); }); + describe('#getDatasourceSuggestionsForVisualizeField', () => { + describe('with no layer', () => { + function stateWithoutLayer() { + return { + ...testInitialState(), + layers: {}, + }; + } + + it('should return an empty array if the field does not exist', () => { + const suggestions = getDatasourceSuggestionsForVisualizeField( + stateWithoutLayer(), + '1', + 'field_not_exist' + ); + + expect(suggestions).toEqual([]); + }); + + it('should apply a bucketed aggregation for a string field', () => { + const suggestions = getDatasourceSuggestionsForVisualizeField( + stateWithoutLayer(), + '1', + 'source' + ); + + expect(suggestions).toContainEqual( + expect.objectContaining({ + state: expect.objectContaining({ + layers: { + id1: expect.objectContaining({ + columnOrder: ['id2', 'id3'], + columns: { + id2: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + params: expect.objectContaining({ size: 5 }), + }), + id3: expect.objectContaining({ + operationType: 'count', + }), + }, + }), + }, + }), + table: { + changeType: 'initial', + label: undefined, + isMultiRow: true, + columns: [ + expect.objectContaining({ + columnId: 'id2', + }), + expect.objectContaining({ + columnId: 'id3', + }), + ], + layerId: 'id1', + }, + }) + ); + }); + }); + }); describe('#getDatasourceSuggestionsFromCurrentState', () => { it('returns no suggestions if there are no columns', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 75945529ffb340..c7eeef178c2514 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -118,6 +118,25 @@ export function getDatasourceSuggestionsForField( } } +// Called when the user navigates from Discover to Lens (Visualize button) +export function getDatasourceSuggestionsForVisualizeField( + state: IndexPatternPrivateState, + indexPatternId: string, + fieldName: string +): IndexPatternSugestion[] { + const layers = Object.keys(state.layers); + const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); + // Identify the field by the indexPatternId and the fieldName + const indexPattern = state.indexPatterns[indexPatternId]; + const field = indexPattern.fields.find((fld) => fld.name === fieldName); + + if (layerIds.length !== 0 || !field) return []; + const newId = generateId(); + return getEmptyLayerSuggestionsForField(state, newId, indexPatternId, field).concat( + getEmptyLayerSuggestionsForField({ ...state, layers: {} }, newId, indexPatternId, field) + ); +} + function getBucketOperation(field: IndexPatternField) { // We allow numeric bucket types in some cases, but it's generally not the right suggestion, // so we eliminate it here. @@ -473,7 +492,6 @@ export function getDatasourceSuggestionsFromCurrentState( suggestions.push(createChangedNestingSuggestion(state, layerId)); } } - return suggestions; }) ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index ef6abbec9a34d1..06cfdf7e034817 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -441,6 +441,34 @@ describe('loader', () => { }); }); + it('should use the indexPatternId of the visualize trigger field, if provided', async () => { + const storage = createMockStorage(); + const state = await loadInitialState({ + savedObjectsClient: mockClient(), + indexPatternsService: mockIndexPatternsService(), + storage, + initialContext: { + indexPatternId: '1', + fieldName: '', + }, + }); + + expect(state).toMatchObject({ + currentIndexPatternId: '1', + indexPatternRefs: [ + { id: '1', title: sampleIndexPatterns['1'].title }, + { id: '2', title: sampleIndexPatterns['2'].title }, + ], + indexPatterns: { + '1': sampleIndexPatterns['1'], + }, + layers: {}, + }); + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: '1', + }); + }); + it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { layers: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index c4b1eb9e0c4c4a..fd8e071d524eed 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -23,6 +23,7 @@ import { IndexPatternsContract, indexPatterns as indexPatternsUtils, } from '../../../../../src/plugins/data/public'; +import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; import { documentField } from './document_field'; import { readFromStorage, writeToStorage } from '../settings_storage'; @@ -179,6 +180,7 @@ export async function loadInitialState({ defaultIndexPatternId, storage, indexPatternsService, + initialContext, }: { persistedState?: IndexPatternPersistedState; references?: SavedObjectReference[]; @@ -186,6 +188,7 @@ export async function loadInitialState({ defaultIndexPatternId?: string; storage: IStorageWrapper; indexPatternsService: IndexPatternsService; + initialContext?: VisualizeFieldContext; }): Promise { const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); @@ -201,13 +204,13 @@ export async function loadInitialState({ : [lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0].id] ); - const currentIndexPatternId = requiredPatterns[0]; + const currentIndexPatternId = initialContext?.indexPatternId ?? requiredPatterns[0]; setLastUsedIndexPatternId(storage, currentIndexPatternId); const indexPatterns = await loadIndexPatterns({ indexPatternsService, cache: {}, - patterns: requiredPatterns, + patterns: initialContext ? [initialContext.indexPatternId] : requiredPatterns, }); if (state) { return { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 38d256d2b3afde..90b0f0a2bde84a 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -29,10 +29,15 @@ import { PieVisualization, PieVisualizationPluginSetupPlugins } from './pie_visu import { stopReportManager } from './lens_ui_telemetry'; import { AppNavLinkStatus } from '../../../../src/core/public'; -import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + UiActionsStart, + ACTION_VISUALIZE_FIELD, + VISUALIZE_FIELD_TRIGGER, +} from '../../../../src/plugins/ui_actions/public'; import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; +import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; import { getSearchProvider } from './search_provider'; import { getLensAttributeService, LensAttributeService } from './lens_attribute_service'; @@ -155,6 +160,14 @@ export class LensPlugin { start(core: CoreStart, startDependencies: LensPluginStartDependencies) { this.attributeService = getLensAttributeService(core, startDependencies); this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; + // unregisters the Visualize action and registers the lens one + if (startDependencies.uiActions.hasAction(ACTION_VISUALIZE_FIELD)) { + startDependencies.uiActions.unregisterAction(ACTION_VISUALIZE_FIELD); + } + startDependencies.uiActions.addTriggerAction( + VISUALIZE_FIELD_TRIGGER, + visualizeFieldAction(core.application) + ); } stop() { diff --git a/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts b/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts new file mode 100644 index 00000000000000..a473d433ac89d2 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts @@ -0,0 +1,28 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + createAction, + ACTION_VISUALIZE_LENS_FIELD, + VisualizeFieldContext, +} from '../../../../../src/plugins/ui_actions/public'; +import { ApplicationStart } from '../../../../../src/core/public'; + +export const visualizeFieldAction = (application: ApplicationStart) => + createAction({ + type: ACTION_VISUALIZE_LENS_FIELD, + id: ACTION_VISUALIZE_LENS_FIELD, + getDisplayName: () => + i18n.translate('xpack.lens.discover.visualizeFieldLegend', { + defaultMessage: 'Visualize field', + }), + isCompatible: async () => !!application.capabilities.visualize.show, + execute: async (context: VisualizeFieldContext) => { + application.navigateToApp('lens', { + state: { type: ACTION_VISUALIZE_LENS_FIELD, payload: context }, + }); + }, + }); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index e5e8a645dd0e80..6061f928bce418 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -22,6 +22,7 @@ import { SELECT_RANGE_TRIGGER, TriggerContext, VALUE_CLICK_TRIGGER, + VisualizeFieldContext, } from '../../../../src/plugins/ui_actions/public'; export type ErrorCallback = (e: { message: string }) => void; @@ -40,6 +41,7 @@ export interface EditorFrameProps { query: Query; filters: Filter[]; savedQuery?: SavedQuery; + initialContext?: VisualizeFieldContext; // Frame loader (app or embeddable) is expected to call this when it loads and updates // This should be replaced with a top-down state @@ -145,7 +147,11 @@ export interface Datasource { // For initializing, either from an empty state or from persisted state // Because this will be called at runtime, state might have a type of `any` and // datasources should validate their arguments - initialize: (state?: P, savedObjectReferences?: SavedObjectReference[]) => Promise; + initialize: ( + state?: P, + savedObjectReferences?: SavedObjectReference[], + initialContext?: VisualizeFieldContext + ) => Promise; // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; @@ -166,6 +172,11 @@ export interface Datasource { toExpression: (state: T, layerId: string) => Ast | string | null; getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; + getDatasourceSuggestionsForVisualizeField: ( + state: T, + indexPatternId: string, + fieldName: string + ) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 816428f7b3cc30..93179ac68a0383 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -14,6 +14,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./async_scripted_fields')); loadTestFile(require.resolve('./reporting')); loadTestFile(require.resolve('./error_handling')); + loadTestFile(require.resolve('./visualize_field')); loadTestFile(require.resolve('./value_suggestions')); }); } diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts new file mode 100644 index 00000000000000..b0e4cb591791b0 --- /dev/null +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -0,0 +1,76 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const filterBar = getService('filterBar'); + const queryBar = getService('queryBar'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const PageObjects = getPageObjects([ + 'common', + 'error', + 'discover', + 'timePicker', + 'security', + 'spaceSelector', + 'header', + ]); + + async function setDiscoverTimeRange() { + await PageObjects.timePicker.setDefaultAbsoluteRange(); + } + + describe('discover field visualize button', () => { + beforeEach(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('lens/basic'); + await PageObjects.common.navigateToApp('discover'); + await setDiscoverTimeRange(); + }); + + after(async () => { + await esArchiver.unload('lens/basic'); + }); + + it('shows "visualize" field button', async () => { + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectFieldListItemVisualize('bytes'); + }); + + it('visualizes field to Lens and loads fields to the dimesion editor', async () => { + await PageObjects.discover.findFieldByName('bytes'); + await PageObjects.discover.clickFieldListItemVisualize('bytes'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async () => { + const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); + expect(dimensions).to.have.length(2); + expect(await dimensions[1].getVisibleText()).to.be('Average of bytes'); + }); + }); + + it('should preserve app filters in lens', async () => { + await filterBar.addFilter('bytes', 'is between', '3500', '4000'); + await PageObjects.discover.findFieldByName('geo.src'); + await PageObjects.discover.clickFieldListItemVisualize('geo.src'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + expect(await filterBar.hasFilter('bytes', '3,500 to 4,000')).to.be(true); + }); + + it('should preserve query in lens', async () => { + await queryBar.setQuery('machine.os : ios'); + await queryBar.submitQuery(); + await PageObjects.discover.findFieldByName('geo.dest'); + await PageObjects.discover.clickFieldListItemVisualize('geo.dest'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + expect(await queryBar.getQueryString()).to.equal('machine.os : ios'); + }); + }); +}