[Lens] Move esql editor to layer_panel.tsx (#208354)

## Summary

moves esql editor to layer_panel.tsx as preparation to enable esql
editing on each layer.

how to test this:
- create esql visualization in discover and put it on a dashboard
- edit visualization on the dashboard (esql query etc)
- everything should work exactly as before

---------

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: dej611 <dej611@gmail.com>
Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Peter Pisljar 2025-03-11 15:50:52 +01:00 committed by GitHub
parent 73c8a5184f
commit 907abc687b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 479 additions and 232 deletions

View file

@ -92,6 +92,7 @@ describe('Lens inline editing helpers', () => {
createMockStartDependencies() as unknown as LensPluginStartDependencies;
const dataViews = dataViewPluginMocks.createStartContract();
dataViews.create.mockResolvedValue(mockDataViewWithTimefield);
mockStartDependencies.data.dataViews = dataViews;
const dataviewSpecArr = [
{
id: 'd2588ae7-9ea0-4439-9f5b-f808754a3b97',
@ -113,7 +114,7 @@ describe('Lens inline editing helpers', () => {
it('returns the suggestions attributes correctly', async () => {
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
startDependencies.data,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
@ -129,7 +130,7 @@ describe('Lens inline editing helpers', () => {
mockSuggestionApi.mockResolvedValueOnce([]);
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
startDependencies.data,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
@ -145,7 +146,7 @@ describe('Lens inline editing helpers', () => {
const setErrorsSpy = jest.fn();
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
startDependencies.data,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,

View file

@ -26,7 +26,6 @@ import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { getTime } from '@kbn/data-plugin/common';
import { type DataPublicPluginStart } from '@kbn/data-plugin/public';
import { TypedLensSerializedState } from '../../../react_embeddable/types';
import type { LensPluginStartDependencies } from '../../../plugin';
import type { DatasourceMap, VisualizationMap } from '../../../types';
import { suggestionsApi } from '../../../lens_suggestions_api';
@ -54,7 +53,7 @@ const getDSLFilter = (queryService: DataPublicPluginStart['query'], timeFieldNam
export const getGridAttrs = async (
query: AggregateQuery,
adHocDataViews: DataViewSpec[],
deps: LensPluginStartDependencies,
data: DataPublicPluginStart,
abortController?: AbortController,
esqlVariables: ESQLControlVariable[] = []
): Promise<ESQLDataGridAttrs> => {
@ -64,18 +63,18 @@ export const getGridAttrs = async (
});
const dataView = dataViewSpec
? await deps.dataViews.create(dataViewSpec)
: await getESQLAdHocDataview(query.esql, deps.dataViews);
? await data.dataViews.create(dataViewSpec)
: await getESQLAdHocDataview(query.esql, data.dataViews);
const filter = getDSLFilter(deps.data.query, dataView.timeFieldName);
const filter = getDSLFilter(data.query, dataView.timeFieldName);
const results = await getESQLResults({
esqlQuery: query.esql,
search: deps.data.search.search,
search: data.search.search,
signal: abortController?.signal,
filter,
dropNullColumns: true,
timeRange: deps.data.query.timefilter.timefilter.getAbsoluteTime(),
timeRange: data.query.timefilter.timefilter.getAbsoluteTime(),
variables: esqlVariables,
});
@ -90,7 +89,7 @@ export const getGridAttrs = async (
export const getSuggestions = async (
query: AggregateQuery,
deps: LensPluginStartDependencies,
data: DataPublicPluginStart,
datasourceMap: DatasourceMap,
visualizationMap: VisualizationMap,
adHocDataViews: DataViewSpec[],
@ -105,7 +104,7 @@ export const getSuggestions = async (
const { dataView, columns, rows } = await getGridAttrs(
query,
adHocDataViews,
deps,
data,
abortController,
esqlVariables
);

View file

@ -28,6 +28,15 @@ export function LayerConfiguration({
setIsInlineFlyoutVisible,
getUserMessages,
onlyAllowSwitchToSubtypes,
lensAdapters,
dataLoading$,
setCurrentAttributes,
updateSuggestion,
parentApi,
panelId,
closeFlyout,
canEditTextBasedQuery,
editorContainer,
}: LayerConfigurationProps) {
const dispatch = useLensDispatch();
const { euiTheme } = useEuiTheme();
@ -51,6 +60,9 @@ export function LayerConfiguration({
);
const layerPanelsProps = {
attributes,
lensAdapters,
dataLoading$,
framePublicAPI,
datasourceMap,
visualizationMap,
@ -63,6 +75,14 @@ export function LayerConfiguration({
indexPatternService,
setIsInlineFlyoutVisible,
getUserMessages,
data: startDependencies.data,
setCurrentAttributes,
updateSuggestion,
parentApi,
panelId,
closeFlyout,
canEditTextBasedQuery,
editorContainer,
};
return (
<div

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useMemo, useCallback, useRef, useEffect, useState } from 'react';
import React, { useMemo, useCallback, useRef, useState } from 'react';
import { isEqual } from 'lodash';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
@ -21,36 +21,17 @@ import {
keys,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import {
getAggregateQueryMode,
isOfAggregateQueryType,
getLanguageDisplayName,
} from '@kbn/es-query';
import type { AggregateQuery, Query } from '@kbn/es-query';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { ESQLLangEditor } from '@kbn/esql/public';
import { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
import { getLanguageDisplayName, isOfAggregateQueryType } from '@kbn/es-query';
import type { TypedLensSerializedState } from '../../../react_embeddable/types';
import { buildExpression } from '../../../editor_frame_service/editor_frame/expression_helpers';
import { MAX_NUM_OF_COLUMNS } from '../../../datasources/text_based/utils';
import {
useLensSelector,
selectFramePublicAPI,
onActiveDataChange,
useLensDispatch,
} from '../../../state_management';
import { useLensSelector, selectFramePublicAPI, useLensDispatch } from '../../../state_management';
import { EXPRESSION_BUILD_ERROR_ID, getAbsoluteDateRange } from '../../../utils';
import { LayerConfiguration } from './layer_configuration_section';
import type { EditConfigPanelProps } from './types';
import { FlyoutWrapper } from './flyout_wrapper';
import { getSuggestions, type ESQLDataGridAttrs } from './helpers';
import { SuggestionPanel } from '../../../editor_frame_service/editor_frame/suggestion_panel';
import { useApplicationUserMessages } from '../../get_application_user_messages';
import { trackSaveUiCounterEvents } from '../../../lens_ui_telemetry';
import { ESQLDataGridAccordion } from './esql_data_grid_accordion';
import { isApiESQLVariablesCompatible } from '../../../react_embeddable/types';
import { useESQLVariables } from './use_esql_variables';
import { getActiveDataFromDatatable } from '../../../state_management/shared_logic';
import { useCurrentAttributes } from './use_current_attributes';
export function LensEditConfigurationFlyout({
@ -82,31 +63,10 @@ export function LensEditConfigurationFlyout({
}: EditConfigPanelProps) {
const euiTheme = useEuiTheme();
const previousAttributes = useRef<TypedLensSerializedState['attributes']>(attributes);
const previousAdapters = useRef<Partial<DefaultInspectorAdapters> | undefined>(lensAdapters);
const prevQuery = useRef<AggregateQuery | Query>(attributes.state.query);
const [query, setQuery] = useState<AggregateQuery | Query>(attributes.state.query);
const [errors, setErrors] = useState<Error[] | undefined>();
const [isInlineFlyoutVisible, setIsInlineFlyoutVisible] = useState(true);
const [isLayerAccordionOpen, setIsLayerAccordionOpen] = useState(true);
const [suggestsLimitedColumns, setSuggestsLimitedColumns] = useState(false);
const [isSuggestionsAccordionOpen, setIsSuggestionsAccordionOpen] = useState(false);
const [isESQLResultsAccordionOpen, setIsESQLResultsAccordionOpen] = useState(false);
const [isVisualizationLoading, setIsVisualizationLoading] = useState(false);
const [dataGridAttrs, setDataGridAttrs] = useState<ESQLDataGridAttrs | undefined>(undefined);
const datasourceState = attributes.state.datasourceStates[datasourceId];
const activeDatasource = datasourceMap[datasourceId];
const esqlVariables = useStateFromPublishingSubject(
isApiESQLVariablesCompatible(parentApi) ? parentApi?.esqlVariables$ : undefined
);
const { onSaveControl, onCancelControl } = useESQLVariables({
parentApi,
panelId,
attributes,
closeFlyout,
});
const { datasourceStates, visualization, isLoading, annotationGroups, searchSessionId } =
useLensSelector((state) => state.lens);
@ -120,44 +80,7 @@ export function LensEditConfigurationFlyout({
startDependencies.data.query.timefilter.timefilter
);
const layers = useMemo(
() => activeDatasource.getLayers(datasourceState),
[activeDatasource, datasourceState]
);
// needed for text based languages mode which works ONLY with adHoc dataviews
const adHocDataViews = Object.values(attributes.state.adHocDataViews ?? {});
const dispatch = useLensDispatch();
useEffect(() => {
const s = dataLoading$?.subscribe((isDataLoading) => {
// go thru only when the loading is complete
if (isDataLoading) {
return;
}
const [defaultLayerId] = Object.keys(framePublicAPI.datasourceLayers);
const activeData = getActiveDataFromDatatable(
defaultLayerId,
previousAdapters.current?.tables?.tables
);
layers.forEach((layer) => {
const table = activeData[layer];
if (table) {
// there are cases where a query can return a big amount of columns
// at this case we don't suggest all columns in a table but the first `MAX_NUM_OF_COLUMNS`
setSuggestsLimitedColumns(table.columns.length >= MAX_NUM_OF_COLUMNS);
}
});
if (Object.keys(activeData).length > 0) {
dispatch(onActiveDataChange({ activeData }));
}
});
return () => s?.unsubscribe();
}, [dispatch, dataLoading$, layers, framePublicAPI.datasourceLayers]);
const attributesChanged: boolean = useMemo(() => {
const previousAttrs = previousAttributes.current;
@ -234,10 +157,7 @@ export function LensEditConfigurationFlyout({
onCancelCallback,
]);
const textBasedMode = useMemo(
() => (isOfAggregateQueryType(query) ? getAggregateQueryMode(query) : undefined),
[query]
);
const textBasedMode = isOfAggregateQueryType(attributes.state.query);
const currentAttributes = useCurrentAttributes({
textBasedMode,
@ -247,7 +167,7 @@ export function LensEditConfigurationFlyout({
});
const onApply = useCallback(() => {
if (visualization.activeId == null) {
if (visualization.activeId == null || !currentAttributes) {
return;
}
if (savedObjectId) {
@ -272,12 +192,12 @@ export function LensEditConfigurationFlyout({
closeFlyout?.();
}, [
visualization.activeId,
visualization.state,
savedObjectId,
activeVisualization,
onApplyCallback,
currentAttributes,
closeFlyout,
onApplyCallback,
visualization.state,
activeVisualization,
currentAttributes,
saveByRef,
updateByRefInput,
]);
@ -294,63 +214,7 @@ export function LensEditConfigurationFlyout({
visualizationState: visualization,
});
const runQuery = useCallback(
async (q: AggregateQuery, abortController?: AbortController, shouldUpdateAttrs?: boolean) => {
const attrs = await getSuggestions(
q,
startDependencies,
datasourceMap,
visualizationMap,
adHocDataViews,
setErrors,
abortController,
setDataGridAttrs,
esqlVariables,
shouldUpdateAttrs,
currentAttributes
);
if (attrs) {
setCurrentAttributes?.(attrs);
setErrors([]);
updateSuggestion?.(attrs);
}
prevQuery.current = q;
setIsVisualizationLoading(false);
},
[
startDependencies,
datasourceMap,
visualizationMap,
adHocDataViews,
esqlVariables,
currentAttributes,
setCurrentAttributes,
updateSuggestion,
]
);
useEffect(() => {
const abortController = new AbortController();
const initializeChart = async () => {
if (isOfAggregateQueryType(query) && !dataGridAttrs) {
try {
await runQuery(query, abortController, Boolean(attributes.state.needsRefresh));
} catch (e) {
setErrors([e]);
prevQuery.current = query;
}
}
};
initializeChart();
}, [
adHocDataViews,
runQuery,
esqlVariables,
query,
startDependencies,
dataGridAttrs,
attributes.state.needsRefresh,
]);
const editorContainer = useRef(null);
const isSaveable = useMemo(() => {
if (!attributesChanged) {
@ -430,6 +294,12 @@ export function LensEditConfigurationFlyout({
hasPadding
framePublicAPI={framePublicAPI}
setIsInlineFlyoutVisible={setIsInlineFlyoutVisible}
updateSuggestion={updateSuggestion}
setCurrentAttributes={setCurrentAttributes}
closeFlyout={closeFlyout}
parentApi={parentApi}
panelId={panelId}
canEditTextBasedQuery={canEditTextBasedQuery}
/>
</FlyoutWrapper>
</>
@ -445,9 +315,9 @@ export function LensEditConfigurationFlyout({
onCancel={onCancel}
navigateToLensEditor={navigateToLensEditor}
onApply={onApply}
language={textBasedMode ? getLanguageDisplayName(textBasedMode) : ''}
isSaveable={isSaveable}
isScrollable={false}
language={textBasedMode ? getLanguageDisplayName('esql') : ''}
isNewPanel={isNewPanel}
>
<EuiFlexGroup
@ -489,60 +359,7 @@ export function LensEditConfigurationFlyout({
direction="column"
gutterSize="none"
>
{isOfAggregateQueryType(query) && canEditTextBasedQuery && (
<EuiFlexItem grow={false} data-test-subj="InlineEditingESQLEditor">
<ESQLLangEditor
query={query}
onTextLangQueryChange={(q) => {
setQuery(q);
}}
detectedTimestamp={adHocDataViews?.[0]?.timeFieldName}
hideTimeFilterInfo={hideTimeFilterInfo}
errors={errors}
warning={
suggestsLimitedColumns
? i18n.translate('xpack.lens.config.configFlyoutCallout', {
defaultMessage:
'Displaying a limited portion of the available fields. Add more from the configuration panel.',
})
: undefined
}
editorIsInline
supportsControls
hideRunQueryText
onTextLangQuerySubmit={async (q, a) => {
// do not run the suggestions if the query is the same as the previous one
if (q && !isEqual(q, prevQuery.current)) {
setIsVisualizationLoading(true);
await runQuery(q, a);
}
}}
isDisabled={false}
allowQueryCancellation
isLoading={isVisualizationLoading}
onSaveControl={onSaveControl}
onCancelControl={onCancelControl}
esqlVariables={esqlVariables}
/>
</EuiFlexItem>
)}
{isOfAggregateQueryType(query) && canEditTextBasedQuery && dataGridAttrs && (
<ESQLDataGridAccordion
dataGridAttrs={dataGridAttrs}
isAccordionOpen={isESQLResultsAccordionOpen}
setIsAccordionOpen={setIsESQLResultsAccordionOpen}
query={query}
isTableView={attributes.visualizationType !== 'lnsDatatable'}
onAccordionToggleCb={(status) => {
if (status && isSuggestionsAccordionOpen) {
setIsSuggestionsAccordionOpen(!status);
}
if (status && isLayerAccordionOpen) {
setIsLayerAccordionOpen(!status);
}
}}
/>
)}
<div ref={editorContainer} />
<EuiFlexItem
grow={isLayerAccordionOpen ? 1 : false}
css={css`
@ -558,8 +375,9 @@ export function LensEditConfigurationFlyout({
<EuiTitle
size="xxs"
css={css`
padding: 2px;
`}
padding: 2px;
}
`}
>
<h5>
{i18n.translate('xpack.lens.config.visualizationConfigurationLabel', {
@ -586,6 +404,8 @@ export function LensEditConfigurationFlyout({
<>
<LayerConfiguration
attributes={attributes}
dataLoading$={dataLoading$}
lensAdapters={lensAdapters}
getUserMessages={getUserMessages}
coreStart={coreStart}
startDependencies={startDependencies}
@ -594,6 +414,13 @@ export function LensEditConfigurationFlyout({
datasourceId={datasourceId}
framePublicAPI={framePublicAPI}
setIsInlineFlyoutVisible={setIsInlineFlyoutVisible}
updateSuggestion={updateSuggestion}
setCurrentAttributes={setCurrentAttributes}
closeFlyout={closeFlyout}
parentApi={parentApi}
panelId={panelId}
canEditTextBasedQuery={canEditTextBasedQuery}
editorContainer={editorContainer.current || undefined}
/>
<EuiSpacer />
</>

View file

@ -92,6 +92,10 @@ export interface EditConfigPanelProps {
export interface LayerConfigurationProps {
attributes: TypedLensSerializedState['attributes'];
/** Embeddable output observable, useful for dashboard flyout */
dataLoading$?: PublishingSubject<boolean | undefined>;
/** Contains the active data, necessary for some panel configuration such as coloring */
lensAdapters?: ReturnType<LensInspector['getInspectorAdapters']>;
coreStart: CoreStart;
startDependencies: LensPluginStartDependencies;
visualizationMap: VisualizationMap;
@ -102,4 +106,12 @@ export interface LayerConfigurationProps {
setIsInlineFlyoutVisible: (flag: boolean) => void;
getUserMessages: UserMessagesGetter;
onlyAllowSwitchToSubtypes?: boolean;
updateSuggestion?: (attrs: TypedLensSerializedState['attributes']) => void;
/** Set the attributes state */
setCurrentAttributes?: (attrs: TypedLensSerializedState['attributes']) => void;
parentApi?: unknown;
panelId?: string;
closeFlyout?: () => void;
canEditTextBasedQuery?: boolean;
editorContainer?: HTMLElement;
}

View file

@ -6,6 +6,7 @@
*/
import { useEffect, useState } from 'react';
import { isEqual } from 'lodash';
import { createEmptyLensState } from '../../../react_embeddable/helper';
import type { TypedLensSerializedState } from '../../../react_embeddable/types';
import { useLensSelector } from '../../../state_management';
import { extractReferencesFromState } from '../../../utils';
@ -17,20 +18,25 @@ export const useCurrentAttributes = ({
datasourceMap,
visualizationMap,
}: {
initialAttributes: TypedLensSerializedState['attributes'];
initialAttributes?: TypedLensSerializedState['attributes'];
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
textBasedMode?: string;
textBasedMode?: boolean;
}) => {
const { datasourceStates, visualization } = useLensSelector((state) => state.lens);
// use the latest activeId, but fallback to attributes
const activeVisualization =
visualizationMap[visualization.activeId ?? initialAttributes.visualizationType];
const [currentAttributes, setCurrentAttributes] =
useState<TypedLensSerializedState['attributes']>(initialAttributes);
const [currentAttributes, setCurrentAttributes] = useState<
TypedLensSerializedState['attributes'] | undefined
>(initialAttributes);
// use the latest activeId, but fallback to attributes
const visualizationType = visualization.activeId ?? initialAttributes?.visualizationType;
const activeVisualization = visualizationType ? visualizationMap[visualizationType] : undefined;
useEffect(() => {
if (!activeVisualization) {
return;
}
const dsStates = Object.fromEntries(
Object.entries(datasourceStates).map(([id, ds]) => {
const dsState = ds.state;
@ -53,15 +59,16 @@ export const useCurrentAttributes = ({
activeVisualization,
})
: [];
const attributes = initialAttributes ?? createEmptyLensState().attributes;
const attrs: TypedLensSerializedState['attributes'] = {
...initialAttributes,
...attributes,
state: {
...initialAttributes.state,
...attributes.state,
visualization: visualization.state,
datasourceStates: dsStates,
},
references,
visualizationType: visualization.activeId ?? initialAttributes.visualizationType,
visualizationType: activeVisualization.id,
};
if (!isEqual(attrs, currentAttributes)) {
setCurrentAttributes(attrs);

View file

@ -20,7 +20,7 @@ export const useESQLVariables = ({
closeFlyout,
}: {
parentApi: unknown;
attributes: TypedLensSerializedState['attributes'];
attributes?: TypedLensSerializedState['attributes'];
panelId?: string;
closeFlyout?: () => void;
}) => {
@ -55,7 +55,7 @@ export const useESQLVariables = ({
id: uuidv4(),
},
});
if (panel && updatedQuery) {
if (panel && updatedQuery && attributes) {
panel.updateAttributes({
...attributes,
state: {

View file

@ -30,6 +30,7 @@ import { ReactWrapper } from 'enzyme';
import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock';
import { AddLayerButton } from '../../../visualizations/xy/add_layer';
import { LayerType } from '@kbn/visualizations-plugin/common';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
jest.mock('../../../id_generator');
@ -155,6 +156,7 @@ describe('ConfigPanel', () => {
toggleFullscreen: jest.fn(),
uiActions,
dataViews: {} as DataViewsPublicPluginStart,
data: dataPluginMock.createStartContract(),
getUserMessages: () => [],
};
}

View file

@ -264,6 +264,11 @@ export function LayerPanels(
!hidden && (
<LayerPanel
{...props}
attributes={props.attributes}
data={props.data}
setCurrentAttributes={props.setCurrentAttributes}
updateSuggestion={props.updateSuggestion}
dataLoading$={props.dataLoading$}
onDropToDimension={handleDimensionDrop}
registerLibraryAnnotationGroup={registerLibraryAnnotationGroupFunction}
dimensionGroups={groups}
@ -327,6 +332,9 @@ export function LayerPanels(
}}
toggleFullscreen={toggleFullscreen}
indexPatternService={indexPatternService}
panelId={props.panelId}
parentApi={props.parentApi}
closeFlyout={props.closeFlyout}
/>
)
);

View file

@ -0,0 +1,322 @@
/*
* 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 { createPortal } from 'react-dom';
import { EuiFlexItem } from '@elastic/eui';
import { AggregateQuery, Query, isOfAggregateQueryType } from '@kbn/es-query';
import { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { isEqual } from 'lodash';
import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
import { ESQLLangEditor } from '@kbn/esql/public';
import type { ESQLControlVariable } from '@kbn/esql-types';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { DataViewSpec } from '@kbn/data-views-plugin/common';
import { useCurrentAttributes } from '../../../app_plugin/shared/edit_on_the_fly/use_current_attributes';
import { getActiveDataFromDatatable } from '../../../state_management/shared_logic';
import type { Simplify } from '../../../types';
import { onActiveDataChange, useLensDispatch } from '../../../state_management';
import {
ESQLDataGridAttrs,
getSuggestions,
} from '../../../app_plugin/shared/edit_on_the_fly/helpers';
import { useESQLVariables } from '../../../app_plugin/shared/edit_on_the_fly/use_esql_variables';
import { MAX_NUM_OF_COLUMNS } from '../../../datasources/text_based/utils';
import { isApiESQLVariablesCompatible } from '../../../react_embeddable/types';
import type { LayerPanelProps } from './types';
import { ESQLDataGridAccordion } from '../../../app_plugin/shared/edit_on_the_fly/esql_data_grid_accordion';
export type ESQLEditorProps = Simplify<
{
isTextBasedLanguage: boolean;
} & Pick<
LayerPanelProps,
| 'attributes'
| 'framePublicAPI'
| 'datasourceMap'
| 'lensAdapters'
| 'parentApi'
| 'layerId'
| 'panelId'
| 'closeFlyout'
| 'data'
| 'canEditTextBasedQuery'
| 'editorContainer'
| 'visualizationMap'
| 'setCurrentAttributes'
| 'updateSuggestion'
| 'dataLoading$'
| 'parentApi'
>
>;
/**
* This is a wrapper around the Monaco ESQL editor for Lens
* It handles its internal state and update both attributes & activeData on changes
* in the Redux store.
* Mind that this component will render either inline (classic React)
* or in a portal if the editorContainer props is provided
*/
export function ESQLEditor({
data,
attributes,
framePublicAPI,
isTextBasedLanguage,
datasourceMap,
visualizationMap,
lensAdapters,
parentApi,
panelId,
layerId,
closeFlyout,
editorContainer,
canEditTextBasedQuery,
dataLoading$,
setCurrentAttributes,
updateSuggestion,
}: ESQLEditorProps) {
const prevQuery = useRef<AggregateQuery | Query>(attributes?.state.query || { esql: '' });
const [query, setQuery] = useState<AggregateQuery | Query>(
attributes?.state.query || { esql: '' }
);
const [errors, setErrors] = useState<Error[]>([]);
const [isLayerAccordionOpen, setIsLayerAccordionOpen] = useState(true);
const [suggestsLimitedColumns, setSuggestsLimitedColumns] = useState(false);
const [isVisualizationLoading, setIsVisualizationLoading] = useState(false);
const [dataGridAttrs, setDataGridAttrs] = useState<ESQLDataGridAttrs | undefined>(undefined);
const [isSuggestionsAccordionOpen, setIsSuggestionsAccordionOpen] = useState(false);
const [isESQLResultsAccordionOpen, setIsESQLResultsAccordionOpen] = useState(false);
const currentAttributes = useCurrentAttributes({
textBasedMode: isTextBasedLanguage,
initialAttributes: attributes,
datasourceMap,
visualizationMap,
});
const adHocDataViews =
attributes && attributes.state.adHocDataViews
? Object.values(attributes.state.adHocDataViews)
: Object.values(framePublicAPI.dataViews.indexPatterns).map((index) => index.spec);
const previousAdapters = useRef<Partial<DefaultInspectorAdapters> | undefined>(lensAdapters);
const esqlVariables = useStateFromPublishingSubject(
isApiESQLVariablesCompatible(parentApi) ? parentApi?.esqlVariables$ : undefined
);
const dispatch = useLensDispatch();
useEffect(() => {
const s = dataLoading$?.subscribe((isDataLoading) => {
// go thru only when the loading is complete
if (isDataLoading) {
return;
}
const activeData = getActiveDataFromDatatable(
layerId,
previousAdapters.current?.tables?.tables
);
const table = activeData?.[layerId];
if (table) {
// there are cases where a query can return a big amount of columns
// at this case we don't suggest all columns in a table but the first `MAX_NUM_OF_COLUMNS`
setSuggestsLimitedColumns(table.columns.length >= MAX_NUM_OF_COLUMNS);
}
if (Object.keys(activeData).length > 0) {
dispatch(onActiveDataChange({ activeData }));
}
});
return () => s?.unsubscribe();
}, [dataLoading$, dispatch, layerId]);
const runQuery = useCallback(
async (q: AggregateQuery, abortController?: AbortController, shouldUpdateAttrs?: boolean) => {
const attrs = await getSuggestions(
q,
data,
datasourceMap,
visualizationMap,
adHocDataViews,
setErrors,
abortController,
setDataGridAttrs,
esqlVariables,
shouldUpdateAttrs,
currentAttributes
);
if (attrs) {
setCurrentAttributes?.(attrs);
setErrors([]);
updateSuggestion?.(attrs);
}
prevQuery.current = q;
setIsVisualizationLoading(false);
},
[
data,
datasourceMap,
visualizationMap,
adHocDataViews,
esqlVariables,
setCurrentAttributes,
updateSuggestion,
currentAttributes,
]
);
useEffect(() => {
const abortController = new AbortController();
const initializeChart = async () => {
if (isTextBasedLanguage && isOfAggregateQueryType(query) && !dataGridAttrs) {
try {
await runQuery(query, abortController, Boolean(attributes?.state.needsRefresh));
} catch (e) {
setErrors([e]);
prevQuery.current = query;
}
}
};
initializeChart();
}, [
adHocDataViews,
runQuery,
esqlVariables,
query,
data,
dataGridAttrs,
attributes?.state.needsRefresh,
isTextBasedLanguage,
]);
// Early exit if it's not in TextBased mode
if (!isTextBasedLanguage || !canEditTextBasedQuery || !isOfAggregateQueryType(query)) {
return null;
}
const EditorComponent = (
<>
<InnerESQLEditor
query={query}
prevQuery={prevQuery}
setQuery={setQuery}
runQuery={runQuery}
adHocDataViews={adHocDataViews}
errors={errors}
suggestsLimitedColumns={suggestsLimitedColumns}
isVisualizationLoading={isVisualizationLoading}
esqlVariables={esqlVariables}
closeFlyout={closeFlyout}
panelId={panelId}
attributes={attributes}
parentApi={parentApi}
/>
{dataGridAttrs ? (
<ESQLDataGridAccordion
dataGridAttrs={dataGridAttrs}
isAccordionOpen={isESQLResultsAccordionOpen}
isTableView={attributes?.visualizationType !== 'lnsDatatable'}
setIsAccordionOpen={setIsESQLResultsAccordionOpen}
query={query}
onAccordionToggleCb={(status) => {
if (status && isSuggestionsAccordionOpen) {
setIsSuggestionsAccordionOpen(!status);
}
if (status && isLayerAccordionOpen) {
setIsLayerAccordionOpen(!status);
}
}}
/>
) : null}
</>
);
if (editorContainer) {
return <>{createPortal(EditorComponent, editorContainer)}</>;
}
return EditorComponent;
}
type InnerEditorProps = Simplify<
{
query: AggregateQuery;
prevQuery: MutableRefObject<AggregateQuery | Query>;
setQuery: (query: AggregateQuery | Query) => void;
runQuery: (
q: AggregateQuery,
abortController?: AbortController,
shouldUpdateAttrs?: boolean
) => Promise<void>;
errors: Error[];
isVisualizationLoading: boolean | undefined;
suggestsLimitedColumns: boolean;
adHocDataViews: DataViewSpec[];
esqlVariables: ESQLControlVariable[] | undefined;
} & Pick<LayerPanelProps, 'attributes' | 'parentApi' | 'panelId' | 'closeFlyout'>
>;
function InnerESQLEditor({
query,
adHocDataViews,
errors,
suggestsLimitedColumns,
attributes,
parentApi,
panelId,
closeFlyout,
setQuery,
isVisualizationLoading,
prevQuery,
runQuery,
esqlVariables,
}: InnerEditorProps) {
const { onSaveControl, onCancelControl } = useESQLVariables({
parentApi,
panelId,
attributes,
closeFlyout,
});
const hideTimeFilterInfo = false;
return (
<EuiFlexItem grow={false} data-test-subj="InlineEditingESQLEditor">
<ESQLLangEditor
query={query}
onTextLangQueryChange={setQuery}
detectedTimestamp={adHocDataViews?.[0]?.timeFieldName}
hideTimeFilterInfo={hideTimeFilterInfo}
errors={errors}
warning={
suggestsLimitedColumns
? i18n.translate('xpack.lens.config.configFlyoutCallout', {
defaultMessage:
'Displaying a limited portion of the available fields. Add more from the configuration panel.',
})
: undefined
}
editorIsInline
hideRunQueryText
onTextLangQuerySubmit={async (q, a) => {
// do not run the suggestions if the query is the same as the previous one
if (q && !isEqual(q, prevQuery.current)) {
// setIsVisualizationLoading(true);
await runQuery(q, a);
}
}}
isDisabled={false}
allowQueryCancellation
isLoading={isVisualizationLoading}
supportsControls={parentApi !== undefined}
esqlVariables={esqlVariables}
onCancelControl={onCancelControl}
onSaveControl={onSaveControl}
/>
</EuiFlexItem>
);
}

View file

@ -27,6 +27,7 @@ import { DimensionButton } from '@kbn/visualization-ui-components';
import { LensAppState } from '../../../state_management';
import type { ProviderProps } from '@kbn/dom-drag-drop/src';
import { LayerPanelProps } from './types';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
jest.mock('../../../id_generator');
@ -117,6 +118,7 @@ describe('LayerPanel', () => {
getUserMessages: () => [],
displayLayerSettings: true,
onDropToDimension,
data: dataPluginMock.createStartContract(),
};
}
let props: LayerPanelProps;

View file

@ -22,6 +22,7 @@ import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { DragDropIdentifier, ReorderProvider, DropType } from '@kbn/dom-drag-drop';
import { DimensionButton } from '@kbn/visualization-ui-components';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { LayerActions } from './layer_actions';
import { isOperation, LayerAction, VisualizationDimensionGroupConfig } from '../../../types';
import { LayerHeader } from './layer_header';
@ -40,6 +41,7 @@ import { getSharedActions } from './layer_actions/layer_actions';
import { FlyoutContainer } from '../../../shared_components/flyout_container';
import { FakeDimensionButton } from './buttons/fake_dimension_button';
import { getLongMessage } from '../../../user_messages_utils';
import { ESQLEditor } from './esql_editor';
export function LayerPanel(props: LayerPanelProps) {
const [openDimension, setOpenDimension] = useState<{
@ -73,6 +75,7 @@ export function LayerPanel(props: LayerPanelProps) {
onDropToDimension,
setIsInlineFlyoutVisible,
onlyAllowSwitchToSubtypes,
...editorProps
} = props;
const isInlineEditing = Boolean(props?.setIsInlineFlyoutVisible);
@ -125,8 +128,8 @@ export function LayerPanel(props: LayerPanelProps) {
};
const datasourcePublicAPI = framePublicAPI.datasourceLayers?.[layerId];
const datasourceId = datasourcePublicAPI?.datasourceId;
let layerDatasourceState = datasourceId ? datasourceStates?.[datasourceId]?.state : undefined;
const datasourceId = datasourcePublicAPI?.datasourceId! as 'formBased' | 'textBased';
let layerDatasourceState = datasourceStates?.[datasourceId]?.state;
// try again with aliases
if (!layerDatasourceState && datasourcePublicAPI?.datasourceAliasIds && datasourceStates) {
const aliasId = datasourcePublicAPI.datasourceAliasIds.find(
@ -284,7 +287,10 @@ export function LayerPanel(props: LayerPanelProps) {
const { dataViews } = props.framePublicAPI;
const [datasource] = Object.values(framePublicAPI.datasourceLayers);
const isTextBasedLanguage = Boolean(datasource?.isTextBasedLanguage());
const isTextBasedLanguage =
datasource?.isTextBasedLanguage() ||
isOfAggregateQueryType(editorProps.attributes?.state.query) ||
false;
const visualizationLayerSettings = useMemo(
() =>
@ -400,7 +406,7 @@ export function LayerPanel(props: LayerPanelProps) {
(layerDatasource || activeVisualization.LayerPanelComponent) && (
<EuiSpacer size="s" />
)}
{layerDatasource && props.indexPatternService && (
{layerDatasource && props.indexPatternService && !isTextBasedLanguage && (
<layerDatasource.LayerPanelComponent
{...{
layerId,
@ -412,6 +418,14 @@ export function LayerPanel(props: LayerPanelProps) {
}}
/>
)}
<ESQLEditor
isTextBasedLanguage={isTextBasedLanguage}
framePublicAPI={framePublicAPI}
datasourceMap={datasourceMap}
layerId={layerId}
visualizationMap={visualizationMap}
{...editorProps}
/>
{activeVisualization.LayerPanelComponent && (
<activeVisualization.LayerPanelComponent
{...{

View file

@ -8,6 +8,10 @@
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { DragDropIdentifier, DropType } from '@kbn/dom-drag-drop';
import { PublishingSubject } from '@kbn/presentation-publishing';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { LensInspector } from '../../../lens_inspector_service';
import type { TypedLensSerializedState } from '../../../react_embeddable/types';
import type { IndexPatternServiceAPI } from '../../../data_views_service/service';
import {
@ -30,15 +34,38 @@ export interface ConfigPanelWrapperProps {
visualizationMap: VisualizationMap;
core: DatasourceDimensionEditorProps['core'];
dataViews: DataViewsPublicPluginStart;
data: DataPublicPluginStart;
indexPatternService?: IndexPatternServiceAPI;
uiActions: UiActionsStart;
getUserMessages?: UserMessagesGetter;
hideLayerHeader?: boolean;
setIsInlineFlyoutVisible?: (status: boolean) => void;
onlyAllowSwitchToSubtypes?: boolean;
attributes?: TypedLensSerializedState['attributes'];
/** Embeddable output observable, useful for dashboard flyout */
dataLoading$?: PublishingSubject<boolean | undefined>;
/** Contains the active data, necessary for some panel configuration such as coloring */
lensAdapters?: ReturnType<LensInspector['getInspectorAdapters']>;
updateSuggestion?: (attrs: TypedLensSerializedState['attributes']) => void;
/** Set the attributes state */
setCurrentAttributes?: (attrs: TypedLensSerializedState['attributes']) => void;
parentApi?: unknown;
panelId?: string;
closeFlyout?: () => void;
canEditTextBasedQuery?: boolean;
editorContainer?: HTMLElement;
}
export interface LayerPanelProps {
attributes?: TypedLensSerializedState['attributes'];
/** Embeddable output observable, useful for dashboard flyout */
dataLoading$?: PublishingSubject<boolean | undefined>;
/** Contains the active data, necessary for some panel configuration such as coloring */
lensAdapters?: ReturnType<LensInspector['getInspectorAdapters']>;
data: DataPublicPluginStart;
updateSuggestion?: (attrs: TypedLensSerializedState['attributes']) => void;
/** Set the attributes state */
setCurrentAttributes?: (attrs: TypedLensSerializedState['attributes']) => void;
visualizationState: unknown;
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
@ -85,6 +112,11 @@ export interface LayerPanelProps {
displayLayerSettings: boolean;
setIsInlineFlyoutVisible?: (status: boolean) => void;
onlyAllowSwitchToSubtypes?: boolean;
panelId?: string;
parentApi?: unknown;
closeFlyout?: () => void;
canEditTextBasedQuery?: boolean;
editorContainer?: HTMLElement;
}
export interface LayerDatasourceDropProps {

View file

@ -158,6 +158,7 @@ export function EditorFrame(props: EditorFrameProps) {
framePublicAPI={framePublicAPI}
uiActions={props.plugins.uiActions}
dataViews={props.plugins.dataViews}
data={props.plugins.data}
indexPatternService={props.indexPatternService}
getUserMessages={props.getUserMessages}
/>

View file

@ -55,7 +55,7 @@ export function createMockSetupDependencies() {
export function createMockStartDependencies() {
return {
data: dataPluginMock.createSetupContract(),
data: dataPluginMock.createStartContract(),
embeddable: embeddablePluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),