mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:  On Fast 4g is still fast  On Slow 4g is acceptable  Even on 3G connection the feedback is much better now  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:
parent
e2730f70db
commit
1e92ae8afb
17 changed files with 301 additions and 232 deletions
|
@ -93,7 +93,7 @@ pageLoadAssetSize:
|
|||
kibanaReact: 74422
|
||||
kibanaUsageCollection: 16463
|
||||
kibanaUtils: 79713
|
||||
lens: 57135
|
||||
lens: 76079
|
||||
licenseManagement: 41817
|
||||
licensing: 29004
|
||||
links: 8000
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -38,6 +38,7 @@ function createMockTimefilter() {
|
|||
getRefreshInterval: () => {},
|
||||
getRefreshIntervalDefaults: () => {},
|
||||
getAutoRefreshFetch$: () => new Observable(),
|
||||
getAbsoluteTime: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?.();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?.();
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue