[ES|QL] Improves the authoring of the query and charts (#186875)

## Summary

It makes the authoring of the ES|QL query easier by:

- changing the flyout to be resizable
- displaying a tab with the results of the query in the ES|QL datagrid
component.

<img width="1255" alt="image"
src="https://github.com/user-attachments/assets/526509fa-7313-4560-9186-61181b5c575b">


### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2024-07-30 10:53:01 +02:00 committed by GitHub
parent 696190db60
commit 1bf4fcd6d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 486 additions and 46 deletions

View file

@ -8,7 +8,7 @@
/* eslint-disable max-classes-per-file */
import { EuiFlyout } from '@elastic/eui';
import { EuiFlyout, EuiFlyoutResizable } from '@elastic/eui';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Subject } from 'rxjs';
@ -102,11 +102,26 @@ export class FlyoutService {
}
};
const getWrapper = (children: JSX.Element) => {
return options?.isResizable ? (
<EuiFlyoutResizable
{...options}
onClose={onCloseFlyout}
ref={React.createRef()}
maxWidth={Number(options?.maxWidth)}
>
{children}
</EuiFlyoutResizable>
) : (
<EuiFlyout {...options} onClose={onCloseFlyout}>
{children}
</EuiFlyout>
);
};
render(
<KibanaRenderContextProvider analytics={analytics} i18n={i18n} theme={theme}>
<EuiFlyout {...options} onClose={onCloseFlyout}>
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
</EuiFlyout>
{getWrapper(<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />)}
</KibanaRenderContextProvider>,
this.targetDomElement
);

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { EuiFlyoutProps } from '@elastic/eui';
import type { EuiFlyoutProps, EuiFlyoutResizableProps } from '@elastic/eui';
import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser';
/**
@ -28,10 +28,11 @@ export interface OverlayFlyoutStart {
/**
* @public
*/
export type OverlayFlyoutOpenOptions = Omit<EuiFlyoutProps, 'onClose'> & {
export type OverlayFlyoutOpenOptions = Omit<EuiFlyoutProps | EuiFlyoutResizableProps, 'onClose'> & {
/**
* EuiFlyout onClose handler.
* If provided the consumer is responsible for calling flyout.close() to close the flyout;
*/
onClose?: (flyout: OverlayRef) => void;
isResizable?: boolean;
};

View file

@ -20,6 +20,7 @@ export {
getESQLQueryColumns,
getESQLQueryColumnsRaw,
getESQLResults,
formatESQLColumns,
getTimeFieldFromESQLQuery,
getStartEndParams,
hasStartEndParams,

View file

@ -22,6 +22,7 @@ export {
getESQLQueryColumns,
getESQLQueryColumnsRaw,
getESQLResults,
formatESQLColumns,
getStartEndParams,
hasStartEndParams,
} from './utils/run_query';

View file

@ -9,7 +9,8 @@
"requiredPlugins": [
"data",
"uiActions",
"fieldFormats"
"fieldFormats",
"share",
],
"requiredBundles": [
"kibanaReact",

View file

@ -22,9 +22,15 @@ interface ESQLDataGridProps {
dataView: DataView;
columns: DatatableColumn[];
query: AggregateQuery;
/**
* Optional parameters
*/
flyoutType?: 'overlay' | 'push';
isTableView?: boolean;
initialColumns?: DatatableColumn[];
fullHeight?: boolean;
initialRowHeight?: number;
controlColumnIds?: string[]; // default: ['openDetails', 'select']
}
const DataGridLazy = withSuspense(lazy(() => import('./data_grid')));
@ -35,6 +41,10 @@ export const ESQLDataGrid = (props: ESQLDataGridProps) => {
return Promise.all([startServicesPromise]);
}, []);
const getWrapper = (children: JSX.Element) => {
return props.fullHeight ? <div style={{ height: 500 }}>{children}</div> : <>{children}</>;
};
const deps = value?.[0];
if (loading || !deps) return <EuiLoadingSpinner />;
@ -45,14 +55,15 @@ export const ESQLDataGrid = (props: ESQLDataGridProps) => {
}}
>
<CellActionsProvider getTriggerCompatibleActions={deps.uiActions.getTriggerCompatibleActions}>
<div style={{ height: 500 }}>
{getWrapper(
<DataGridLazy
data={deps.data}
fieldFormats={deps.fieldFormats}
core={deps.core}
share={deps.share}
{...props}
/>
</div>
)}
</CellActionsProvider>
</KibanaContextProvider>
);

View file

@ -8,10 +8,19 @@
import React, { useState, useCallback, useMemo } from 'react';
import { zipObject } from 'lodash';
import { UnifiedDataTable, DataLoadingState, type SortOrder } from '@kbn/unified-data-table';
import {
UnifiedDataTable,
DataLoadingState,
type SortOrder,
renderCustomToolbar,
} from '@kbn/unified-data-table';
import { i18n } from '@kbn/i18n';
import { EuiLink, EuiText, EuiIcon } from '@elastic/eui';
import { css } from '@emotion/react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { ESQLRow } from '@kbn/es-types';
import type { DatatableColumn, DatatableColumnMeta } from '@kbn/expressions-plugin/common';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DataView } from '@kbn/data-views-plugin/common';
@ -24,6 +33,7 @@ interface ESQLDataGridProps {
core: CoreStart;
data: DataPublicPluginStart;
fieldFormats: FieldFormatsStart;
share?: SharePluginStart;
rows: ESQLRow[];
dataView: DataView;
columns: DatatableColumn[];
@ -31,6 +41,8 @@ interface ESQLDataGridProps {
flyoutType?: 'overlay' | 'push';
isTableView?: boolean;
initialColumns?: DatatableColumn[];
initialRowHeight?: number;
controlColumnIds?: string[];
}
type DataTableColumnsMeta = Record<
string,
@ -41,13 +53,19 @@ type DataTableColumnsMeta = Record<
>;
const sortOrder: SortOrder[] = [];
const DEFAULT_INITIAL_ROW_HEIGHT = 5;
const DEFAULT_ROWS_PER_PAGE = 10;
const ROWS_PER_PAGE_OPTIONS = [10, 25];
const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
const [activeColumns, setActiveColumns] = useState<string[]>(
(props.initialColumns || (props.isTableView ? props.columns : [])).map((c) => c.name)
);
const [rowHeight, setRowHeight] = useState<number>(5);
const [rowHeight, setRowHeight] = useState<number>(
props.initialRowHeight ?? DEFAULT_INITIAL_ROW_HEIGHT
);
const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE);
const onSetColumns = useCallback((columns) => {
setActiveColumns(columns);
@ -123,9 +141,71 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
props.fieldFormats,
]);
const discoverLocator = useMemo(() => {
return props.share?.url.locators.get('DISCOVER_APP_LOCATOR');
}, [props.share?.url.locators]);
const renderToolbar = useCallback(
(customToolbarProps) => {
const discoverLink = discoverLocator?.getRedirectUrl({
dataViewSpec: props.dataView.toSpec(),
timeRange: props.data.query.timefilter.timefilter.getTime(),
query: props.query,
columns: activeColumns,
});
return renderCustomToolbar({
...customToolbarProps,
toolbarProps: {
...customToolbarProps.toolbarProps,
hasRoomForGridControls: true,
},
gridProps: {
additionalControls: (
<EuiLink
href={discoverLink}
target="_blank"
color="primary"
css={css`
display: flex;
align-items: center;
`}
external={false}
>
<EuiIcon
type="discoverApp"
size="s"
color="primary"
css={css`
margin-right: 4px;
`}
/>
<EuiText size="xs">
{i18n.translate('esqlDataGrid.openInDiscoverLabel', {
defaultMessage: 'Open in Discover',
})}
</EuiText>
</EuiLink>
),
},
});
},
[
activeColumns,
discoverLocator,
props.data.query.timefilter.timefilter,
props.dataView,
props.query,
]
);
return (
<UnifiedDataTable
columns={activeColumns}
css={css`
.unifiedDataTableToolbar {
padding: 4px 0px;
}
`}
rows={rows}
columnsMeta={columnsMeta}
services={services}
@ -134,8 +214,10 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
loadingState={DataLoadingState.loaded}
dataView={props.dataView}
sampleSizeState={rows.length}
rowsPerPageState={10}
rowsPerPageState={rowsPerPage}
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
onSetColumns={onSetColumns}
onUpdateRowsPerPage={setRowsPerPage}
expandedDoc={expandedDoc}
setExpandedDoc={setExpandedDoc}
showTimeCol
@ -146,9 +228,11 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
maxDocFieldsDisplayed={100}
renderDocumentView={renderDocumentView}
showFullScreenButton={false}
configRowHeight={5}
configRowHeight={DEFAULT_INITIAL_ROW_HEIGHT}
rowHeightState={rowHeight}
onUpdateRowHeight={setRowHeight}
controlColumnIds={props.controlColumnIds}
renderCustomToolbar={discoverLocator ? renderToolbar : undefined}
/>
);
};

View file

@ -11,6 +11,7 @@ import type { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
export let core: CoreStart;
@ -19,6 +20,7 @@ interface ServiceDeps {
data: DataPublicPluginStart;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
share?: SharePluginStart;
}
const servicesReady$ = new BehaviorSubject<ServiceDeps | undefined>(undefined);
@ -38,7 +40,8 @@ export const setKibanaServices = (
kibanaCore: CoreStart,
data: DataPublicPluginStart,
uiActions: UiActionsStart,
fieldFormats: FieldFormatsStart
fieldFormats: FieldFormatsStart,
share?: SharePluginStart
) => {
core = kibanaCore;
servicesReady$.next({
@ -46,5 +49,6 @@ export const setKibanaServices = (
data,
uiActions,
fieldFormats,
share,
});
};

View file

@ -10,20 +10,25 @@ import type { Plugin, CoreStart, CoreSetup } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { setKibanaServices } from './kibana_services';
interface ESQLDataGridPluginStart {
data: DataPublicPluginStart;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
share?: SharePluginStart;
}
export class ESQLDataGridPlugin implements Plugin<{}, void> {
public setup(_: CoreSetup, {}: {}) {
return {};
}
public start(core: CoreStart, { data, uiActions, fieldFormats }: ESQLDataGridPluginStart): void {
setKibanaServices(core, data, uiActions, fieldFormats);
public start(
core: CoreStart,
{ data, uiActions, fieldFormats, share }: ESQLDataGridPluginStart
): void {
setKibanaServices(core, data, uiActions, fieldFormats, share);
}
public stop() {}

View file

@ -25,6 +25,8 @@
"@kbn/unified-doc-viewer-plugin",
"@kbn/core-notifications-browser",
"@kbn/shared-ux-utility",
"@kbn/share-plugin",
"@kbn/i18n",
],
"exclude": [
"target/**/*",

View file

@ -56,6 +56,7 @@
"embeddable",
"fieldFormats",
"charts",
"esqlDataGrid",
"esql",
],
"extraPublicDirs": [

View file

@ -0,0 +1,105 @@
/*
* 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 React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiTitle, EuiAccordion, EuiSpacer, EuiFlexItem, EuiNotificationBadge } from '@elastic/eui';
import type { AggregateQuery } from '@kbn/es-query';
import { euiThemeVars } from '@kbn/ui-theme';
import { ESQLDataGrid } from '@kbn/esql-datagrid/public';
import type { ESQLDataGridAttrs } from './helpers';
interface ESQLDataGridAccordionProps {
isAccordionOpen: boolean;
dataGridAttrs: ESQLDataGridAttrs;
query: AggregateQuery;
isTableView: boolean;
setIsAccordionOpen: (flag: boolean) => void;
onAccordionToggleCb: (status: boolean) => void;
}
export const ESQLDataGridAccordion = ({
isAccordionOpen,
dataGridAttrs,
query,
isTableView,
setIsAccordionOpen,
onAccordionToggleCb,
}: ESQLDataGridAccordionProps) => {
const onAccordionToggle = useCallback(
(status: boolean) => {
setIsAccordionOpen(!isAccordionOpen);
onAccordionToggleCb(status);
},
[isAccordionOpen, onAccordionToggleCb, setIsAccordionOpen]
);
return (
<EuiFlexItem
grow={isAccordionOpen ? 1 : false}
data-test-subj="ESQLQueryResults"
css={css`
.euiAccordion__childWrapper {
flex: ${isAccordionOpen ? 1 : 'none'};
}
padding: 0 ${euiThemeVars.euiSize};
border-bottom: ${euiThemeVars.euiBorderThin};
`}
>
<EuiAccordion
id="esql-results"
css={css`
.euiAccordion__children {
display: flex;
flex-direction: column;
height: 100%;
}
`}
buttonContent={
<EuiTitle
size="xxs"
css={css`
padding: 2px;
}
`}
>
<h5>
{i18n.translate('xpack.lens.config.ESQLQueryResultsTitle', {
defaultMessage: 'ES|QL Query Results',
})}
</h5>
</EuiTitle>
}
buttonProps={{
paddingSize: 'm',
}}
initialIsOpen={isAccordionOpen}
forceState={isAccordionOpen ? 'open' : 'closed'}
onToggle={onAccordionToggle}
extraAction={
<EuiNotificationBadge size="m" color="subdued">
{dataGridAttrs.rows.length}
</EuiNotificationBadge>
}
>
<>
<ESQLDataGrid
rows={dataGridAttrs?.rows}
columns={dataGridAttrs?.columns}
dataView={dataGridAttrs?.dataView}
query={query}
flyoutType="overlay"
isTableView={isTableView}
initialRowHeight={0}
controlColumnIds={['openDetails']}
/>
<EuiSpacer />
</>
</EuiAccordion>
</EuiFlexItem>
);
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { getESQLQueryColumns } from '@kbn/esql-utils';
import { getESQLResults } from '@kbn/esql-utils';
import type { LensPluginStartDependencies } from '../../../plugin';
import { createMockStartDependencies } from '../../../editor_frame_service/mocks';
import {
@ -18,7 +18,7 @@ import { suggestionsApi } from '../../../lens_suggestions_api';
import { getSuggestions } from './helpers';
const mockSuggestionApi = suggestionsApi as jest.Mock;
const mockFetchData = getESQLQueryColumns as jest.Mock;
const mockFetchData = getESQLResults as jest.Mock;
jest.mock('../../../lens_suggestions_api', () => ({
suggestionsApi: jest.fn(() => mockAllSuggestions),
@ -26,7 +26,37 @@ jest.mock('../../../lens_suggestions_api', () => ({
jest.mock('@kbn/esql-utils', () => {
return {
getESQLQueryColumns: jest.fn().mockResolvedValue(() => [
getESQLResults: jest.fn().mockResolvedValue({
response: {
columns: [
{
name: '@timestamp',
id: '@timestamp',
meta: {
type: 'date',
},
},
{
name: 'bytes',
id: 'bytes',
meta: {
type: 'number',
},
},
{
name: 'memory',
id: 'memory',
meta: {
type: 'number',
},
},
],
values: [],
},
}),
getIndexPatternFromESQLQuery: jest.fn().mockReturnValue('index1'),
getESQLAdHocDataview: jest.fn().mockResolvedValue({}),
formatESQLColumns: jest.fn().mockReturnValue([
{
name: '@timestamp',
id: '@timestamp',
@ -49,7 +79,6 @@ jest.mock('@kbn/esql-utils', () => {
},
},
]),
getIndexPatternFromESQLQuery: jest.fn().mockReturnValue('index1'),
};
});

View file

@ -7,16 +7,59 @@
import {
getIndexPatternFromESQLQuery,
getESQLAdHocDataview,
getESQLQueryColumns,
getESQLResults,
formatESQLColumns,
} from '@kbn/esql-utils';
import type { AggregateQuery } from '@kbn/es-query';
import type { ESQLRow } from '@kbn/es-types';
import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils';
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component';
import type { LensPluginStartDependencies } from '../../../plugin';
import type { DatasourceMap, VisualizationMap } from '../../../types';
import { suggestionsApi } from '../../../lens_suggestions_api';
export interface ESQLDataGridAttrs {
rows: ESQLRow[];
dataView: DataView;
columns: DatatableColumn[];
}
export const getGridAttrs = async (
query: AggregateQuery,
adHocDataViews: DataViewSpec[],
deps: LensPluginStartDependencies,
abortController?: AbortController
): Promise<ESQLDataGridAttrs> => {
const indexPattern = getIndexPatternFromESQLQuery(query.esql);
const dataViewSpec = adHocDataViews.find((adHoc) => {
return adHoc.name === indexPattern;
});
const [results, dataView] = await Promise.all([
getESQLResults({
esqlQuery: query.esql,
search: deps.data.search.search,
signal: abortController?.signal,
dropNullColumns: true,
timeRange: deps.data.query.timefilter.timefilter.getAbsoluteTime(),
}),
dataViewSpec
? deps.dataViews.create(dataViewSpec)
: getESQLAdHocDataview(query.esql, deps.dataViews),
]);
const columns = formatESQLColumns(results.response.columns);
return {
rows: results.response.values,
dataView,
columns,
};
};
export const getSuggestions = async (
query: AggregateQuery,
deps: LensPluginStartDependencies,
@ -24,24 +67,23 @@ export const getSuggestions = async (
visualizationMap: VisualizationMap,
adHocDataViews: DataViewSpec[],
setErrors: (errors: Error[]) => void,
abortController?: AbortController
abortController?: AbortController,
setDataGridAttrs?: (attrs: ESQLDataGridAttrs) => void
) => {
try {
const indexPattern = getIndexPatternFromESQLQuery(query.esql);
const dataViewSpec = adHocDataViews.find((adHoc) => {
return adHoc.name === indexPattern;
const { dataView, columns, rows } = await getGridAttrs(
query,
adHocDataViews,
deps,
abortController
);
setDataGridAttrs?.({
rows,
dataView,
columns,
});
const dataView = dataViewSpec
? await deps.dataViews.create(dataViewSpec)
: await getESQLAdHocDataview(query.esql, deps.dataViews);
const columns = await getESQLQueryColumns({
esqlQuery: 'esql' in query ? query.esql : '',
search: deps.data.search.search,
signal: abortController?.signal,
timeRange: deps.data.query.timefilter.timefilter.getAbsoluteTime(),
});
const context = {
dataViewSpec: dataView?.toSpec(false),
fieldName: '',

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { renderWithReduxStore } from '../../../mocks';
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { Query, AggregateQuery } from '@kbn/es-query';
import { coreMock } from '@kbn/core/public/mocks';
@ -17,6 +17,64 @@ import type { TypedLensByValueInput } from '../../../embeddable/embeddable_compo
import { LensEditConfigurationFlyout } from './lens_configuration_flyout';
import type { EditConfigPanelProps } from './types';
jest.mock('@kbn/esql-utils', () => {
return {
getESQLResults: jest.fn().mockResolvedValue({
response: {
columns: [
{
name: '@timestamp',
id: '@timestamp',
meta: {
type: 'date',
},
},
{
name: 'bytes',
id: 'bytes',
meta: {
type: 'number',
},
},
{
name: 'memory',
id: 'memory',
meta: {
type: 'number',
},
},
],
values: [],
},
}),
getIndexPatternFromESQLQuery: jest.fn().mockReturnValue('index1'),
getESQLAdHocDataview: jest.fn().mockResolvedValue({}),
formatESQLColumns: jest.fn().mockReturnValue([
{
name: '@timestamp',
id: '@timestamp',
meta: {
type: 'date',
},
},
{
name: 'bytes',
id: 'bytes',
meta: {
type: 'number',
},
},
{
name: 'memory',
id: 'memory',
meta: {
type: 'number',
},
},
]),
};
});
const lensAttributes = {
title: 'test',
visualizationType: 'testVis',
@ -38,11 +96,27 @@ const lensAttributes = {
} as unknown as TypedLensByValueInput['attributes'];
const mockStartDependencies =
createMockStartDependencies() as unknown as LensPluginStartDependencies;
const data = mockDataPlugin();
(data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
from: 'now-2m',
to: 'now',
});
const data = {
...mockDataPlugin(),
query: {
...mockDataPlugin().query,
timefilter: {
...mockDataPlugin().query.timefilter,
timefilter: {
...mockDataPlugin().query.timefilter.timefilter,
getTime: jest.fn(() => ({
from: 'now-2m',
to: 'now',
})),
getAbsoluteTime: jest.fn(() => ({
from: '2021-01-10T04:00:00.000Z',
to: '2021-01-10T04:00:00.000Z',
})),
},
},
},
};
const startDependencies = {
...mockStartDependencies,
data,
@ -196,6 +270,14 @@ describe('LensEditConfigurationFlyout', () => {
expect(screen.getByTestId('InlineEditingESQLEditor')).toBeInTheDocument();
expect(screen.getByTestId('InlineEditingSuggestions')).toBeInTheDocument();
});
it('should display the ES|QL results table if canEditTextBasedQuery prop is true', async () => {
renderConfigFlyout({
canEditTextBasedQuery: true,
});
await waitFor(() => expect(screen.getByTestId('ESQLQueryResults')).toBeInTheDocument());
});
it('save button is disabled if no changes have been made', async () => {
const updateByRefInputSpy = jest.fn();
const saveByRefSpy = jest.fn();

View file

@ -45,10 +45,11 @@ import {
import { LayerConfiguration } from './layer_configuration_section';
import type { EditConfigPanelProps } from './types';
import { FlyoutWrapper } from './flyout_wrapper';
import { getSuggestions } from './helpers';
import { getSuggestions, getGridAttrs, type ESQLDataGridAttrs } from './helpers';
import { SuggestionPanel } from '../../../editor_frame_service/editor_frame/suggestion_panel';
import { useApplicationUserMessages } from '../../get_application_user_messages';
import { trackUiCounterEvents } from '../../../lens_ui_telemetry';
import { ESQLDataGridAccordion } from './esql_data_grid_accordion';
export function LensEditConfigurationFlyout({
attributes,
@ -86,7 +87,9 @@ export function LensEditConfigurationFlyout({
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];
@ -107,6 +110,9 @@ export function LensEditConfigurationFlyout({
[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 = output$?.subscribe(() => {
@ -128,6 +134,30 @@ export function LensEditConfigurationFlyout({
return () => s?.unsubscribe();
}, [dispatch, output$, layers]);
useEffect(() => {
const abortController = new AbortController();
const getESQLGridAttrs = async () => {
if (!dataGridAttrs && isOfAggregateQueryType(query)) {
const { dataView, columns, rows } = await getGridAttrs(
query,
adHocDataViews,
startDependencies,
abortController
);
setDataGridAttrs({
rows,
dataView,
columns,
});
}
};
getESQLGridAttrs();
return () => {
abortController.abort();
};
}, [adHocDataViews, dataGridAttrs, query, startDependencies]);
const attributesChanged: boolean = useMemo(() => {
const previousAttrs = previousAttributes.current;
@ -284,9 +314,6 @@ export function LensEditConfigurationFlyout({
visualizationState: visualization,
});
// needed for text based languages mode which works ONLY with adHoc dataviews
const adHocDataViews = Object.values(attributes.state.adHocDataViews ?? {});
const runQuery = useCallback(
async (q, abortController) => {
const attrs = await getSuggestions(
@ -296,7 +323,8 @@ export function LensEditConfigurationFlyout({
visualizationMap,
adHocDataViews,
setErrors,
abortController
abortController,
setDataGridAttrs
);
if (attrs) {
setCurrentAttributes?.(attrs);
@ -479,6 +507,23 @@ export function LensEditConfigurationFlyout({
/>
</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);
}
}}
/>
)}
<EuiFlexItem
grow={isLayerAccordionOpen ? 1 : false}
css={css`
@ -514,6 +559,9 @@ export function LensEditConfigurationFlyout({
if (status && isSuggestionsAccordionOpen) {
setIsSuggestionsAccordionOpen(!status);
}
if (status && isESQLResultsAccordionOpen) {
setIsESQLResultsAccordionOpen(!status);
}
setIsLayerAccordionOpen(!isLayerAccordionOpen);
}}
>
@ -562,6 +610,9 @@ export function LensEditConfigurationFlyout({
if (!status && isLayerAccordionOpen) {
setIsLayerAccordionOpen(status);
}
if (status && isESQLResultsAccordionOpen) {
setIsESQLResultsAccordionOpen(!status);
}
setIsSuggestionsAccordionOpen(!isSuggestionsAccordionOpen);
}}
/>

View file

@ -66,6 +66,7 @@ const openInlineLensConfigEditor = (
'data-test-subj': 'customizeLens',
className: 'lnsConfigPanel__overlay',
hideCloseButton: true,
isResizable: true,
onClose: (overlayRef) => {
overlayTracker?.clearOverlays();
overlayRef.close();

View file

@ -4,6 +4,8 @@
@include euiBreakpoint('xs', 's', 'm') {
clip-path: none;
}
max-inline-size: $euiSizeXXL * 20;
min-inline-size: $euiSizeXXL * 8;
background: $euiColorLightestShade;
.kbnOverlayMountWrapper {
padding-left: $euiFormMaxWidth;

View file

@ -111,6 +111,8 @@
"@kbn/licensing-plugin",
"@kbn/react-kibana-context-render",
"@kbn/react-kibana-mount",
"@kbn/es-types",
"@kbn/esql-datagrid",
],
"exclude": ["target/**/*"]
}