From 80ae8a4e0b98d546daef045deb74fcaeb70128c9 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 6 Feb 2025 18:20:24 +0100 Subject: [PATCH 1/6] :zap: Defer evaluation loading into the build embeddable load --- .../shared/lens/public/async_services.ts | 1 - .../lens/public/mocks/data_plugin_mock.ts | 1 + .../plugins/shared/lens/public/plugin.ts | 124 ++++++++-------- .../react_embeddable/data_loader.test.ts | 116 ++++++++++++--- .../public/react_embeddable/data_loader.ts | 96 ++++++++++++- .../initializers/initialize_edit.tsx | 8 +- .../initializers/initialize_internal_api.ts | 2 +- .../react_embeddable/initializers/utils.ts | 7 +- .../inline_editing/setup_inline_editing.tsx | 18 +-- .../react_embeddable/lens_embeddable.tsx | 2 +- .../open_lens_config/create_action.test.tsx | 24 +--- .../open_lens_config/create_action.tsx | 32 ++--- .../open_lens_config/create_action_helpers.ts | 136 ------------------ .../in_app_embeddable_edit_action.test.tsx | 17 ++- .../in_app_embeddable_edit_action.tsx | 34 ++++- .../in_app_embeddable_edit_action_helpers.tsx | 28 +--- 16 files changed, 331 insertions(+), 315 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts diff --git a/x-pack/platform/plugins/shared/lens/public/async_services.ts b/x-pack/platform/plugins/shared/lens/public/async_services.ts index e5523b38b525d..cecb02d8bd29f 100644 --- a/x-pack/platform/plugins/shared/lens/public/async_services.ts +++ b/x-pack/platform/plugins/shared/lens/public/async_services.ts @@ -49,5 +49,4 @@ export * from './app_plugin/save_modal_container'; export * from './chart_info_api'; export * from './trigger_actions/open_in_discover_helpers'; -export * from './trigger_actions/open_lens_config/create_action_helpers'; export * from './trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers'; diff --git a/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts b/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts index 18bf09c9c2a92..707b21b8cfb93 100644 --- a/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts +++ b/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts @@ -38,6 +38,7 @@ function createMockTimefilter() { getRefreshInterval: () => {}, getRefreshIntervalDefaults: () => {}, getAutoRefreshFetch$: () => new Observable(), + getAbsoluteTime: jest.fn(), }; } diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts index 8c009148831df..dc56a7cbd24c7 100644 --- a/x-pack/platform/plugins/shared/lens/public/plugin.ts +++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts @@ -322,6 +322,54 @@ export class LensPlugin { visualizationMap: VisualizationMap; }> => ({ datasourceMap: {}, visualizationMap: {} }); + private getStartServicesForEmbeddable = async ( + getStartServices: () => { core: CoreStart; plugins: LensPluginStartDependencies } + ): Promise => { + const { core: coreStart, plugins } = getStartServices(); + const [ + { setUsageCollectionStart, initMemoizedErrorNotification, getLensAttributeService }, + eventAnnotationService, + { visualizationMap, datasourceMap }, + ] = await Promise.all([ + import('./async_services'), + plugins.eventAnnotation.getService(), + this.initEditorFrameService(), + ]); + + if (plugins.usageCollection) { + setUsageCollectionStart(plugins.usageCollection); + } + + initMemoizedErrorNotification(coreStart); + + return { + ...plugins, + attributeService: getLensAttributeService(coreStart, plugins), + capabilities: coreStart.application.capabilities, + coreHttp: coreStart.http, + coreStart, + timefilter: plugins.data.query.timefilter.timefilter, + expressionRenderer: plugins.expressions.ReactExpressionRenderer, + documentToExpression: (doc: LensDocument, forceDSL?: boolean) => + this.editorFrameService!.documentToExpression(doc, { + dataViews: plugins.dataViews, + storage: new Storage(localStorage), + uiSettings: coreStart.uiSettings, + timefilter: plugins.data.query.timefilter.timefilter, + nowProvider: plugins.data.nowProvider, + forceDSL, + eventAnnotationService, + }), + injectFilterReferences: plugins.data.query.filterManager.inject.bind( + plugins.data.query.filterManager + ), + visualizationMap, + datasourceMap, + theme: coreStart.theme, + uiSettings: coreStart.uiSettings, + }; + }; + setup( core: CoreSetup, { @@ -341,55 +389,11 @@ export class LensPlugin { ) { const startServices = createStartServicesGetter(core.getStartServices); - const getStartServicesForEmbeddable = async (): Promise => { - const { setUsageCollectionStart, initMemoizedErrorNotification } = await import( - './async_services' - ); - const { core: coreStart, plugins } = startServices(); - - const { visualizationMap, datasourceMap } = await this.initEditorFrameService(); - const [{ getLensAttributeService }, eventAnnotationService] = await Promise.all([ - import('./async_services'), - plugins.eventAnnotation.getService(), - ]); - - if (plugins.usageCollection) { - setUsageCollectionStart(plugins.usageCollection); - } - - initMemoizedErrorNotification(coreStart); - - return { - ...plugins, - attributeService: getLensAttributeService(coreStart, plugins), - capabilities: coreStart.application.capabilities, - coreHttp: coreStart.http, - coreStart, - timefilter: plugins.data.query.timefilter.timefilter, - expressionRenderer: plugins.expressions.ReactExpressionRenderer, - documentToExpression: (doc: LensDocument, forceDSL?: boolean) => - this.editorFrameService!.documentToExpression(doc, { - dataViews: plugins.dataViews, - storage: new Storage(localStorage), - uiSettings: core.uiSettings, - timefilter: plugins.data.query.timefilter.timefilter, - nowProvider: plugins.data.nowProvider, - forceDSL, - eventAnnotationService, - }), - injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager), - visualizationMap, - datasourceMap, - theme: core.theme, - uiSettings: core.uiSettings, - }; - }; - if (embeddable) { // Let Kibana know about the Lens embeddable embeddable.registerReactEmbeddableFactory(LENS_EMBEDDABLE_TYPE, async () => { const [deps, { createLensEmbeddableFactory }] = await Promise.all([ - getStartServicesForEmbeddable(), + this.getStartServicesForEmbeddable(startServices), import('./react_embeddable/lens_embeddable'), ]); return createLensEmbeddableFactory(deps); @@ -398,7 +402,7 @@ export class LensPlugin { // Let Dashboard know about the Lens panel type embeddable.registerAddFromLibraryType({ onAdd: async (container, savedObject) => { - const { attributeService } = await getStartServicesForEmbeddable(); + const { attributeService } = await this.getStartServicesForEmbeddable(startServices); // deserialize the saved object from visualize library // this make sure to fit into the new embeddable model, where the following build() // function expects a fully loaded runtime state @@ -542,9 +546,6 @@ export class LensPlugin { this.editorFrameService!.loadVisualizations(), this.editorFrameService!.loadDatasources(), ]); - const { setVisualizationMap, setDatasourceMap } = await import('./async_services'); - setDatasourceMap(datasourceMap); - setVisualizationMap(visualizationMap); return { datasourceMap, visualizationMap }; }; @@ -675,7 +676,10 @@ export class LensPlugin { ); // Allows the Lens embeddable to easily open the inline editing flyout - const editLensEmbeddableAction = new EditLensEmbeddableAction(startDependencies, core); + const editLensEmbeddableAction = new EditLensEmbeddableAction(core, () => + this.getStartServicesForEmbeddable(() => ({ core, plugins: startDependencies })) + ); + // embeddable inline edit panel action startDependencies.uiActions.addTriggerAction( IN_APP_EMBEDDABLE_EDIT_TRIGGER, @@ -683,13 +687,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'); @@ -739,11 +737,11 @@ export class LensPlugin { }, stateHelperApi: async () => { - const { createFormulaPublicApi, createChartInfoApi, suggestionsApi } = await import( - './async_services' - ); + const [ + { createFormulaPublicApi, createChartInfoApi, suggestionsApi }, + { visualizationMap, datasourceMap }, + ] = await Promise.all([import('./async_services'), this.initEditorFrameService()]); - const { visualizationMap, datasourceMap } = await this.initEditorFrameService(); return { formula: createFormulaPublicApi(), chartInfo: createChartInfoApi( @@ -773,8 +771,8 @@ export class LensPlugin { // TODO: remove this in faviour of the custom action thing // This is currently used in Discover by the unified histogram plugin EditLensConfigPanelApi: async () => { - const { visualizationMap, datasourceMap } = await this.initEditorFrameService(); - const { getEditLensConfiguration } = await import('./async_services'); + const [{ visualizationMap, datasourceMap }, { getEditLensConfiguration }] = + await Promise.all([this.initEditorFrameService(), import('./async_services')]); const Component = await getEditLensConfiguration( core, startDependencies, 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 21b06ed8b5512..ecef92449adc4 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 @@ -35,6 +35,15 @@ import { import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; import { isObject } from 'lodash'; import { createMockDatasource, defaultDoc } from '../mocks'; +import { coreMock } from '@kbn/core/public/mocks'; +import * as suggestionModule from '../lens_suggestions_api'; +// Need to do this magic in order to spy on specific functions +import * as esqlUtils from '@kbn/esql-utils'; +import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +jest.mock('@kbn/esql-utils', () => ({ + __esModule: true, + ...jest.requireActual('@kbn/esql-utils'), +})); jest.mock('@kbn/interpreter', () => ({ toExpression: jest.fn().mockReturnValue('expression'), @@ -65,6 +74,11 @@ jest.mock('@kbn/presentation-publishing', () => { }; }); +function getUiSettingsOverrides() { + const core = coreMock.createStart({ basePath: '/testbasepath' }); + return core.uiSettings; +} + // In order to listen the reload function, we need to // monitor the internalApi dispatchRenderStart spy type ChangeFnType = ({ @@ -84,7 +98,7 @@ type ChangeFnType = ({ services: LensEmbeddableStartServices; }) => Promise; -async function expectRerenderOnDataLoder( +async function expectRerenderOnDataLoader( changeFn: ChangeFnType, runtimeState: LensRuntimeState = { attributes: getLensAttributesMock() }, { @@ -133,7 +147,7 @@ async function expectRerenderOnDataLoder( documentToExpression: jest.fn().mockResolvedValue({ ast: 'expression_string' }), ...servicesOverrides, }; - const { cleanup } = loadEmbeddableData( + const { cleanup } = await loadEmbeddableData( faker.string.uuid(), getState, api, @@ -182,7 +196,7 @@ describe('Data Loader', () => { beforeEach(() => jest.clearAllMocks()); it('should re-render once on filter change', async () => { - await expectRerenderOnDataLoder(async ({ api }) => { + await expectRerenderOnDataLoader(async ({ api }) => { (api.filters$ as BehaviorSubject).next([ { meta: { alias: 'test', negate: false, disabled: false } }, ]); @@ -190,7 +204,7 @@ describe('Data Loader', () => { }); it('should re-render once on search session change', async () => { - await expectRerenderOnDataLoder(async ({ api }) => { + await expectRerenderOnDataLoader(async ({ api }) => { // dispatch a new searchSessionId ( @@ -200,7 +214,7 @@ describe('Data Loader', () => { }); it('should re-render once on attributes change', async () => { - await expectRerenderOnDataLoder(async ({ internalApi }) => { + await expectRerenderOnDataLoader(async ({ internalApi }) => { // trigger a change by changing the title in the attributes (internalApi.attributes$ as BehaviorSubject).next({ ...internalApi.attributes$.getValue(), @@ -210,7 +224,7 @@ describe('Data Loader', () => { }); it('should re-render when dashboard view/edit mode changes if dynamic actions are set', async () => { - await expectRerenderOnDataLoder(async ({ api, getState }) => { + await expectRerenderOnDataLoader(async ({ api, getState }) => { getState.mockReturnValue({ attributes: getLensAttributesMock(), enhancements: { @@ -225,7 +239,7 @@ describe('Data Loader', () => { }); it('should not re-render when dashboard view/edit mode changes if dynamic actions are not set', async () => { - await expectRerenderOnDataLoder(async ({ api }) => { + await expectRerenderOnDataLoader(async ({ api }) => { // the default get state does not have dynamic actions // trigger a change by changing the title in the attributes (api.viewMode$ as BehaviorSubject).next('view'); @@ -238,7 +252,7 @@ describe('Data Loader', () => { const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; - await expectRerenderOnDataLoder( + await expectRerenderOnDataLoader( async ({ internalApi }) => { await waitForValue( internalApi.expressionParams$, @@ -258,7 +272,7 @@ describe('Data Loader', () => { }); it('should pass render mode to expression', async () => { - await expectRerenderOnDataLoder(async ({ internalApi }) => { + await expectRerenderOnDataLoader(async ({ internalApi }) => { await waitForValue( internalApi.expressionParams$, (v: unknown) => isObject(v) && 'renderMode' in v @@ -295,7 +309,7 @@ describe('Data Loader', () => { ], }; - await expectRerenderOnDataLoder( + await expectRerenderOnDataLoader( async ({ internalApi }) => { await waitForValue( internalApi.expressionParams$, @@ -327,7 +341,7 @@ describe('Data Loader', () => { }); it('should call onload after rerender and onData$ call', async () => { - await expectRerenderOnDataLoder(async ({ parentApi, internalApi, api }) => { + await expectRerenderOnDataLoader(async ({ parentApi, internalApi, api }) => { expect(parentApi.onLoad).toHaveBeenLastCalledWith(true); await waitForValue( @@ -347,7 +361,7 @@ describe('Data Loader', () => { }); it('should initialize dateViews api with deduped list of index patterns', async () => { - await expectRerenderOnDataLoder( + await expectRerenderOnDataLoader( async ({ internalApi }) => { await waitForValue( internalApi.dataViews$, @@ -374,7 +388,7 @@ describe('Data Loader', () => { }); it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => { - await expectRerenderOnDataLoder(async ({ internalApi }) => { + await expectRerenderOnDataLoader(async ({ internalApi }) => { await waitForValue( internalApi.expressionParams$, (v: unknown) => isObject(v) && 'expression' in v && typeof v.expression != null @@ -387,7 +401,7 @@ describe('Data Loader', () => { }); it('should reload only once when the attributes or savedObjectId and the search context change at the same time', async () => { - await expectRerenderOnDataLoder(async ({ internalApi, api }) => { + await expectRerenderOnDataLoader(async ({ internalApi, api }) => { // trigger a change by changing the title in the attributes (internalApi.attributes$ as BehaviorSubject).next({ ...internalApi.attributes$.getValue(), @@ -398,7 +412,7 @@ describe('Data Loader', () => { }); it('should pass over the overrides as variables', async () => { - await expectRerenderOnDataLoder( + await expectRerenderOnDataLoader( async ({ internalApi }) => { await waitForValue( internalApi.expressionParams$, @@ -430,7 +444,7 @@ describe('Data Loader', () => { }); it('should catch missing dataView errors correctly', async () => { - await expectRerenderOnDataLoder( + await expectRerenderOnDataLoader( async ({ internalApi }) => { // wait for the error to appear await waitForValue(internalApi.blockingError$); @@ -478,4 +492,74 @@ describe('Data Loader', () => { } ); }); + + describe('ES|QL creation flow', () => { + function getTestBaseOverrides() { + return { + servicesOverrides: { + uiSettings: { ...getUiSettingsOverrides(), get: jest.fn().mockReturnValue(true) }, + }, + internalApiOverrides: { + isNewlyCreated$: new BehaviorSubject(true), + }, + }; + } + it('should not update the attributes if no index is available', async () => { + jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce(null); + + await expectRerenderOnDataLoader( + async ({ internalApi }) => { + expect(internalApi.updateAttributes).not.toHaveBeenCalled(); + return false; + }, + undefined, + getTestBaseOverrides() + ); + }); + + it('should not update the attributes if no suggestion is generated', async () => { + jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce('index'); + jest.spyOn(esqlUtils, 'getESQLAdHocDataview').mockResolvedValueOnce(dataViewMock); + jest.spyOn(esqlUtils, 'getESQLQueryColumns').mockResolvedValueOnce([]); + jest.spyOn(suggestionModule, 'suggestionsApi').mockReturnValue([]); + + await expectRerenderOnDataLoader( + async ({ internalApi }) => { + expect(internalApi.updateAttributes).not.toHaveBeenCalled(); + return false; + }, + undefined, + getTestBaseOverrides() + ); + }); + + it('should update the attributes if there is a valid suggestion', async () => { + jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce('index'); + jest.spyOn(esqlUtils, 'getESQLAdHocDataview').mockResolvedValueOnce(dataViewMock); + jest.spyOn(esqlUtils, 'getESQLQueryColumns').mockResolvedValueOnce([]); + jest.spyOn(suggestionModule, 'suggestionsApi').mockReturnValue([ + { + title: 'MyTitle', + visualizationId: 'lnsXY', + datasourceId: 'form_based', + datasourceState: {}, + visualizationState: {}, + columns: 1, + score: 1, + previewIcon: 'icon', + changeType: 'initial', + keptLayerIds: [], + }, + ]); + + await expectRerenderOnDataLoader( + async ({ internalApi }) => { + expect(internalApi.updateAttributes).toHaveBeenCalled(); + return false; + }, + undefined, + getTestBaseOverrides() + ); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index 952e41112369d..0e5c9fca8929c 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -22,8 +22,21 @@ import { } from 'rxjs'; import fastIsEqual from 'fast-deep-equal'; import { pick } from 'lodash'; +import { + getESQLAdHocDataview, + getESQLQueryColumns, + getIndexForESQLQuery, + getInitialESQLQuery, +} from '@kbn/esql-utils'; +import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; import { getEditPath } from '../../common/constants'; -import type { GetStateType, LensApi, LensInternalApi, LensPublicCallbacks } from './types'; +import type { + GetStateType, + LensApi, + LensInternalApi, + LensPublicCallbacks, + LensRuntimeState, +} from './types'; import { getExpressionRendererParams } from './expressions/expression_params'; import type { LensEmbeddableStartServices } from './types'; import { prepareCallbacks } from './expressions/callbacks'; @@ -35,6 +48,8 @@ import { getRenderMode, getParentContext } from './helper'; import { addLog } from './logger'; import { getUsedDataViews } from './expressions/update_data_views'; import { getMergedSearchContext } from './expressions/merged_search_context'; +import { isESQLModeEnabled } from './initializers/utils'; +import { suggestionsApi } from '../lens_suggestions_api'; const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [ 'visualization', @@ -69,12 +84,82 @@ function getSearchContext(parentApi: unknown, esqlVariables: ESQLControlVariable }; } +async function loadESQLAttributes( + { dataViews, data, visualizationMap, datasourceMap, ...rest }: LensEmbeddableStartServices, + updateAttributes: (attributes: LensRuntimeState['attributes']) => void +) { + console.log('init'); + // Early exit if ESQL is not supported + if (!isESQLModeEnabled(rest)) { + return; + } + console.log('has esql'); + const indexName = await getIndexForESQLQuery({ dataViews }); + // Early exit if there's no data view to use + if (!indexName) { + return; + } + console.log('has index'); + + const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews); + + console.log('has dataView'); + + const esqlQuery = getInitialESQLQuery(dataView); + + const defaultEsqlQuery = { + esql: esqlQuery, + }; + + // For the suggestions api we need only the columns + // so we are requesting them with limit 0 + // this is much more performant than requesting + // all the table + const abortController = new AbortController(); + const columns = await getESQLQueryColumns({ + esqlQuery, + search: data.search.search, + signal: abortController.signal, + timeRange: data.query.timefilter.timefilter.getAbsoluteTime(), + }); + + const context = { + dataViewSpec: dataView.toSpec(false), + fieldName: '', + textBasedColumns: columns, + query: defaultEsqlQuery, + }; + + // get the initial attributes from the suggestions api + const allSuggestions = + suggestionsApi({ context, dataView, datasourceMap, visualizationMap }) ?? []; + + console.log({ allSuggestions }); + // Lens might not return suggestions for some cases, i.e. in case of errors + if (!allSuggestions.length) { + return; + } + const [firstSuggestion] = allSuggestions; + const newAttributes = getLensAttributesFromSuggestion({ + filters: [], + query: defaultEsqlQuery, + suggestion: { + ...firstSuggestion, + title: '', // when creating a new panel, we don't want to use the title from the suggestion + }, + dataView, + }); + + // time to update the existing attributes$ + updateAttributes(newAttributes); +} + /** * The function computes the expression used to render the panel and produces the necessary props * for the ExpressionWrapper component, binding any outer context to them. * @returns */ -export function loadEmbeddableData( +export async function loadEmbeddableData( uuid: string, getState: GetStateType, api: LensApi, @@ -302,6 +387,13 @@ export function loadEmbeddableData( } }), ]; + + // At this point everything has been checked, so load the editor frame + // and update the attributes if it's a creation flow + if (internalApi.isNewlyCreated$.getValue()) { + await loadESQLAttributes(services, internalApi.updateAttributes); + } + // There are few key moments when errors are checked and displayed: // * at setup time (here) before the first expression evaluation // * at runtime => when the expression is running and ES/Kibana server could emit errors) diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx index 9b170f24bd2a3..a39a12ee43884 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx @@ -14,7 +14,6 @@ import { apiHasAppContext, apiPublishesDisabledActionIds, } from '@kbn/presentation-publishing'; -import { ENABLE_ESQL } from '@kbn/esql-utils'; import { noop } from 'lodash'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { tracksOverlays } from '@kbn/presentation-containers'; @@ -39,6 +38,7 @@ import { mountInlineEditPanel } from '../inline_editing/mount'; import { StateManagementConfig } from './initialize_state_management'; import { apiPublishesInlineEditingCapabilities } from '../type_guards'; import { SearchContextConfig } from './initialize_search_context'; +import { isESQLModeEnabled } from './utils'; function getSupportedTriggers( getState: GetStateType, @@ -78,8 +78,6 @@ export function initializeEditApi( } { const supportedTriggers = getSupportedTriggers(getState, startDependencies.visualizationMap); - const isESQLModeEnabled = () => uiSettings.get(ENABLE_ESQL); - const [viewMode$] = buildObservableVariable( extractInheritedViewModeObservable(parentApi) ); @@ -170,14 +168,14 @@ export function initializeEditApi( /** * The rest of the edit stuff */ - const { uiSettings, capabilities, data } = startDependencies; + const { capabilities, data } = startDependencies; const canEdit = () => { if (viewMode$.getValue() !== 'edit') { return false; } // check if it's in ES|QL mode - if (isTextBasedLanguage(getState()) && !isESQLModeEnabled()) { + if (isTextBasedLanguage(getState()) && !isESQLModeEnabled(startDependencies)) { return false; } return ( 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 34e5f43d66ec2..490724d9ffee4 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 @@ -73,7 +73,7 @@ export function initializeInternalApi( apiPublishesESQLVariables(parentApi) ? parentApi.esqlVariables$ : [] ); - const query = initialState.attributes.state.query; + const query = initialState.attributes?.state?.query; const panelEsqlVariables$ = new BehaviorSubject([]); esqlVariables$.subscribe((newVariables) => { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/utils.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/utils.ts index c054c8dd67c93..1a6bedef8ba27 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/utils.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/utils.ts @@ -6,7 +6,12 @@ */ import { type AggregateQuery, type Query, isOfAggregateQueryType } from '@kbn/es-query'; import type { ESQLControlVariable } from '@kbn/esql-validation-autocomplete'; -import { getESQLQueryVariables } from '@kbn/esql-utils'; +import { ENABLE_ESQL, getESQLQueryVariables } from '@kbn/esql-utils'; +import { LensEmbeddableStartServices } from '../types'; + +export function isESQLModeEnabled({ uiSettings }: Pick) { + return uiSettings.get(ENABLE_ESQL); +} export function getEmbeddableVariables( query: Query | AggregateQuery, 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..0456fe0ab5b21 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 @@ -40,11 +40,10 @@ export function prepareInlineEditPanel( | 'expressionRenderer' | 'documentToExpression' | 'injectFilterReferences' - | 'visualizationMap' - | 'datasourceMap' | 'theme' | 'uiSettings' | 'attributeService' + | 'loadEditorFrame' >, navigateToLensEditor?: ( stateTransfer: EmbeddableStateTransfer, @@ -58,12 +57,6 @@ export function prepareInlineEditPanel( onCancel, hideTimeFilterInfo, }: Partial> = {}) { - const { getEditLensConfiguration, getVisualizationMap, getDatasourceMap } = await import( - '../../async_services' - ); - const visualizationMap = getVisualizationMap(); - const datasourceMap = getDatasourceMap(); - const currentState = getState(); const attributes = currentState.attributes as TypedLensSerializedState['attributes']; const activeDatasourceId = (getActiveDatasourceIdFromDoc(attributes) || @@ -78,19 +71,20 @@ export function prepareInlineEditPanel( savedObjectId: resetId ? undefined : currentState.savedObjectId, }); }, - visualizationMap, - datasourceMap, + startDependencies.visualizationMap, + startDependencies.datasourceMap, startDependencies.data.query.filterManager.extract ); const updateByRefInput = (savedObjectId: LensRuntimeState['savedObjectId']) => { updateState({ attributes, savedObjectId }); }; + const { getEditLensConfiguration } = await import('../../async_services'); const Component = await getEditLensConfiguration( coreStart, startDependencies, - visualizationMap, - datasourceMap + startDependencies.visualizationMap, + startDependencies.datasourceMap ); if (attributes?.visualizationType == null) { 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..d5e04cf1da1d3 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 @@ -167,7 +167,7 @@ export const createLensEmbeddableFactory = ( // Compute the expression using the provided parameters // Inside a subscription will be updated based on each unifiedSearch change // and as side effect update few observables as expressionParams$, expressionAbortController$ and renderCount$ with the new values upon updates - const expressionConfig = loadEmbeddableData( + const expressionConfig = await loadEmbeddableData( uuid, getState, api, 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..dbe96c3725398 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 { LensApi } from '../../react_embeddable/types'; +import { generateId } from '../../id_generator'; 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,18 @@ 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, + const lensEmbeddable = await embeddable.addNewPanel({ + panelType: 'lens', + initialState: { + id: generateId(), + isNewPanel: true, + }, }); + lensEmbeddable?.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 deleted file mode 100644 index 6f875e49f160c..0000000000000 --- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 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 '../..'; - -// datasourceMap and visualizationMap setters/getters -export const [getVisualizationMap, setVisualizationMap] = createGetterSetter< - Record> ->('VisualizationMap', false); - -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(); - } - - let visualizationMap = getVisualizationMap(); - let datasourceMap = getDatasourceMap(); - - if (!visualizationMap || !datasourceMap) { - [visualizationMap, datasourceMap] = await Promise.all([ - editorFrameService.loadVisualizations(), - editorFrameService.loadDatasources(), - ]); - - if (!visualizationMap && !datasourceMap) { - throw new IncompatibleActionError(); - } - - // persist for retrieval elsewhere - setDatasourceMap(datasourceMap); - setVisualizationMap(visualizationMap); - } - - const esqlQuery = getInitialESQLQuery(dataView); - - const defaultEsqlQuery = { - esql: esqlQuery, - }; - - // For the suggestions api we need only the columns - // so we are requesting them with limit 0 - // this is much more performant than requesting - // all the table - const abortController = new AbortController(); - const columns = await getESQLQueryColumns({ - esqlQuery, - search: deps.data.search.search, - signal: abortController.signal, - timeRange: deps.data.query.timefilter.timefilter.getAbsoluteTime(), - }); - - const context = { - dataViewSpec: dataView.toSpec(false), - fieldName: '', - textBasedColumns: columns, - query: defaultEsqlQuery, - }; - - // get the initial attributes from the suggestions api - const allSuggestions = - suggestionsApi({ context, dataView, datasourceMap, visualizationMap }) ?? []; - - // 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({ - filters: [], - query: defaultEsqlQuery, - suggestion: { - ...firstSuggestion, - title: '', // when creating a new panel, we don't want to use the title from the suggestion - }, - 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?.(); -} diff --git a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx index 1e1ab4cacff26..7b6ab69d56ebc 100644 --- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx @@ -6,16 +6,19 @@ */ import type { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; -import type { LensPluginStartDependencies } from '../../../plugin'; -import { createMockStartDependencies } from '../../../editor_frame_service/mocks'; import { EditLensEmbeddableAction } from './in_app_embeddable_edit_action'; import { TypedLensSerializedState } from '../../../react_embeddable/types'; import { BehaviorSubject } from 'rxjs'; +import { makeEmbeddableServices } from '../../../react_embeddable/mocks'; describe('inapp editing of Lens embeddable', () => { const core = coreMock.createStart(); - const mockStartDependencies = - createMockStartDependencies() as unknown as LensPluginStartDependencies; + const mockStartDependencies = makeEmbeddableServices(new BehaviorSubject(''), undefined, { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'form_based' }, + }); + + const lazyLoadServices = jest.fn().mockResolvedValue(mockStartDependencies); const renderComplete$ = new BehaviorSubject(false); @@ -35,7 +38,7 @@ describe('inapp editing of Lens embeddable', () => { references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], } as TypedLensSerializedState['attributes']; it('is incompatible for ESQL charts and if ui setting for ES|QL is off', async () => { - const inAppEditAction = new EditLensEmbeddableAction(mockStartDependencies, core); + const inAppEditAction = new EditLensEmbeddableAction(core, lazyLoadServices); const context = { attributes, lensEvent: { @@ -60,7 +63,7 @@ describe('inapp editing of Lens embeddable', () => { }, }, } as CoreStart; - const inAppEditAction = new EditLensEmbeddableAction(mockStartDependencies, updatedCore); + const inAppEditAction = new EditLensEmbeddableAction(updatedCore, lazyLoadServices); const context = { attributes, lensEvent: { @@ -76,7 +79,7 @@ describe('inapp editing of Lens embeddable', () => { }); it('is compatible for dataview charts', async () => { - const inAppEditAction = new EditLensEmbeddableAction(mockStartDependencies, core); + const inAppEditAction = new EditLensEmbeddableAction(core, lazyLoadServices); const newAttributes = { ...attributes, state: { diff --git a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx index 74ffac32605be..9f324be31dddb 100644 --- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx +++ b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.tsx @@ -6,20 +6,34 @@ */ import { i18n } from '@kbn/i18n'; import type { CoreStart } from '@kbn/core/public'; -import { Action } from '@kbn/ui-actions-plugin/public'; -import type { LensPluginStartDependencies } from '../../../plugin'; +import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { ENABLE_ESQL } from '@kbn/esql-utils'; +import type { + LensEmbeddableStartServices, + TypedLensByValueInput, +} from '../../../react_embeddable/types'; import type { InlineEditLensEmbeddableContext } from './types'; const ACTION_EDIT_LENS_EMBEDDABLE = 'ACTION_EDIT_LENS_EMBEDDABLE'; +export function isEmbeddableEditActionCompatible( + core: CoreStart, + attributes: TypedLensByValueInput['attributes'] +) { + // for ES|QL is compatible only when advanced setting is enabled + const query = attributes.state.query; + return isOfAggregateQueryType(query) ? core.uiSettings.get(ENABLE_ESQL) : true; +} + export class EditLensEmbeddableAction implements Action { public type = ACTION_EDIT_LENS_EMBEDDABLE; public id = ACTION_EDIT_LENS_EMBEDDABLE; public order = 50; constructor( - protected readonly startDependencies: LensPluginStartDependencies, - protected readonly core: CoreStart + protected readonly core: CoreStart, + protected readonly loadEmbeddableServices: () => Promise ) {} public getDisplayName(): string { @@ -33,7 +47,6 @@ export class EditLensEmbeddableAction implements Action {}; -export function isEmbeddableEditActionCompatible( - core: CoreStart, - attributes: TypedLensByValueInput['attributes'] -) { - // for ES|QL is compatible only when advanced setting is enabled - const query = attributes.state.query; - return isOfAggregateQueryType(query) ? core.uiSettings.get(ENABLE_ESQL) : true; -} - export async function executeEditEmbeddableAction({ deps, core, @@ -40,7 +31,7 @@ export async function executeEditEmbeddableAction({ onApply, onCancel, }: { - deps: LensPluginStartDependencies; + deps: LensEmbeddableStartServices; core: CoreStart; attributes: TypedLensByValueInput['attributes']; lensEvent: LensChartLoadEvent; @@ -49,11 +40,6 @@ export async function executeEditEmbeddableAction({ onApply?: (newAttributes: TypedLensByValueInput['attributes']) => void; onCancel?: () => void; }) { - const isCompatibleAction = isEmbeddableEditActionCompatible(core, attributes); - if (!isCompatibleAction) { - throw new IncompatibleActionError(); - } - const uuid = generateId(); const isNewlyCreated$ = new BehaviorSubject(false); const panelManagementApi = setupPanelManagement(uuid, container, { @@ -80,7 +66,7 @@ export async function executeEditEmbeddableAction({ closeInspector: asyncNoop, adapters$: new BehaviorSubject(lensEvent?.adapters), }, - { coreStart: core, ...deps } + deps ); const ConfigPanel = await openInlineEditor({ From bea5334447b34d889efb56cce7e17cdc63bbf00d Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 6 Feb 2025 18:23:01 +0100 Subject: [PATCH 2/6] :fire: Remove loggers --- .../shared/lens/public/react_embeddable/data_loader.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index 0e5c9fca8929c..1d8816c83c5f3 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -88,23 +88,18 @@ async function loadESQLAttributes( { dataViews, data, visualizationMap, datasourceMap, ...rest }: LensEmbeddableStartServices, updateAttributes: (attributes: LensRuntimeState['attributes']) => void ) { - console.log('init'); // Early exit if ESQL is not supported if (!isESQLModeEnabled(rest)) { return; } - console.log('has esql'); const indexName = await getIndexForESQLQuery({ dataViews }); // Early exit if there's no data view to use if (!indexName) { return; } - console.log('has index'); const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews); - console.log('has dataView'); - const esqlQuery = getInitialESQLQuery(dataView); const defaultEsqlQuery = { @@ -134,7 +129,6 @@ async function loadESQLAttributes( const allSuggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap }) ?? []; - console.log({ allSuggestions }); // Lens might not return suggestions for some cases, i.e. in case of errors if (!allSuggestions.length) { return; From d5590054afd047a008abe44cde3fff494afa8957 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 6 Feb 2025 18:37:46 +0100 Subject: [PATCH 3/6] :bug: Fix initial error blip --- .../lens/public/react_embeddable/data_loader.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index 1d8816c83c5f3..d8408ee890fe5 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -162,6 +162,15 @@ export async function loadEmbeddableData( services: LensEmbeddableStartServices, metaInfo?: SharingSavedObjectProps ) { + // if it's a new ES|QL panel, async load the correct attributes + // before subscribe to the apis + if (internalApi.isNewlyCreated$.getValue()) { + await loadESQLAttributes(services, (attributes: LensRuntimeState['attributes']) => { + // TODO: merge these two calls + internalApi.updateAttributes(attributes); + internalApi.updateVisualizationContext({ activeAttributes: attributes }); + }); + } const { onLoad, onBeforeBadgesRender, ...callbacks } = apiHasLensComponentCallbacks(parentApi) ? parentApi : ({} as LensPublicCallbacks); @@ -382,12 +391,6 @@ export async function loadEmbeddableData( }), ]; - // At this point everything has been checked, so load the editor frame - // and update the attributes if it's a creation flow - if (internalApi.isNewlyCreated$.getValue()) { - await loadESQLAttributes(services, internalApi.updateAttributes); - } - // There are few key moments when errors are checked and displayed: // * at setup time (here) before the first expression evaluation // * at runtime => when the expression is running and ES/Kibana server could emit errors) From c23e3eb2027b7ab48e69c7b65efe22218fb10f1c Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 7 Feb 2025 17:53:54 +0100 Subject: [PATCH 4/6] :recycle: Refactor into deserialize --- .../plugins/shared/lens/public/plugin.ts | 4 +- .../react_embeddable/data_loader.test.ts | 84 ------------- .../public/react_embeddable/data_loader.ts | 112 ++++-------------- .../lens/public/react_embeddable/esql.test.ts | 77 ++++++++++++ .../lens/public/react_embeddable/esql.ts | 84 +++++++++++++ .../public/react_embeddable/helper.test.ts | 50 ++++---- .../lens/public/react_embeddable/helper.ts | 37 +++++- .../initializers/initialize_internal_api.ts | 41 +++---- .../react_embeddable/lens_embeddable.tsx | 4 +- .../open_lens_config/create_action.tsx | 9 +- 10 files changed, 269 insertions(+), 233 deletions(-) create mode 100644 x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.test.ts create mode 100644 x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts index dc56a7cbd24c7..ad878a1605539 100644 --- a/x-pack/platform/plugins/shared/lens/public/plugin.ts +++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts @@ -402,12 +402,12 @@ export class LensPlugin { // Let Dashboard know about the Lens panel type embeddable.registerAddFromLibraryType({ onAdd: async (container, savedObject) => { - const { attributeService } = await this.getStartServicesForEmbeddable(startServices); + const services = await this.getStartServicesForEmbeddable(startServices); // deserialize the saved object from visualize library // this make sure to fit into the new embeddable model, where the following build() // function expects a fully loaded runtime state const state = await deserializeState( - attributeService, + services, { savedObjectId: savedObject.id }, savedObject.references ); 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 ecef92449adc4..70a83f98dd1c0 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 @@ -35,15 +35,6 @@ import { import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; import { isObject } from 'lodash'; import { createMockDatasource, defaultDoc } from '../mocks'; -import { coreMock } from '@kbn/core/public/mocks'; -import * as suggestionModule from '../lens_suggestions_api'; -// Need to do this magic in order to spy on specific functions -import * as esqlUtils from '@kbn/esql-utils'; -import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -jest.mock('@kbn/esql-utils', () => ({ - __esModule: true, - ...jest.requireActual('@kbn/esql-utils'), -})); jest.mock('@kbn/interpreter', () => ({ toExpression: jest.fn().mockReturnValue('expression'), @@ -74,11 +65,6 @@ jest.mock('@kbn/presentation-publishing', () => { }; }); -function getUiSettingsOverrides() { - const core = coreMock.createStart({ basePath: '/testbasepath' }); - return core.uiSettings; -} - // In order to listen the reload function, we need to // monitor the internalApi dispatchRenderStart spy type ChangeFnType = ({ @@ -492,74 +478,4 @@ describe('Data Loader', () => { } ); }); - - describe('ES|QL creation flow', () => { - function getTestBaseOverrides() { - return { - servicesOverrides: { - uiSettings: { ...getUiSettingsOverrides(), get: jest.fn().mockReturnValue(true) }, - }, - internalApiOverrides: { - isNewlyCreated$: new BehaviorSubject(true), - }, - }; - } - it('should not update the attributes if no index is available', async () => { - jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce(null); - - await expectRerenderOnDataLoader( - async ({ internalApi }) => { - expect(internalApi.updateAttributes).not.toHaveBeenCalled(); - return false; - }, - undefined, - getTestBaseOverrides() - ); - }); - - it('should not update the attributes if no suggestion is generated', async () => { - jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce('index'); - jest.spyOn(esqlUtils, 'getESQLAdHocDataview').mockResolvedValueOnce(dataViewMock); - jest.spyOn(esqlUtils, 'getESQLQueryColumns').mockResolvedValueOnce([]); - jest.spyOn(suggestionModule, 'suggestionsApi').mockReturnValue([]); - - await expectRerenderOnDataLoader( - async ({ internalApi }) => { - expect(internalApi.updateAttributes).not.toHaveBeenCalled(); - return false; - }, - undefined, - getTestBaseOverrides() - ); - }); - - it('should update the attributes if there is a valid suggestion', async () => { - jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce('index'); - jest.spyOn(esqlUtils, 'getESQLAdHocDataview').mockResolvedValueOnce(dataViewMock); - jest.spyOn(esqlUtils, 'getESQLQueryColumns').mockResolvedValueOnce([]); - jest.spyOn(suggestionModule, 'suggestionsApi').mockReturnValue([ - { - title: 'MyTitle', - visualizationId: 'lnsXY', - datasourceId: 'form_based', - datasourceState: {}, - visualizationState: {}, - columns: 1, - score: 1, - previewIcon: 'icon', - changeType: 'initial', - keptLayerIds: [], - }, - ]); - - await expectRerenderOnDataLoader( - async ({ internalApi }) => { - expect(internalApi.updateAttributes).toHaveBeenCalled(); - return false; - }, - undefined, - getTestBaseOverrides() - ); - }); - }); }); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index d8408ee890fe5..3e09b465054f2 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -22,21 +22,8 @@ import { } from 'rxjs'; import fastIsEqual from 'fast-deep-equal'; import { pick } from 'lodash'; -import { - getESQLAdHocDataview, - getESQLQueryColumns, - getIndexForESQLQuery, - getInitialESQLQuery, -} from '@kbn/esql-utils'; -import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; import { getEditPath } from '../../common/constants'; -import type { - GetStateType, - LensApi, - LensInternalApi, - LensPublicCallbacks, - LensRuntimeState, -} from './types'; +import type { GetStateType, LensApi, LensInternalApi, LensPublicCallbacks } from './types'; import { getExpressionRendererParams } from './expressions/expression_params'; import type { LensEmbeddableStartServices } from './types'; import { prepareCallbacks } from './expressions/callbacks'; @@ -44,12 +31,11 @@ import { buildUserMessagesHelpers } from './user_messages/api'; import { getLogError } from './expressions/telemetry'; import type { SharingSavedObjectProps, UserMessagesDisplayLocationId } from '../types'; import { apiHasLensComponentCallbacks } from './type_guards'; -import { getRenderMode, getParentContext } from './helper'; +import { getRenderMode, getParentContext, buildObservableVariable } from './helper'; import { addLog } from './logger'; import { getUsedDataViews } from './expressions/update_data_views'; import { getMergedSearchContext } from './expressions/merged_search_context'; -import { isESQLModeEnabled } from './initializers/utils'; -import { suggestionsApi } from '../lens_suggestions_api'; +import { getEmbeddableVariables } from './initializers/utils'; const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [ 'visualization', @@ -84,68 +70,15 @@ function getSearchContext(parentApi: unknown, esqlVariables: ESQLControlVariable }; } -async function loadESQLAttributes( - { dataViews, data, visualizationMap, datasourceMap, ...rest }: LensEmbeddableStartServices, - updateAttributes: (attributes: LensRuntimeState['attributes']) => void +function buildESQLControlVariablesUpdater( + attributes$: LensInternalApi['attributes$'], + updatePanelVariables: (variables: ESQLControlVariable[]) => void ) { - // Early exit if ESQL is not supported - if (!isESQLModeEnabled(rest)) { - return; - } - const indexName = await getIndexForESQLQuery({ dataViews }); - // Early exit if there's no data view to use - if (!indexName) { - return; - } - - const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews); - - const esqlQuery = getInitialESQLQuery(dataView); - - const defaultEsqlQuery = { - esql: esqlQuery, - }; - - // For the suggestions api we need only the columns - // so we are requesting them with limit 0 - // this is much more performant than requesting - // all the table - const abortController = new AbortController(); - const columns = await getESQLQueryColumns({ - esqlQuery, - search: data.search.search, - signal: abortController.signal, - timeRange: data.query.timefilter.timefilter.getAbsoluteTime(), - }); - - const context = { - dataViewSpec: dataView.toSpec(false), - fieldName: '', - textBasedColumns: columns, - query: defaultEsqlQuery, + return function updateESQLControlVariables(newVariables: ESQLControlVariable[]) { + const query = attributes$.getValue().state?.query; + const esqlVariables = getEmbeddableVariables(query, newVariables) ?? []; + updatePanelVariables(esqlVariables); }; - - // get the initial attributes from the suggestions api - const allSuggestions = - suggestionsApi({ context, dataView, datasourceMap, visualizationMap }) ?? []; - - // Lens might not return suggestions for some cases, i.e. in case of errors - if (!allSuggestions.length) { - return; - } - const [firstSuggestion] = allSuggestions; - const newAttributes = getLensAttributesFromSuggestion({ - filters: [], - query: defaultEsqlQuery, - suggestion: { - ...firstSuggestion, - title: '', // when creating a new panel, we don't want to use the title from the suggestion - }, - dataView, - }); - - // time to update the existing attributes$ - updateAttributes(newAttributes); } /** @@ -153,7 +86,7 @@ async function loadESQLAttributes( * for the ExpressionWrapper component, binding any outer context to them. * @returns */ -export async function loadEmbeddableData( +export function loadEmbeddableData( uuid: string, getState: GetStateType, api: LensApi, @@ -162,15 +95,12 @@ export async function loadEmbeddableData( services: LensEmbeddableStartServices, metaInfo?: SharingSavedObjectProps ) { - // if it's a new ES|QL panel, async load the correct attributes - // before subscribe to the apis - if (internalApi.isNewlyCreated$.getValue()) { - await loadESQLAttributes(services, (attributes: LensRuntimeState['attributes']) => { - // TODO: merge these two calls - internalApi.updateAttributes(attributes); - internalApi.updateVisualizationContext({ activeAttributes: attributes }); - }); - } + const [controlESQLVariables$] = buildObservableVariable([]); + const updateESQLControlVariables = buildESQLControlVariablesUpdater( + internalApi.attributes$, + (vars: ESQLControlVariable[]) => controlESQLVariables$.next(vars) + ); + const { onLoad, onBeforeBadgesRender, ...callbacks } = apiHasLensComponentCallbacks(parentApi) ? parentApi : ({} as LensPublicCallbacks); @@ -277,11 +207,9 @@ export async function loadEmbeddableData( callbacks ); - const esqlVariables = internalApi?.esqlVariables$?.getValue(); - const searchContext = getMergedSearchContext( currentState, - getSearchContext(parentApi, esqlVariables), + getSearchContext(parentApi, controlESQLVariables$?.getValue()), api.timeRange$, parentApi, services @@ -348,7 +276,7 @@ export async function loadEmbeddableData( const mergedSubscriptions = merge( // on search context change, reload fetch$(api).pipe(map(() => 'searchContext' as ReloadReason)), - internalApi?.esqlVariables$.pipe( + controlESQLVariables$.pipe( waitUntilChanged(), map(() => 'ESQLvariables' as ReloadReason) ), @@ -382,6 +310,8 @@ export async function loadEmbeddableData( const subscriptions: Subscription[] = [ mergedSubscriptions.pipe(debounceTime(0)).subscribe(reload), + // In case of changes to the dashboard ES|QL controls, re-map them + internalApi.esqlVariables$.subscribe(updateESQLControlVariables), // make sure to reload on viewMode change api.viewMode$.subscribe(() => { // only reload if drilldowns are set diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.test.ts new file mode 100644 index 0000000000000..86f947dd31cb8 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loadESQLAttributes } from './esql'; +import { makeEmbeddableServices } from './mocks'; +import { LensEmbeddableStartServices } from './types'; +import { coreMock } from '@kbn/core/public/mocks'; +import { BehaviorSubject } from 'rxjs'; +import * as suggestionModule from '../lens_suggestions_api'; +// Need to do this magic in order to spy on specific functions +import * as esqlUtils from '@kbn/esql-utils'; +import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +jest.mock('@kbn/esql-utils', () => ({ + __esModule: true, + ...jest.requireActual('@kbn/esql-utils'), +})); + +function getUiSettingsOverrides() { + const core = coreMock.createStart({ basePath: '/testbasepath' }); + return core.uiSettings; +} + +describe('ES|QL attributes creation', () => { + function getServices(servicesOverrides?: Partial) { + return { + ...makeEmbeddableServices(new BehaviorSubject(''), undefined, { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'form_based' }, + }), + uiSettings: { ...getUiSettingsOverrides(), get: jest.fn().mockReturnValue(true) }, + ...servicesOverrides, + }; + } + it('should not update the attributes if no index is available', async () => { + jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce(null); + + const attributes = await loadESQLAttributes(getServices()); + expect(attributes).toBeUndefined(); + }); + + it('should not update the attributes if no suggestion is generated', async () => { + jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce('index'); + jest.spyOn(esqlUtils, 'getESQLAdHocDataview').mockResolvedValueOnce(dataViewMock); + jest.spyOn(esqlUtils, 'getESQLQueryColumns').mockResolvedValueOnce([]); + jest.spyOn(suggestionModule, 'suggestionsApi').mockReturnValue([]); + + const attributes = await loadESQLAttributes(getServices()); + expect(attributes).toBeUndefined(); + }); + + it('should update the attributes if there is a valid suggestion', async () => { + jest.spyOn(esqlUtils, 'getIndexForESQLQuery').mockResolvedValueOnce('index'); + jest.spyOn(esqlUtils, 'getESQLAdHocDataview').mockResolvedValueOnce(dataViewMock); + jest.spyOn(esqlUtils, 'getESQLQueryColumns').mockResolvedValueOnce([]); + jest.spyOn(suggestionModule, 'suggestionsApi').mockReturnValue([ + { + title: 'MyTitle', + visualizationId: 'lnsXY', + datasourceId: 'form_based', + datasourceState: {}, + visualizationState: {}, + columns: 1, + score: 1, + previewIcon: 'icon', + changeType: 'initial', + keptLayerIds: [], + }, + ]); + + const attributes = await loadESQLAttributes(getServices()); + expect(attributes).not.toBeUndefined(); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts new file mode 100644 index 0000000000000..0319a860b2df5 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getIndexForESQLQuery, + getESQLAdHocDataview, + getInitialESQLQuery, + getESQLQueryColumns, +} from '@kbn/esql-utils'; +import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; +import { isESQLModeEnabled } from './initializers/utils'; +import type { LensEmbeddableStartServices } from './types'; +import { suggestionsApi } from '../lens_suggestions_api'; + +export async function loadESQLAttributes({ + dataViews, + data, + visualizationMap, + datasourceMap, + uiSettings, +}: Pick< + LensEmbeddableStartServices, + 'dataViews' | 'data' | 'visualizationMap' | 'datasourceMap' | 'uiSettings' +>) { + // Early exit if ESQL is not supported + if (!isESQLModeEnabled({ uiSettings })) { + return; + } + const indexName = await getIndexForESQLQuery({ dataViews }); + // Early exit if there's no data view to use + if (!indexName) { + return; + } + + const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews); + + const esqlQuery = getInitialESQLQuery(dataView); + + const defaultEsqlQuery = { + esql: esqlQuery, + }; + + // For the suggestions api we need only the columns + // so we are requesting them with limit 0 + // this is much more performant than requesting + // all the table + const abortController = new AbortController(); + const columns = await getESQLQueryColumns({ + esqlQuery, + search: data.search.search, + signal: abortController.signal, + timeRange: data.query.timefilter.timefilter.getAbsoluteTime(), + }); + + const context = { + dataViewSpec: dataView.toSpec(false), + fieldName: '', + textBasedColumns: columns, + query: defaultEsqlQuery, + }; + + // get the initial attributes from the suggestions api + const allSuggestions = + suggestionsApi({ context, dataView, datasourceMap, visualizationMap }) ?? []; + + // Lens might not return suggestions for some cases, i.e. in case of errors + if (!allSuggestions.length) { + return; + } + const [firstSuggestion] = allSuggestions; + return getLensAttributesFromSuggestion({ + filters: [], + query: defaultEsqlQuery, + suggestion: { + ...firstSuggestion, + title: '', // when creating a new panel, we don't want to use the title from the suggestion + }, + dataView, + }); +} diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts index 33a8d0d0093d4..c25fe75756dc0 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.test.ts @@ -5,23 +5,31 @@ * 2.0. */ -import { defaultDoc, makeAttributeService } from '../mocks/services_mock'; +import { BehaviorSubject } from 'rxjs'; +import { defaultDoc } from '../mocks/services_mock'; import { deserializeState } from './helper'; +import { makeEmbeddableServices } from './mocks'; describe('Embeddable helpers', () => { describe('deserializeState', () => { + function getServices() { + return makeEmbeddableServices(new BehaviorSubject(''), undefined, { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'form_based' }, + }); + } it('should forward a by value raw state', async () => { - const attributeService = makeAttributeService(defaultDoc); + const services = getServices(); const rawState = { attributes: defaultDoc, }; - const runtimeState = await deserializeState(attributeService, rawState); + const runtimeState = await deserializeState(services, rawState); expect(runtimeState).toEqual(rawState); }); it('should wrap Lens doc/attributes into component state shape', async () => { - const attributeService = makeAttributeService(defaultDoc); - const runtimeState = await deserializeState(attributeService, defaultDoc); + const services = getServices(); + const runtimeState = await deserializeState(services, defaultDoc); expect(runtimeState).toEqual( expect.objectContaining({ attributes: { ...defaultDoc, references: defaultDoc.references }, @@ -30,18 +38,20 @@ describe('Embeddable helpers', () => { }); it('load a by-ref doc from the attribute service', async () => { - const attributeService = makeAttributeService(defaultDoc); - await deserializeState(attributeService, { + const services = getServices(); + await deserializeState(services, { savedObjectId: '123', }); - expect(attributeService.loadFromLibrary).toHaveBeenCalledWith('123'); + expect(services.attributeService.loadFromLibrary).toHaveBeenCalledWith('123'); }); it('should fallback to an empty Lens doc if the saved object is not found', async () => { - const attributeService = makeAttributeService(defaultDoc); - attributeService.loadFromLibrary.mockRejectedValueOnce(new Error('not found')); - const runtimeState = await deserializeState(attributeService, { + const services = getServices(); + services.attributeService.loadFromLibrary = jest + .fn() + .mockRejectedValueOnce(new Error('not found')); + const runtimeState = await deserializeState(services, { savedObjectId: '123', }); // check the visualizationType set to null for empty state @@ -55,51 +65,51 @@ describe('Embeddable helpers', () => { // * other space for a by-value with new ref ids it('should inject correctly serialized references into runtime state for a by value in the default space', async () => { - const attributeService = makeAttributeService(defaultDoc); + const services = getServices(); const mockedReferences = [ { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, ]; const runtimeState = await deserializeState( - attributeService, + services, { attributes: defaultDoc, }, mockedReferences ); - expect(attributeService.injectReferences).toHaveBeenCalled(); + expect(services.attributeService.injectReferences).toHaveBeenCalled(); expect(runtimeState.attributes.references).toEqual(mockedReferences); }); it('should inject correctly serialized references into runtime state for a by ref in the default space', async () => { - const attributeService = makeAttributeService(defaultDoc); + const services = getServices(); const mockedReferences = [ { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, ]; const runtimeState = await deserializeState( - attributeService, + services, { savedObjectId: '123', }, mockedReferences ); - expect(attributeService.injectReferences).not.toHaveBeenCalled(); + expect(services.attributeService.injectReferences).not.toHaveBeenCalled(); // Note the original references should be kept expect(runtimeState.attributes.references).toEqual(defaultDoc.references); }); it('should inject correctly serialized references into runtime state for a by value in another space', async () => { - const attributeService = makeAttributeService(defaultDoc); + const services = getServices(); const mockedReferences = [ { id: 'serializedRefs', name: 'index-pattern-0', type: 'mocked-reference' }, ]; const runtimeState = await deserializeState( - attributeService, + services, { attributes: defaultDoc, }, mockedReferences ); - expect(attributeService.injectReferences).toHaveBeenCalled(); + expect(services.attributeService.injectReferences).toHaveBeenCalled(); // note: in this case the references are swapped expect(runtimeState.attributes.references).toEqual(mockedReferences); }); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts index a0f7121c5e2e7..9ad8506f1f4d3 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts @@ -19,8 +19,8 @@ import fastIsEqual from 'fast-deep-equal'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { RenderMode } from '@kbn/expressions-plugin/common'; import { SavedObjectReference } from '@kbn/core/types'; -import { LensRuntimeState, LensSerializedState } from './types'; -import type { LensAttributesService } from '../lens_attribute_service'; +import type { LensEmbeddableStartServices, LensRuntimeState, LensSerializedState } from './types'; +import { loadESQLAttributes } from './esql'; export function createEmptyLensState( visualizationType: null | string = null, @@ -51,10 +51,23 @@ export function createEmptyLensState( // Make sure to inject references from the container down to the runtime state // this ensure migrations/copy to spaces works correctly export async function deserializeState( - attributeService: LensAttributesService, + { + attributeService, + ...services + }: Pick< + LensEmbeddableStartServices, + | 'attributeService' + | 'data' + | 'dataViews' + | 'data' + | 'visualizationMap' + | 'datasourceMap' + | 'uiSettings' + >, rawState: LensSerializedState, references?: SavedObjectReference[] ) { + const fallbackAttributes = createEmptyLensState().attributes; if (rawState.savedObjectId) { try { const { attributes, managed, sharingSavedObjectProps } = @@ -62,14 +75,28 @@ export async function deserializeState( return { ...rawState, attributes, managed, sharingSavedObjectProps }; } catch (e) { // return an empty Lens document if no saved object is found - return { ...rawState, attributes: createEmptyLensState().attributes }; + return { ...rawState, attributes: fallbackAttributes }; } } // Inject applied only to by-value SOs - return attributeService.injectReferences( + const newState = attributeService.injectReferences( ('attributes' in rawState ? rawState : { attributes: rawState }) as LensRuntimeState, references?.length ? references : undefined ); + if (newState.isNewPanel) { + try { + const newAttributes = await loadESQLAttributes(services); + // provide a fallback + return { + ...newState, + attributes: newAttributes || newState.attributes || fallbackAttributes, + }; + } catch (e) { + // return an empty Lens document if no saved object is found + return { ...newState, attributes: fallbackAttributes }; + } + } + return newState; } export function emptySerializer() { 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 490724d9ffee4..a82a8ea93ba15 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 @@ -5,10 +5,8 @@ * 2.0. */ -import { BehaviorSubject } from 'rxjs'; import { initializeTitleManager } from '@kbn/presentation-publishing'; import { apiPublishesESQLVariables } from '@kbn/esql-variables-types'; -import type { ESQLControlVariable } from '@kbn/esql-validation-autocomplete'; import type { DataView } from '@kbn/data-views-plugin/common'; import { buildObservableVariable, createEmptyLensState } from '../helper'; import type { @@ -21,7 +19,6 @@ import type { } from '../types'; import { apiHasAbortController, apiHasLensComponentProps } from '../type_guards'; import type { UserMessage } from '../../types'; -import { getEmbeddableVariables } from './utils'; export function initializeInternalApi( initialState: LensRuntimeState, @@ -31,33 +28,33 @@ export function initializeInternalApi( ): LensInternalApi { const [hasRenderCompleted$] = buildObservableVariable(false); const [expressionParams$] = buildObservableVariable(null); - const expressionAbortController$ = new BehaviorSubject(undefined); - if (apiHasAbortController(parentApi)) { - expressionAbortController$.next(parentApi.abortController); - } + const [expressionAbortController$] = buildObservableVariable( + apiHasAbortController(parentApi) ? parentApi.abortController : undefined + ); + const [renderCount$] = buildObservableVariable(0); - const attributes$ = new BehaviorSubject( + const [attributes$] = buildObservableVariable( initialState.attributes || createEmptyLensState().attributes ); - const overrides$ = new BehaviorSubject(initialState.overrides); - const disableTriggers$ = new BehaviorSubject(initialState.disableTriggers); - const dataLoading$ = new BehaviorSubject(undefined); + const [overrides$] = buildObservableVariable(initialState.overrides); + const [disableTriggers$] = buildObservableVariable(initialState.disableTriggers); + const [dataLoading$] = buildObservableVariable(undefined); - const dataViews$ = new BehaviorSubject(undefined); + const [dataViews$] = buildObservableVariable(undefined); // This is an internal error state, not to be confused with the runtime error state thrown by the expression pipeline // In both cases a blocking error can happen, but for Lens validation errors we want to have full control over the UI // while for runtime errors the error will bubble up to the embeddable presentation layer - const validationMessages$ = new BehaviorSubject([]); + const [validationMessages$] = buildObservableVariable([]); // This other set of messages is for non-blocking messages that can be displayed in the UI - const messages$ = new BehaviorSubject([]); + const [messages$] = buildObservableVariable([]); // This should settle the thing once and for all // the isNewPanel won't be serialized so it will be always false after the edit panel closes applying the changes - const isNewlyCreated$ = new BehaviorSubject(initialState.isNewPanel || false); + const [isNewlyCreated$] = buildObservableVariable(initialState.isNewPanel || false); - const blockingError$ = new BehaviorSubject(undefined); - const visualizationContext$ = new BehaviorSubject({ + const [blockingError$] = buildObservableVariable(undefined); + const [visualizationContext$] = buildObservableVariable({ // doc can point to a different set of attributes for the visualization // i.e. when inline editing or applying a suggestion activeAttributes: initialState.attributes, @@ -73,21 +70,13 @@ export function initializeInternalApi( apiPublishesESQLVariables(parentApi) ? parentApi.esqlVariables$ : [] ); - const query = initialState.attributes?.state?.query; - - const panelEsqlVariables$ = new BehaviorSubject([]); - esqlVariables$.subscribe((newVariables) => { - const esqlVariables = getEmbeddableVariables(query, newVariables) ?? []; - panelEsqlVariables$.next(esqlVariables); - }); - // No need to expose anything at public API right now, that would happen later on // where each initializer will pick what it needs and publish it return { attributes$, overrides$, disableTriggers$, - esqlVariables$: panelEsqlVariables$, + esqlVariables$, dataLoading$, hasRenderCompleted$, expressionParams$, 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 d5e04cf1da1d3..29c0d7a7a5653 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 @@ -38,7 +38,7 @@ export const createLensEmbeddableFactory = ( * final state will contain the attributes object */ deserializeState: async ({ rawState, references }) => - deserializeState(services.attributeService, rawState, references), + deserializeState(services, rawState, references), /** * This is called after the deserialize, so some assumptions can be made about its arguments: * @param state the Lens "runtime" state, which means that 'attributes' is always present. @@ -167,7 +167,7 @@ export const createLensEmbeddableFactory = ( // Compute the expression using the provided parameters // Inside a subscription will be updated based on each unifiedSearch change // and as side effect update few observables as expressionParams$, expressionAbortController$ and renderCount$ with the new values upon updates - const expressionConfig = await loadEmbeddableData( + const expressionConfig = loadEmbeddableData( uuid, getState, api, 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 dbe96c3725398..cef053dd649ce 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 @@ -45,9 +45,12 @@ export class CreateESQLPanelAction implements Action { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); const lensEmbeddable = await embeddable.addNewPanel({ panelType: 'lens', - initialState: { - id: generateId(), - isNewPanel: true, + serializedState: { + rawState: { + id: generateId(), + isNewPanel: true, + attributes: { references: [] }, + }, }, }); lensEmbeddable?.onEdit(); From 913f8a129bb8cb34c9ba8f6e5a765d3628c755ce Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 10 Feb 2025 14:16:24 +0100 Subject: [PATCH 5/6] :recycle: Restore non-async --- .../shared/lens/public/react_embeddable/data_loader.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 70a83f98dd1c0..995e944d564c0 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 @@ -133,7 +133,7 @@ async function expectRerenderOnDataLoader( documentToExpression: jest.fn().mockResolvedValue({ ast: 'expression_string' }), ...servicesOverrides, }; - const { cleanup } = await loadEmbeddableData( + const { cleanup } = loadEmbeddableData( faker.string.uuid(), getState, api, From c4f0cc4f17d449cb4a376f4f46e92fa637e9ea1c Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 10 Feb 2025 14:22:31 +0100 Subject: [PATCH 6/6] :fire: Remove old cruft --- .../react_embeddable/inline_editing/setup_inline_editing.tsx | 1 - 1 file changed, 1 deletion(-) 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 0456fe0ab5b21..b1e418cf0791d 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 @@ -43,7 +43,6 @@ export function prepareInlineEditPanel( | 'theme' | 'uiSettings' | 'attributeService' - | 'loadEditorFrame' >, navigateToLensEditor?: ( stateTransfer: EmbeddableStateTransfer,