diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts index 8c009148831df..f2a0d3b5e0f07 100644 --- a/x-pack/platform/plugins/shared/lens/public/plugin.ts +++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts @@ -382,6 +382,13 @@ export class LensPlugin { datasourceMap, theme: core.theme, uiSettings: core.uiSettings, + getEditorFrameService: async () => { + if (!this.editorFrameService) { + await this.initEditorFrameService(); + } + + return this.editorFrameService!; + }, }; }; @@ -683,13 +690,7 @@ export class LensPlugin { ); // Displays the add ESQL panel in the dashboard add Panel menu - const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core, async () => { - if (!this.editorFrameService) { - await this.initEditorFrameService(); - } - - return this.editorFrameService!; - }); + const createESQLPanelAction = new CreateESQLPanelAction(core); startDependencies.uiActions.addTriggerAction(ADD_PANEL_TRIGGER, createESQLPanelAction); const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR'); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts index 57bdf3889397e..80ceccbd10ced 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts @@ -116,7 +116,7 @@ async function expectRerenderOnDataLoder( parentApi, }; const getState = jest.fn(() => runtimeState); - const internalApi = getLensInternalApiMock(); + const internalApi = await getLensInternalApiMock(); const services = makeEmbeddableServices(new BehaviorSubject(''), undefined, { visOverrides: { id: 'lnsXY' }, dataOverrides: { id: 'form_based' }, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.test.ts index b3a553bf97604..e87fffee2bc6d 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.test.ts @@ -34,7 +34,7 @@ jest.mock('../../app_plugin/show_underlying_data', () => { }; }); -function setupActionsApi( +async function setupActionsApi( stateOverrides?: Partial, contextOverrides?: Omit ) { @@ -46,7 +46,7 @@ function setupActionsApi( const runtimeState = getLensRuntimeStateMock(stateOverrides); const apiMock = getLensApiMock(); // create the internal API and customize internal state - const internalApi = getLensInternalApiMock(); + const internalApi = await getLensInternalApiMock(); internalApi.updateVisualizationContext({ ...contextOverrides, activeAttributes: runtimeState.attributes, @@ -74,12 +74,12 @@ function setupActionsApi( describe('Dashboard actions', () => { describe('Drilldowns', () => { it('should expose drilldowns for DSL based visualization', async () => { - const api = setupActionsApi(); + const api = await setupActionsApi(); expect(api.enhancements).toBeDefined(); }); it('should not expose drilldowns for ES|QL chart types', async () => { - const api = setupActionsApi( + const api = await setupActionsApi( createEmptyLensState('lnsXY', faker.lorem.words(), faker.lorem.text(), { esql: 'FROM index', }) @@ -120,13 +120,13 @@ describe('Dashboard actions', () => { activeData: {}, }; it('should expose the "explore in discover" capability for DSL based visualization when compatible', async () => { - const api = setupActionsApi(undefined, visualizationContextMockOverrides); + const api = await setupActionsApi(undefined, visualizationContextMockOverrides); api.loadViewUnderlyingData(); expect(api.canViewUnderlyingData$.getValue()).toBe(true); }); it('should expose the "explore in discover" capability for ES|QL chart types', async () => { - const api = setupActionsApi( + const api = await setupActionsApi( createEmptyLensState('lnsXY', faker.lorem.words(), faker.lorem.text(), { esql: 'FROM index', }), diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.ts index ca6ec57893b88..56366cb8b2978 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_service.test.ts @@ -13,9 +13,9 @@ import { initializeDashboardServices } from './initialize_dashboard_services'; import { faker } from '@faker-js/faker'; import { createEmptyLensState } from '../helper'; -function setupDashboardServicesApi(runtimeOverrides?: Partial) { +async function setupDashboardServicesApi(runtimeOverrides?: Partial) { const services = makeEmbeddableServices(); - const internalApiMock = getLensInternalApiMock(); + const internalApiMock = await getLensInternalApiMock(); const runtimeState = getLensRuntimeStateMock(runtimeOverrides); const stateManagementConfig = initializeStateManagement(runtimeState, internalApiMock); const titleManager = initializeTitleManager(runtimeState); @@ -33,18 +33,18 @@ function setupDashboardServicesApi(runtimeOverrides?: Partial) describe('Transformation API', () => { it("should not save to library if there's already a saveObjectId", async () => { - const api = setupDashboardServicesApi({ savedObjectId: faker.string.uuid() }); + const api = await setupDashboardServicesApi({ savedObjectId: faker.string.uuid() }); expect(await api.canLinkToLibrary()).toBe(false); }); it("should save to library if there's no saveObjectId declared", async () => { - const api = setupDashboardServicesApi(); + const api = await setupDashboardServicesApi(); expect(await api.canLinkToLibrary()).toBe(true); }); it('should not save to library for ES|QL chart types', async () => { // setup a state with an ES|QL query - const api = setupDashboardServicesApi( + const api = await setupDashboardServicesApi( createEmptyLensState('lnsXY', faker.lorem.words(), faker.lorem.text(), { esql: 'FROM index', }) diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts index f3ef29f84e218..be64995c55328 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts @@ -17,8 +17,8 @@ import { BehaviorSubject } from 'rxjs'; import { ApplicationStart } from '@kbn/core/public'; import { LensEmbeddableStartServices } from '../types'; -function createEditApi(servicesOverrides: Partial = {}) { - const internalApi = getLensInternalApiMock(); +async function createEditApi(servicesOverrides: Partial = {}) { + const internalApi = await getLensInternalApiMock(); const runtimeState = getLensRuntimeStateMock(); const api = getLensApiMock(); const services = { @@ -43,13 +43,13 @@ function createEditApi(servicesOverrides: Partial = } describe('edit features', () => { - it('should be editable if visualize library privileges allow it', () => { - const editApi = createEditApi(); + it('should be editable if visualize library privileges allow it', async () => { + const editApi = await createEditApi(); expect(editApi.api.isEditingEnabled()).toBe(true); }); - it('should not be editable if visualize library privileges do not allow it', () => { - const editApi = createEditApi({ + it('should not be editable if visualize library privileges do not allow it', async () => { + const editApi = await createEditApi({ capabilities: { visualize_v2: { // cannot save diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts index 7b2cb38b36e87..2b819e1a1585a 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts @@ -21,12 +21,12 @@ import type { import { apiHasAbortController, apiHasLensComponentProps } from '../type_guards'; import type { UserMessage } from '../../types'; -export function initializeInternalApi( +export async function initializeInternalApi( initialState: LensRuntimeState, parentApi: unknown, titleManager: ReturnType, - { visualizationMap }: LensEmbeddableStartServices -): LensInternalApi { + services: LensEmbeddableStartServices +): Promise { const [hasRenderCompleted$] = buildObservableVariable(false); const [expressionParams$] = buildObservableVariable(null); const expressionAbortController$ = new BehaviorSubject(undefined); @@ -35,8 +35,26 @@ export function initializeInternalApi( } const [renderCount$] = buildObservableVariable(0); + async function getInitialAttributes() { + if (initialState.attributes) { + return initialState.attributes; + } + + if (initialState.isNewPanel) { + try { + const { createNewEsqlAttributes } = await import('../../async_services'); + const attributes = await createNewEsqlAttributes(services); + if (attributes) return attributes; + } catch (error) { + // fallback to empty state + } + } + + return createEmptyLensState().attributes; + } + const attributes$ = new BehaviorSubject( - initialState.attributes || createEmptyLensState().attributes + await getInitialAttributes() ); const overrides$ = new BehaviorSubject(initialState.overrides); const disableTriggers$ = new BehaviorSubject(initialState.disableTriggers); @@ -120,7 +138,7 @@ export function initializeInternalApi( } let displayOptions = - visualizationMap[latestAttributes.visualizationType]?.getDisplayOptions?.() ?? {}; + services.visualizationMap[latestAttributes.visualizationType]?.getDisplayOptions?.() ?? {}; if (apiHasLensComponentProps(parentApi) && parentApi.noPadding != null) { displayOptions = { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx index c7ef545a546b7..e39e9a9c55a42 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/setup_inline_editing.tsx @@ -45,6 +45,7 @@ export function prepareInlineEditPanel( | 'theme' | 'uiSettings' | 'attributeService' + | 'getEditorFrameService' >, navigateToLensEditor?: ( stateTransfer: EmbeddableStateTransfer, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx index f3a76432afcca..627987461ee1b 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx @@ -61,7 +61,12 @@ export const createLensEmbeddableFactory = ( * Observables and functions declared here are used internally to store mutating state values * This is an internal API not exposed outside of the embeddable. */ - const internalApi = initializeInternalApi(initialState, parentApi, titleManager, services); + const internalApi = await initializeInternalApi( + initialState, + parentApi, + titleManager, + services + ); /** * Initialize various configurations required to build all the required diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx index 77f4137890e9e..573edbaae1c08 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx @@ -34,6 +34,7 @@ import { import { createMockDatasource, createMockVisualization, makeDefaultServices } from '../../mocks'; import { Datasource, DatasourceMap, Visualization, VisualizationMap } from '../../types'; import { initializeInternalApi } from '../initializers/initialize_internal_api'; +import { EditorFrameService } from '../../async_services'; function getDefaultLensApiMock() { const LensApiMock: LensApi = { @@ -206,6 +207,12 @@ export function makeEmbeddableServices( } as unknown as ReactEmbeddableDynamicActionsApi) ), }, + getEditorFrameService: jest.fn(() => + Promise.resolve({ + loadVisualizations: jest.fn(), + loadDatasources: jest.fn(), + } as unknown as EditorFrameService) + ), }; } @@ -276,9 +283,9 @@ export function getValidExpressionParams( }; } -function getInternalApiWithFunctionWrappers() { +async function getInternalApiWithFunctionWrappers() { const mockRuntimeState = getLensRuntimeStateMock(); - const newApi = initializeInternalApi( + const newApi = await initializeInternalApi( mockRuntimeState, {}, initializeTitleManager(mockRuntimeState), @@ -295,9 +302,11 @@ function getInternalApiWithFunctionWrappers() { return newApi; } -export function getLensInternalApiMock(overrides: Partial = {}): LensInternalApi { +export async function getLensInternalApiMock( + overrides: Partial = {} +): Promise { return { - ...getInternalApiWithFunctionWrappers(), + ...(await getInternalApiWithFunctionWrappers()), ...overrides, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx index 6d73f24e3dcda..1904dfda71bdf 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx @@ -21,11 +21,11 @@ jest.mock('../expression_wrapper', () => ({ type GetValueType = Type extends PublishingSubject ? X : never; -function getDefaultProps({ +async function getDefaultProps({ internalApiOverrides = undefined, apiOverrides = undefined, }: { internalApiOverrides?: Partial; apiOverrides?: Partial } = {}) { - const internalApi = getLensInternalApiMock(internalApiOverrides); + const internalApi = await getLensInternalApiMock(internalApiOverrides); // provide a valid expression to render internalApi.updateExpressionParams(getValidExpressionParams()); return { @@ -36,8 +36,8 @@ function getDefaultProps({ } describe('Lens Embeddable component', () => { - it('should not render the visualization if any error arises', () => { - const props = getDefaultProps({ + it('should not render the visualization if any error arises', async () => { + const props = await getDefaultProps({ internalApiOverrides: { expressionParams$: new BehaviorSubject>( null @@ -49,9 +49,9 @@ describe('Lens Embeddable component', () => { expect(screen.queryByTestId('lens-embeddable')).not.toBeInTheDocument(); }); - it('shoud not render the title if the visualization forces the title to be hidden', () => { + it('shoud not render the title if the visualization forces the title to be hidden', async () => { const getDisplayOptions = jest.fn(() => ({ noPanelTitle: true })); - const props = getDefaultProps({ + const props = await getDefaultProps({ internalApiOverrides: { getDisplayOptions, }, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts index 8758152a54f0d..b5f517ebc93c5 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts @@ -94,6 +94,7 @@ import type { FormBasedPersistedState } from '..'; import type { TextBasedPersistedState } from '../datasources/text_based/types'; import type { GaugeVisualizationState } from '../visualizations/gauge/constants'; import type { MetricVisualizationState } from '../visualizations/metric/types'; +import type { EditorFrameService } from '../editor_frame_service'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface LensApiProps {} @@ -145,6 +146,7 @@ export type LensEmbeddableStartServices = Simplify< theme: ThemeServiceStart; uiSettings: IUiSettingsClient; attributeService: LensAttributesService; + getEditorFrameService: () => Promise; } >; diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts index 03717a91228e3..66268e007d4fe 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts @@ -43,7 +43,7 @@ function createUserMessage( }; } -function buildUserMessagesApi( +async function buildUserMessagesApi( metaInfo?: SharingSavedObjectProps, { visOverrides, @@ -57,7 +57,7 @@ function buildUserMessagesApi( } ) { const api = getLensApiMock(); - const internalApi = getLensInternalApiMock(); + const internalApi = await getLensInternalApiMock(); const services = makeEmbeddableServices(new BehaviorSubject(''), undefined, { visOverrides, dataOverrides, @@ -88,8 +88,8 @@ function buildUserMessagesApi( describe('User Messages API', () => { describe('resetMessages', () => { - it('should reset the runtime errors', () => { - const { userMessagesApi } = buildUserMessagesApi(); + it('should reset the runtime errors', async () => { + const { userMessagesApi } = await buildUserMessagesApi(); // add runtime messages const userMessageError = createUserMessage(); const userMessageWarning = createUserMessage(['embeddableBadge'], 'warning'); @@ -102,8 +102,8 @@ describe('User Messages API', () => { }); describe('updateValidationErrors', () => { - it('should basically work', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should basically work', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); internalApi.updateValidationMessages = jest.fn(); const messages = Array(3).fill(createUserMessage()); userMessagesApi.updateValidationErrors(messages); @@ -112,8 +112,8 @@ describe('User Messages API', () => { }); describe('updateMessages', () => { - it('should avoid to update duplicate messages', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should avoid to update duplicate messages', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); // start with these 3 messages const messages = Array(3) .fill(1) @@ -130,8 +130,8 @@ describe('User Messages API', () => { expect(internalApi.updateMessages).toHaveBeenCalledTimes(2); }); - it('should update the messages if there are new messages', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should update the messages if there are new messages', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); // start with these 3 messages const messages = Array(3).fill(createUserMessage()); // update the messages @@ -143,8 +143,8 @@ describe('User Messages API', () => { expect(internalApi.updateMessages).toHaveBeenCalledWith(messagesWithNewEntry); }); - it('should update the messages when changing', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should update the messages when changing', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); // start with these 3 messages const messages = Array(3).fill(createUserMessage()); // update the messages @@ -158,16 +158,16 @@ describe('User Messages API', () => { }); describe('updateBlockingErrors', () => { - it('should basically work with a regular Error', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should basically work with a regular Error', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); internalApi.updateBlockingError = jest.fn(); const error = new Error('Something went wrong'); userMessagesApi.updateBlockingErrors(error); expect(internalApi.updateBlockingError).toHaveBeenCalledWith(error); }); - it('should work with user messages too', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should work with user messages too', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); internalApi.updateBlockingError = jest.fn(); const userMessage = createUserMessage(); userMessagesApi.updateBlockingErrors([userMessage]); @@ -176,8 +176,8 @@ describe('User Messages API', () => { ); }); - it('should pick only the first error from a list of user messages', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should pick only the first error from a list of user messages', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); internalApi.updateBlockingError = jest.fn(); const userMessage = createUserMessage(); userMessagesApi.updateBlockingErrors([userMessage, createUserMessage(), createUserMessage()]); @@ -186,8 +186,8 @@ describe('User Messages API', () => { ); }); - it('should clear out the error when an empty error is passed', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should clear out the error when an empty error is passed', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); internalApi.updateBlockingError = jest.fn(); userMessagesApi.updateBlockingErrors(new Error('')); expect(internalApi.updateBlockingError).toHaveBeenCalledWith(undefined); @@ -195,15 +195,15 @@ describe('User Messages API', () => { }); describe('getUserMessages', () => { - it('should return empty list for no messages', () => { - const { userMessagesApi } = buildUserMessagesApi(); + it('should return empty list for no messages', async () => { + const { userMessagesApi } = await buildUserMessagesApi(); for (const locationId of ALL_LOCATIONS) { expect(userMessagesApi.getUserMessages(locationId)).toEqual([]); } }); - it('should return basic validation for missing parts of the config', () => { - const { userMessagesApi, internalApi } = buildUserMessagesApi(); + it('should return basic validation for missing parts of the config', async () => { + const { userMessagesApi, internalApi } = await buildUserMessagesApi(); // no doc scenario internalApi.updateVisualizationContext({ ...internalApi.getVisualizationContext(), @@ -216,8 +216,8 @@ describe('User Messages API', () => { } }); - it('should detect a URL conflict', () => { - const { userMessagesApi } = buildUserMessagesApi({ outcome: 'conflict' }); + it('should detect a URL conflict', async () => { + const { userMessagesApi } = await buildUserMessagesApi({ outcome: 'conflict' }); for (const locationId of ALL_LOCATIONS.filter((id) => id !== 'visualization')) { expect(userMessagesApi.getUserMessages(locationId)).toEqual([]); @@ -227,8 +227,8 @@ describe('User Messages API', () => { ); }); - it('should filter messages based on severity criteria', () => { - const { userMessagesApi } = buildUserMessagesApi(); + it('should filter messages based on severity criteria', async () => { + const { userMessagesApi } = await buildUserMessagesApi(); const userMessageError = createUserMessage(); const userMessageWarning = createUserMessage(['embeddableBadge'], 'warning'); const userMessageInfo = createUserMessage(['embeddableBadge'], 'info'); @@ -244,8 +244,8 @@ describe('User Messages API', () => { ); }); - it('should filter messages based on locationId', () => { - const { userMessagesApi } = buildUserMessagesApi(); + it('should filter messages based on locationId', async () => { + const { userMessagesApi } = await buildUserMessagesApi(); const userMessageEmbeddable = createUserMessage(['embeddableBadge']); const userMessageVisualization = createUserMessage(['visualization']); const userMessageEmbeddableVisualization = createUserMessage([ @@ -262,10 +262,10 @@ describe('User Messages API', () => { expect(userMessagesApi.getUserMessages('visualizationOnEmbeddable').length).toEqual(0); }); - it('should return deeper validation messages from both datasource and visualization', () => { + it('should return deeper validation messages from both datasource and visualization', async () => { const vizGetUserMessages = jest.fn(); const datasourceGetUserMessages = jest.fn(); - const { userMessagesApi } = buildUserMessagesApi(undefined, { + const { userMessagesApi } = await buildUserMessagesApi(undefined, { visOverrides: { id: 'lnsXY', getUserMessages: vizGetUserMessages }, dataOverrides: { id: 'formBased', getUserMessages: datasourceGetUserMessages }, }); @@ -277,8 +277,8 @@ describe('User Messages API', () => { expect(datasourceGetUserMessages).toHaveBeenCalled(); }); - it('should enable consumers to filter the final list of messages', () => { - const { userMessagesApi, onBeforeBadgesRender } = buildUserMessagesApi(); + it('should enable consumers to filter the final list of messages', async () => { + const { userMessagesApi, onBeforeBadgesRender } = await buildUserMessagesApi(); // it should not be called when no messages are avaialble userMessagesApi.getUserMessages('embeddableBadge'); expect(onBeforeBadgesRender).not.toHaveBeenCalled(); @@ -291,8 +291,8 @@ describe('User Messages API', () => { }); describe('addUserMessages', () => { - it('should basically work', () => { - const { userMessagesApi } = buildUserMessagesApi(); + it('should basically work', async () => { + const { userMessagesApi } = await buildUserMessagesApi(); expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(0); // now add a message, then check that it has been called const userMessageEmbeddable = createUserMessage(); diff --git a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.test.tsx b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.test.tsx index 63844b1d6d3ea..8b753d2ddd407 100644 --- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.test.tsx @@ -7,31 +7,15 @@ import type { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; -import type { LensPluginStartDependencies } from '../../plugin'; -import type { EditorFrameService } from '../../editor_frame_service'; -import { createMockStartDependencies } from '../../editor_frame_service/mocks'; import { CreateESQLPanelAction } from './create_action'; describe('create Lens panel action', () => { const core = coreMock.createStart(); - const mockStartDependencies = - createMockStartDependencies() as unknown as LensPluginStartDependencies; const mockPresentationContainer = getMockPresentationContainer(); - const mockEditorFrameService = { - loadVisualizations: jest.fn(), - loadDatasources: jest.fn(), - } as unknown as EditorFrameService; - - const mockGetEditorFrameService = jest.fn(() => Promise.resolve(mockEditorFrameService)); - describe('compatibility check', () => { it('is incompatible if ui setting for ES|QL is off', async () => { - const configurablePanelAction = new CreateESQLPanelAction( - mockStartDependencies, - core, - mockGetEditorFrameService - ); + const configurablePanelAction = new CreateESQLPanelAction(core); const isCompatible = await configurablePanelAction.isCompatible({ embeddable: mockPresentationContainer, @@ -51,11 +35,7 @@ describe('create Lens panel action', () => { }, } as CoreStart; - const createESQLAction = new CreateESQLPanelAction( - mockStartDependencies, - updatedCore, - mockGetEditorFrameService - ); + const createESQLAction = new CreateESQLPanelAction(updatedCore); const isCompatible = await createESQLAction.isCompatible({ embeddable: mockPresentationContainer, diff --git a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.tsx b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.tsx index d59e2b629ef92..7e2a35ebc8c7d 100644 --- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.tsx +++ b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action.tsx @@ -10,13 +10,12 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; import { ADD_PANEL_VISUALIZATION_GROUP } from '@kbn/embeddable-plugin/public'; -import type { LensPluginStartDependencies } from '../../plugin'; -import type { EditorFrameService } from '../../editor_frame_service'; +import { ENABLE_ESQL } from '@kbn/esql-utils'; +import { generateId } from '../../id_generator'; +import type { LensApi } from '../../react_embeddable/types'; const ACTION_CREATE_ESQL_CHART = 'ACTION_CREATE_ESQL_CHART'; -export const getAsyncHelpers = async () => await import('../../async_services'); - export class CreateESQLPanelAction implements Action { public type = ACTION_CREATE_ESQL_CHART; public id = ACTION_CREATE_ESQL_CHART; @@ -24,11 +23,7 @@ export class CreateESQLPanelAction implements Action { public grouping = [ADD_PANEL_VISUALIZATION_GROUP]; - constructor( - protected readonly startDependencies: LensPluginStartDependencies, - protected readonly core: CoreStart, - protected readonly getEditorFrameService: () => Promise - ) {} + constructor(protected readonly core: CoreStart) {} public getDisplayName(): string { return i18n.translate('xpack.lens.app.createVisualizationLabel', { @@ -43,21 +38,19 @@ export class CreateESQLPanelAction implements Action { public async isCompatible({ embeddable }: EmbeddableApiContext) { if (!apiIsPresentationContainer(embeddable)) return false; - const { isCreateActionCompatible } = await getAsyncHelpers(); - - return isCreateActionCompatible(this.core); + return this.core.uiSettings.get(ENABLE_ESQL); } - public async execute({ embeddable }: EmbeddableApiContext) { - if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); - const { executeCreateAction } = await getAsyncHelpers(); - const editorFrameService = await this.getEditorFrameService(); - - executeCreateAction({ - deps: this.startDependencies, - core: this.core, - api: embeddable, - editorFrameService, + public async execute({ embeddable: parentApi }: EmbeddableApiContext) { + if (!apiIsPresentationContainer(parentApi)) throw new IncompatibleActionError(); + const embeddable = await parentApi.addNewPanel({ + panelType: 'lens', + initialState: { + id: generateId(), + isNewPanel: true, + }, }); + // open the flyout if embeddable has been created successfully + embeddable?.onEdit(); } } diff --git a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 6f875e49f160c..c9a739eca09b6 100644 --- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -5,23 +5,16 @@ * 2.0. */ import { createGetterSetter } from '@kbn/kibana-utils-plugin/common'; -import type { CoreStart } from '@kbn/core/public'; import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; -import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { PresentationContainer } from '@kbn/presentation-containers'; import { getESQLAdHocDataview, getIndexForESQLQuery, - ENABLE_ESQL, getESQLQueryColumns, getInitialESQLQuery, } from '@kbn/esql-utils'; import type { Datasource, Visualization } from '../../types'; -import type { LensPluginStartDependencies } from '../../plugin'; import { suggestionsApi } from '../../lens_suggestions_api'; -import { generateId } from '../../id_generator'; -import type { EditorFrameService } from '../../editor_frame_service'; -import { LensApi } from '../..'; +import type { LensEmbeddableStartServices } from '../../react_embeddable/types'; // datasourceMap and visualizationMap setters/getters export const [getVisualizationMap, setVisualizationMap] = createGetterSetter< @@ -32,36 +25,15 @@ export const [getDatasourceMap, setDatasourceMap] = createGetterSetter< Record> >('DatasourceMap', false); -export async function isCreateActionCompatible(core: CoreStart) { - return core.uiSettings.get(ENABLE_ESQL); -} - -export async function executeCreateAction({ - deps, - core, - api, - editorFrameService, -}: { - deps: LensPluginStartDependencies; - core: CoreStart; - api: PresentationContainer; - editorFrameService: EditorFrameService; -}) { - const getFallbackDataView = async () => { - const indexName = await getIndexForESQLQuery({ dataViews: deps.dataViews }); - if (!indexName) return null; - const dataView = await getESQLAdHocDataview(`from ${indexName}`, deps.dataViews); - return dataView; - }; - - const [isCompatibleAction, dataView] = await Promise.all([ - isCreateActionCompatible(core), - getFallbackDataView(), - ]); - - if (!isCompatibleAction || !dataView) { - throw new IncompatibleActionError(); +export async function createNewEsqlAttributes( + services: Pick +) { + const indexName = await getIndexForESQLQuery({ dataViews: services.dataViews }); + if (!indexName) { + return; } + const dataView = await getESQLAdHocDataview(`from ${indexName}`, services.dataViews); + const editorFrameService = await services.getEditorFrameService(); let visualizationMap = getVisualizationMap(); let datasourceMap = getDatasourceMap(); @@ -73,7 +45,7 @@ export async function executeCreateAction({ ]); if (!visualizationMap && !datasourceMap) { - throw new IncompatibleActionError(); + return; } // persist for retrieval elsewhere @@ -94,9 +66,9 @@ export async function executeCreateAction({ const abortController = new AbortController(); const columns = await getESQLQueryColumns({ esqlQuery, - search: deps.data.search.search, + search: services.data.search.search, signal: abortController.signal, - timeRange: deps.data.query.timefilter.timefilter.getAbsoluteTime(), + timeRange: services.data.query.timefilter.timefilter.getAbsoluteTime(), }); const context = { @@ -113,7 +85,7 @@ export async function executeCreateAction({ // Lens might not return suggestions for some cases, i.e. in case of errors if (!allSuggestions.length) return undefined; const [firstSuggestion] = allSuggestions; - const attrs = getLensAttributesFromSuggestion({ + return getLensAttributesFromSuggestion({ filters: [], query: defaultEsqlQuery, suggestion: { @@ -122,15 +94,4 @@ export async function executeCreateAction({ }, dataView, }); - - const embeddable = await api.addNewPanel({ - panelType: 'lens', - initialState: { - attributes: attrs, - id: generateId(), - isNewPanel: true, - }, - }); - // open the flyout if embeddable has been created successfully - embeddable?.onEdit?.(); }