[Lens][Embeddable] Make UI react faster to click actions like create or edit (#210810)

## Summary

This PR is based on the idea in #209361 and tries to improve perceived
performances for all the scenarios where the `editorFrame` is loaded.

On fast connections this is now perceived very fast:

![esql_fast](https://github.com/user-attachments/assets/efb26416-bf15-449e-912f-a689c689c593)

On Fast 4g is still fast

![esql_fast_4g](https://github.com/user-attachments/assets/acc199be-683d-4a4b-a53c-f37a9117c258)

On Slow 4g is acceptable


![esql_slow_4g](https://github.com/user-attachments/assets/6fed9ec4-dc3f-4557-976c-91d82bddc10f)

Even on 3G connection the feedback is much better now


![esql_3g](https://github.com/user-attachments/assets/27e96c01-9149-4dd1-8a6d-e005202149ff)

As a bonus extra tests have been added for the ES|QL creation flow.

cc @thomasneirynck @nreese 

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Nick Partridge <nick.ryan.partridge@gmail.com>
This commit is contained in:
Marco Liberati 2025-02-20 18:19:46 +01:00 committed by GitHub
parent e2730f70db
commit 1e92ae8afb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 301 additions and 232 deletions

View file

@ -93,7 +93,7 @@ pageLoadAssetSize:
kibanaReact: 74422
kibanaUsageCollection: 16463
kibanaUtils: 79713
lens: 57135
lens: 76079
licenseManagement: 41817
licensing: 29004
links: 8000

View file

@ -49,7 +49,6 @@ 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';
export { getAddLensPanelAction } from './trigger_actions/add_lens_panel_action';
export { AddESQLPanelAction } from './trigger_actions/open_lens_config/add_esql_panel_action';

View file

@ -38,6 +38,7 @@ function createMockTimefilter() {
getRefreshInterval: () => {},
getRefreshIntervalDefaults: () => {},
getAutoRefreshFetch$: () => new Observable(),
getAbsoluteTime: jest.fn(),
};
}

View file

@ -398,12 +398,12 @@ export class LensPlugin {
// Let Dashboard know about the Lens panel type
embeddable.registerAddFromLibraryType<LensSavedObjectAttributes>({
onAdd: async (container, savedObject) => {
const { attributeService } = await getStartServicesForEmbeddable();
const services = await getStartServicesForEmbeddable();
// 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
);
@ -542,9 +542,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 +672,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, async () => {
const { visualizationMap, datasourceMap } = await this.initEditorFrameService();
return { ...startDependencies, visualizationMap, datasourceMap };
});
// embeddable inline edit panel action
startDependencies.uiActions.addTriggerAction(
IN_APP_EMBEDDABLE_EDIT_TRIGGER,
@ -687,13 +687,7 @@ export class LensPlugin {
ACTION_CREATE_ESQL_CHART,
async () => {
const { AddESQLPanelAction } = await import('./async_services');
return new AddESQLPanelAction(startDependencies, core, async () => {
if (!this.editorFrameService) {
await this.initEditorFrameService();
}
return this.editorFrameService!;
});
return new AddESQLPanelAction(core);
}
);
startDependencies.uiActions.registerActionAsync('addLensPanelAction', async () => {

View file

@ -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<LensEmbeddableStartServices>) {
return {
...makeEmbeddableServices(new BehaviorSubject<string>(''), 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();
});
});

View file

@ -0,0 +1,88 @@
/*
* 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';
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;
}
// From this moment on there are no longer early exists before suggestions
// so make sure to load async modules while doing other async stuff to save some time
const [dataView, { suggestionsApi }] = await Promise.all([
getESQLAdHocDataview(`from ${indexName}`, dataViews),
import('../async_services'),
]);
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,
});
}

View file

@ -4,24 +4,31 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 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<string>(''), 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 +37,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 +64,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);
});

View file

@ -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() {

View file

@ -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 type { LensEmbeddableStartServices } from '../types';
export function isESQLModeEnabled({ uiSettings }: Pick<LensEmbeddableStartServices, 'uiSettings'>) {
return uiSettings.get(ENABLE_ESQL);
}
export function getEmbeddableVariables(
query: Query | AggregateQuery,

View file

@ -31,6 +31,8 @@ export function prepareInlineEditPanel(
inspectorApi: LensInspectorAdapters,
{
coreStart,
visualizationMap,
datasourceMap,
...startDependencies
}: Omit<
LensEmbeddableStartServices,
@ -40,8 +42,6 @@ export function prepareInlineEditPanel(
| 'expressionRenderer'
| 'documentToExpression'
| 'injectFilterReferences'
| 'visualizationMap'
| 'datasourceMap'
| 'theme'
| 'uiSettings'
| 'attributeService'
@ -58,11 +58,7 @@ export function prepareInlineEditPanel(
onCancel,
hideTimeFilterInfo,
}: Partial<Pick<EditConfigPanelProps, 'onApply' | 'onCancel' | 'hideTimeFilterInfo'>> = {}) {
const { getEditLensConfiguration, getVisualizationMap, getDatasourceMap } = await import(
'../../async_services'
);
const visualizationMap = getVisualizationMap();
const datasourceMap = getDatasourceMap();
const { getEditLensConfiguration } = await import('../../async_services');
const currentState = getState();
const attributes = currentState.attributes as TypedLensSerializedState['attributes'];

View file

@ -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.

View file

@ -7,27 +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 { AddESQLPanelAction } from './add_esql_panel_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 action = new AddESQLPanelAction(mockStartDependencies, core, mockGetEditorFrameService);
const action = new AddESQLPanelAction(core);
const isCompatible = await action.isCompatible({
embeddable: mockPresentationContainer,
@ -47,11 +35,7 @@ describe('create Lens panel action', () => {
},
} as CoreStart;
const action = new AddESQLPanelAction(
mockStartDependencies,
updatedCore,
mockGetEditorFrameService
);
const action = new AddESQLPanelAction(updatedCore);
const isCompatible = await action.isCompatible({
embeddable: mockPresentationContainer,

View file

@ -10,10 +10,10 @@ 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 { ACTION_CREATE_ESQL_CHART } from './constants';
import { executeCreateAction, isCreateActionCompatible } from '../../async_services';
import { generateId } from '../../id_generator';
import type { LensApi } from '../../react_embeddable/types';
export class AddESQLPanelAction implements Action<EmbeddableApiContext> {
public type = ACTION_CREATE_ESQL_CHART;
@ -22,11 +22,7 @@ export class AddESQLPanelAction implements Action<EmbeddableApiContext> {
public grouping = [ADD_PANEL_VISUALIZATION_GROUP];
constructor(
protected readonly startDependencies: LensPluginStartDependencies,
protected readonly core: CoreStart,
protected readonly getEditorFrameService: () => Promise<EditorFrameService>
) {}
constructor(protected readonly core: CoreStart) {}
public getDisplayName(): string {
return i18n.translate('xpack.lens.app.createVisualizationLabel', {
@ -40,18 +36,22 @@ export class AddESQLPanelAction implements Action<EmbeddableApiContext> {
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
return apiIsPresentationContainer(embeddable) && isCreateActionCompatible(this.core);
return apiIsPresentationContainer(embeddable) && this.core.uiSettings.get(ENABLE_ESQL);
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
const editorFrameService = await this.getEditorFrameService();
executeCreateAction({
deps: this.startDependencies,
core: this.core,
api: embeddable,
editorFrameService,
public async execute({ embeddable: api }: EmbeddableApiContext) {
if (!apiIsPresentationContainer(api)) throw new IncompatibleActionError();
const embeddable = await api.addNewPanel<object, LensApi>({
panelType: 'lens',
serializedState: {
rawState: {
id: generateId(),
isNewPanel: true,
attributes: { references: [] },
},
},
});
// open the flyout if embeddable has been created successfully
embeddable?.onEdit?.();
}
}

View file

@ -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<string, Visualization<unknown, unknown, unknown>>
>('VisualizationMap', false);
export const [getDatasourceMap, setDatasourceMap] = createGetterSetter<
Record<string, Datasource<unknown, unknown>>
>('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<object, LensApi>({
panelType: 'lens',
initialState: {
attributes: attrs,
id: generateId(),
isNewPanel: true,
},
});
// open the flyout if embeddable has been created successfully
embeddable?.onEdit?.();
}

View file

@ -35,7 +35,11 @@ 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, async () => ({
...mockStartDependencies,
visualizationMap: {},
datasourceMap: {},
}));
const context = {
attributes,
lensEvent: {
@ -60,7 +64,11 @@ describe('inapp editing of Lens embeddable', () => {
},
},
} as CoreStart;
const inAppEditAction = new EditLensEmbeddableAction(mockStartDependencies, updatedCore);
const inAppEditAction = new EditLensEmbeddableAction(updatedCore, async () => ({
...mockStartDependencies,
visualizationMap: {},
datasourceMap: {},
}));
const context = {
attributes,
lensEvent: {
@ -76,7 +84,11 @@ describe('inapp editing of Lens embeddable', () => {
});
it('is compatible for dataview charts', async () => {
const inAppEditAction = new EditLensEmbeddableAction(mockStartDependencies, core);
const inAppEditAction = new EditLensEmbeddableAction(core, async () => ({
...mockStartDependencies,
visualizationMap: {},
datasourceMap: {},
}));
const newAttributes = {
...attributes,
state: {

View file

@ -8,6 +8,7 @@ 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 type { VisualizationMap, DatasourceMap } from '../../../types';
import type { InlineEditLensEmbeddableContext } from './types';
const ACTION_EDIT_LENS_EMBEDDABLE = 'ACTION_EDIT_LENS_EMBEDDABLE';
@ -18,8 +19,13 @@ export class EditLensEmbeddableAction implements Action<InlineEditLensEmbeddable
public order = 50;
constructor(
protected readonly startDependencies: LensPluginStartDependencies,
protected readonly core: CoreStart
protected readonly core: CoreStart,
protected readonly getServices: () => Promise<
LensPluginStartDependencies & {
visualizationMap: VisualizationMap;
datasourceMap: DatasourceMap;
}
>
) {}
public getDisplayName(): string {
@ -45,11 +51,14 @@ export class EditLensEmbeddableAction implements Action<InlineEditLensEmbeddable
onApply,
onCancel,
}: InlineEditLensEmbeddableContext) {
const { executeEditEmbeddableAction } = await import('../../../async_services');
const [{ executeEditEmbeddableAction }, services] = await Promise.all([
import('../../../async_services'),
this.getServices(),
]);
if (attributes) {
executeEditEmbeddableAction({
deps: this.startDependencies,
core: this.core,
deps: services,
attributes,
lensEvent,
container,

View file

@ -11,12 +11,13 @@ import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { BehaviorSubject } from 'rxjs';
import '../helpers.scss';
import { PublishingSubject } from '@kbn/presentation-publishing';
import { LensPluginStartDependencies } from '../../../plugin';
import { DatasourceMap, VisualizationMap } from '../../../types';
import { generateId } from '../../../id_generator';
import { setupPanelManagement } from '../../../react_embeddable/inline_editing/panel_management';
import { prepareInlineEditPanel } from '../../../react_embeddable/inline_editing/setup_inline_editing';
import { mountInlineEditPanel } from '../../../react_embeddable/inline_editing/mount';
import type { TypedLensByValueInput, LensRuntimeState } from '../../../react_embeddable/types';
import type { LensPluginStartDependencies } from '../../../plugin';
import type { LensChartLoadEvent } from './types';
const asyncNoop = async () => {};
@ -31,8 +32,8 @@ export function isEmbeddableEditActionCompatible(
}
export async function executeEditEmbeddableAction({
deps,
core,
deps,
attributes,
lensEvent,
container,
@ -40,8 +41,11 @@ export async function executeEditEmbeddableAction({
onApply,
onCancel,
}: {
deps: LensPluginStartDependencies;
core: CoreStart;
deps: LensPluginStartDependencies & {
visualizationMap: VisualizationMap;
datasourceMap: DatasourceMap;
};
attributes: TypedLensByValueInput['attributes'];
lensEvent: LensChartLoadEvent;
container?: HTMLElement | null;