[ES|QL] Render a Discover-like table in the assistant instead of a Lens chart (#184106)

## Summary

This PR does 2 things:

- Creates a new plugin that is a wrapper of the unified datatable and is
only for rendering as a table ES|QL results. The UnifiedDatatable
package is good but the consumers need to know all the properties to
understand how to use it and the necessity of displaying in a table the
results of an ES|QL query comes a lot lately. This plugin has only 3
required properties (rows, columns, query) which make it very easy for
the consumers to use it. It also integrates the Row Viewer flyout

- It changes the implementation of the obs ai assistant to render a
Discover like table instead of a Lens table. The Discover-like table is
much better on rendering a table with thousands of columns and is going
to be much more helpful for our users.

The same plugin can be used later for the inline ediitng flyout too in a
dashboard if we want to also display the results of an ES|QL query.

Some screenshots of the new possibilities in the assistant:

- I can see the results of an ES|QL query in a visualization


![meow](27f77ca3-633b-45f2-b935-42c62c184a04)

- I can render my results as a Document view

<img width="880" alt="image"
src="e8034e10-325d-4d9e-b8a5-34d01b0dbd9d">

<img width="1095" alt="image"
src="c8236e65-96aa-4fcb-b7c3-835e2a5665bd">

<img width="955" alt="image"
src="78b1d664-6863-42bf-a337-659143b7683d">


### Checklist

Delete any items that are not applicable to this PR.

- [ ] 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] 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))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] 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-06-05 20:20:20 +02:00 committed by GitHub
parent eaed27c14b
commit 5860259222
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1066 additions and 105 deletions

1
.github/CODEOWNERS vendored
View file

@ -411,6 +411,7 @@ examples/eso_model_version_example @elastic/kibana-security
x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security
packages/kbn-esql-ast @elastic/kibana-esql
examples/esql_ast_inspector @elastic/kibana-esql
src/plugins/esql_datagrid @elastic/kibana-esql
packages/kbn-esql-utils @elastic/kibana-esql
packages/kbn-esql-validation-autocomplete @elastic/kibana-esql
examples/esql_validation_example @elastic/kibana-esql

View file

@ -116,6 +116,7 @@
"coloring": "packages/kbn-coloring/src",
"languageDocumentationPopover": "packages/kbn-language-documentation-popover/src",
"textBasedLanguages": "src/plugins/text_based_languages",
"esqlDataGrid": "src/plugins/esql_datagrid",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"],
"timelion": ["src/plugins/vis_types/timelion"],

View file

@ -102,6 +102,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a
|Embeddables are React components that manage their own state, can be serialized and deserialized, and return an API that can be used to interact with them imperatively.
|{kib-repo}blob/{branch}/src/plugins/esql_datagrid/README.md[esqlDataGrid]
|Contains a Discover-like table specifically for ES|QL queries:
|{kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared]
|This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module.

View file

@ -457,6 +457,7 @@
"@kbn/eso-plugin": "link:x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin",
"@kbn/esql-ast": "link:packages/kbn-esql-ast",
"@kbn/esql-ast-inspector-plugin": "link:examples/esql_ast_inspector",
"@kbn/esql-datagrid": "link:src/plugins/esql_datagrid",
"@kbn/esql-utils": "link:packages/kbn-esql-utils",
"@kbn/esql-validation-autocomplete": "link:packages/kbn-esql-validation-autocomplete",
"@kbn/esql-validation-example-plugin": "link:examples/esql_validation_example",

View file

@ -42,6 +42,7 @@ pageLoadAssetSize:
embeddable: 87309
embeddableEnhanced: 22107
enterpriseSearch: 50858
esqlDataGrid: 24598
esUiShared: 326654
eventAnnotation: 30000
eventAnnotationListing: 25841

View file

@ -7,7 +7,7 @@
*/
export { UnifiedDataTable, DataLoadingState } from './src/components/data_table';
export type { UnifiedDataTableProps } from './src/components/data_table';
export type { UnifiedDataTableProps, SortOrder } from './src/components/data_table';
export {
RowHeightSettings,
type RowHeightSettingsProps,

View file

@ -267,7 +267,7 @@ export interface UnifiedDataTableProps {
theme: ThemeServiceStart;
fieldFormats: FieldFormatsStart;
uiSettings: IUiSettingsClient;
dataViewFieldEditor: DataViewFieldEditorStart;
dataViewFieldEditor?: DataViewFieldEditorStart;
toastNotifications: ToastsStart;
storage: Storage;
data: DataPublicPluginStart;
@ -611,7 +611,7 @@ export const UnifiedDataTable = ({
useNewFieldsApi,
shouldShowFieldHandler,
closePopover: () => dataGridRef.current?.closeCellPopover(),
fieldFormats: services.fieldFormats,
fieldFormats,
maxEntries: maxDocFieldsDisplayed,
externalCustomRenderers,
isPlainRecord,
@ -622,7 +622,7 @@ export const UnifiedDataTable = ({
useNewFieldsApi,
shouldShowFieldHandler,
maxDocFieldsDisplayed,
services.fieldFormats,
fieldFormats,
externalCustomRenderers,
isPlainRecord,
]
@ -651,18 +651,20 @@ export const UnifiedDataTable = ({
() =>
onFieldEdited
? (fieldName: string) => {
closeFieldEditor.current = services.dataViewFieldEditor.openEditor({
ctx: {
dataView,
},
fieldName,
onSave: async () => {
await onFieldEdited();
},
});
closeFieldEditor.current =
onFieldEdited &&
services?.dataViewFieldEditor?.openEditor({
ctx: {
dataView,
},
fieldName,
onSave: async () => {
await onFieldEdited();
},
});
}
: undefined,
[dataView, onFieldEdited, services.dataViewFieldEditor]
[dataView, onFieldEdited, services?.dataViewFieldEditor]
);
const timeFieldName = dataView.timeFieldName;
@ -756,7 +758,8 @@ export const UnifiedDataTable = ({
uiSettings,
toastNotifications,
},
hasEditDataViewPermission: () => dataViewFieldEditor.userPermissions.editIndexPattern(),
hasEditDataViewPermission: () =>
Boolean(dataViewFieldEditor?.userPermissions?.editIndexPattern()),
valueToStringConverter,
onFilter,
editField,

View file

@ -0,0 +1,6 @@
{
"prefix": "esqlDataGrid",
"paths": {
"esqlDataGrid": "."
}
}

View file

@ -0,0 +1,47 @@
# @kbn/esql-datagrid
Contains a Discover-like table specifically for ES|QL queries:
- You have to run the esql query on your application, this is just a UI component
- You pass the columns and rows of the _query response to the table
- The table operates in both Document view and table view mode, define this with the `isTableView` property
- The table offers a built in Row Viewer flyout
- The table offers a rows comparison mode, exactly as Discover
---
### Properties
* rows: ESQLRow[], is the array of values returned by the _query api
* columns: DatatableColumn[], is the array of columns in a kibana compatible format. You can sue the `formatESQLColumns` helper function from the `@kbn/esql-utils` package
* query: AggregateQuery, the ES|QL query in the format of
```json
{
esql: <queryString>
}
```
* flyoutType?: "overlay" | "push", defines the type of flyout for the Row Viewer
* isTableView?: boolean, defines if the table will render as a Document Viewer or a Table View
### How to use it
```tsx
import { getIndexPatternFromESQLQuery, getESQLAdHocDataview, formatESQLColumns } from '@kbn/esql-utils';
import { ESQLDataGrid } from '@kbn/esql-datagrid/public';
/**
Run the _query api to get the datatable with the ES|QL query you want.
This will return a response with columns and values
**/
const indexPattern = getIndexPatternFromESQLQuery(query);
const adHocDataView = getESQLAdHocDataview(indexPattern, dataViewService);
const formattedColumns = formatESQLColumns(columns);
<ESQLDataGrid
rows={values}
columns={formattedColumns}
dataView={adHocDataView}
query={{ esql: query }}
flyoutType="overlay"
isTableView
/>
```

View file

@ -0,0 +1,19 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/esql_datagrid'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/esql_datagrid',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/plugins/esql_datagrid/{common,public,server}/**/*.{js,ts,tsx}',
],
setupFiles: ['jest-canvas-mock'],
};

View file

@ -0,0 +1,21 @@
{
"type": "plugin",
"id": "@kbn/esql-datagrid",
"owner": "@elastic/kibana-esql",
"plugin": {
"id": "esqlDataGrid",
"server": false,
"browser": true,
"requiredPlugins": [
"data",
"uiActions",
"fieldFormats"
],
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"dataViews",
"unifiedDocViewer"
]
}
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/esql-datagrid",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,58 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { ESQLRow } from '@kbn/es-types';
import type { AggregateQuery } from '@kbn/es-query';
import { withSuspense } from '@kbn/shared-ux-utility';
import useAsync from 'react-use/lib/useAsync';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { CellActionsProvider } from '@kbn/cell-actions';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { untilPluginStartServicesReady } from './kibana_services';
interface ESQLDataGridProps {
rows: ESQLRow[];
dataView: DataView;
columns: DatatableColumn[];
query: AggregateQuery;
flyoutType?: 'overlay' | 'push';
isTableView?: boolean;
}
const DataGridLazy = withSuspense(lazy(() => import('./data_grid')));
export const ESQLDataGrid = (props: ESQLDataGridProps) => {
const { loading, value } = useAsync(() => {
const startServicesPromise = untilPluginStartServicesReady();
return Promise.all([startServicesPromise]);
}, []);
const deps = value?.[0];
if (loading || !deps) return <EuiLoadingSpinner />;
return (
<KibanaContextProvider
services={{
...deps,
}}
>
<CellActionsProvider getTriggerCompatibleActions={deps.uiActions.getTriggerCompatibleActions}>
<div style={{ height: 500 }}>
<DataGridLazy
data={deps.data}
fieldFormats={deps.fieldFormats}
core={deps.core}
{...props}
/>
</div>
</CellActionsProvider>
</KibanaContextProvider>
);
};

View file

@ -0,0 +1,156 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, useCallback, useMemo } from 'react';
import { zipObject } from 'lodash';
import { UnifiedDataTable, DataLoadingState, type SortOrder } from '@kbn/unified-data-table';
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 { AggregateQuery } from '@kbn/es-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { RowViewer } from './row_viewer_lazy';
interface ESQLDataGridProps {
core: CoreStart;
data: DataPublicPluginStart;
fieldFormats: FieldFormatsStart;
rows: ESQLRow[];
dataView: DataView;
columns: DatatableColumn[];
query: AggregateQuery;
flyoutType?: 'overlay' | 'push';
isTableView?: boolean;
}
type DataTableColumnsMeta = Record<
string,
{
type: DatatableColumnMeta['type'];
esType?: DatatableColumnMeta['esType'];
}
>;
const sortOrder: SortOrder[] = [];
const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
const [activeColumns, setActiveColumns] = useState<string[]>(
props.isTableView ? props.columns.map((c) => c.name) : []
);
const [rowHeight, setRowHeight] = useState<number>(5);
const onSetColumns = useCallback((columns) => {
setActiveColumns(columns);
}, []);
const renderDocumentView = useCallback(
(
hit: DataTableRecord,
displayedRows: DataTableRecord[],
displayedColumns: string[],
customColumnsMeta?: DataTableColumnsMeta
) => (
<RowViewer
dataView={props.dataView}
toastNotifications={props.core.notifications}
hit={hit}
hits={displayedRows}
columns={displayedColumns}
columnsMeta={customColumnsMeta}
flyoutType={props.flyoutType ?? 'push'}
onRemoveColumn={(column) => {
setActiveColumns(activeColumns.filter((c) => c !== column));
}}
onAddColumn={(column) => {
setActiveColumns([...activeColumns, column]);
}}
onClose={() => setExpandedDoc(undefined)}
setExpandedDoc={setExpandedDoc}
/>
),
[activeColumns, props.core.notifications, props.dataView, props.flyoutType]
);
const columnsMeta = useMemo(() => {
return props.columns.reduce((acc, column) => {
acc[column.id] = {
type: column.meta?.type,
esType: column.meta?.esType ?? column.meta?.type,
};
return acc;
}, {} as DataTableColumnsMeta);
}, [props.columns]);
const rows: DataTableRecord[] = useMemo(() => {
const columnNames = props.columns?.map(({ name }) => name);
return props.rows
.map((row) => zipObject(columnNames, row))
.map((row, idx: number) => {
return {
id: String(idx),
raw: row,
flattened: row,
} as unknown as DataTableRecord;
});
}, [props.columns, props.rows]);
const services = useMemo(() => {
const storage = new Storage(localStorage);
return {
data: props.data,
theme: props.core.theme,
uiSettings: props.core.uiSettings,
toastNotifications: props.core.notifications.toasts,
fieldFormats: props.fieldFormats,
storage,
};
}, [
props.core.notifications.toasts,
props.core.theme,
props.core.uiSettings,
props.data,
props.fieldFormats,
]);
return (
<UnifiedDataTable
columns={activeColumns}
rows={rows}
columnsMeta={columnsMeta}
services={services}
isPlainRecord
isSortEnabled={false}
loadingState={DataLoadingState.loaded}
dataView={props.dataView}
sampleSizeState={rows.length}
rowsPerPageState={10}
onSetColumns={onSetColumns}
expandedDoc={expandedDoc}
setExpandedDoc={setExpandedDoc}
showTimeCol
useNewFieldsApi
enableComparisonMode
sort={sortOrder}
ariaLabelledBy="esqlDataGrid"
maxDocFieldsDisplayed={100}
renderDocumentView={renderDocumentView}
showFullScreenButton={false}
configRowHeight={5}
rowHeightState={rowHeight}
onUpdateRowHeight={setRowHeight}
/>
);
};
// eslint-disable-next-line import/no-default-export
export default DataGrid;

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ESQLDataGridPlugin } from './plugin';
export { ESQLDataGrid } from './create_datagrid';
export function plugin() {
return new ESQLDataGridPlugin();
}

View file

@ -0,0 +1,50 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject } from 'rxjs';
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';
export let core: CoreStart;
interface ServiceDeps {
core: CoreStart;
data: DataPublicPluginStart;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
}
const servicesReady$ = new BehaviorSubject<ServiceDeps | undefined>(undefined);
export const untilPluginStartServicesReady = () => {
if (servicesReady$.value) return Promise.resolve(servicesReady$.value);
return new Promise<ServiceDeps>((resolve) => {
const subscription = servicesReady$.subscribe((deps) => {
if (deps) {
subscription.unsubscribe();
resolve(deps);
}
});
});
};
export const setKibanaServices = (
kibanaCore: CoreStart,
data: DataPublicPluginStart,
uiActions: UiActionsStart,
fieldFormats: FieldFormatsStart
) => {
core = kibanaCore;
servicesReady$.next({
core,
data,
uiActions,
fieldFormats,
});
};

View file

@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
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 { setKibanaServices } from './kibana_services';
interface ESQLDataGridPluginStart {
data: DataPublicPluginStart;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
}
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 stop() {}
}

View file

@ -0,0 +1,119 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DataView } from '@kbn/data-views-plugin/common';
import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin';
import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__';
import { RowViewer } from './row_viewer';
describe('RowViewer', () => {
function renderComponent(closeFlyoutSpy?: jest.Mock, extraHit?: DataTableRecord) {
const dataView = {
title: 'foo',
id: 'foo',
name: 'foo',
toSpec: jest.fn(),
toMinimalSpec: jest.fn(),
isPersisted: jest.fn().mockReturnValue(false),
fields: {
getByName: jest.fn(),
},
timeFieldName: 'timestamp',
};
const columns = ['bytes', 'destination'];
const hit = {
flattened: {
bytes: 123,
destination: 'Amsterdam',
},
id: '1',
raw: {
bytes: 123,
destination: 'Amsterdam',
},
} as unknown as DataTableRecord;
const hits = [hit];
if (extraHit) {
hits.push(extraHit);
}
const services = {
toastNotifications: {
addSuccess: jest.fn(),
},
};
setUnifiedDocViewerServices(mockUnifiedDocViewerServices);
render(
<KibanaContextProvider services={services}>
<RowViewer
dataView={dataView as unknown as DataView}
toastNotifications={
{
toasts: {
addSuccess: jest.fn(),
},
} as unknown as CoreStart['notifications']
}
hit={hit}
hits={hits}
columns={columns}
flyoutType={'push'}
onRemoveColumn={jest.fn()}
onAddColumn={jest.fn()}
onClose={closeFlyoutSpy ?? jest.fn()}
setExpandedDoc={jest.fn()}
/>
</KibanaContextProvider>
);
}
it('should render a flyout', async () => {
renderComponent();
await waitFor(() => expect(screen.getByTestId('esqlRowDetailsFlyout')).toBeInTheDocument());
});
it('should run the onClose prop when the close button is clicked', async () => {
const closeFlyoutSpy = jest.fn();
renderComponent(closeFlyoutSpy);
await waitFor(() => {
userEvent.click(screen.getByTestId('esqlRowDetailsFlyoutCloseBtn'));
expect(closeFlyoutSpy).toHaveBeenCalled();
});
});
it('displays row navigation when there is more than 1 row available', async () => {
renderComponent(undefined, {
flattened: {
bytes: 456,
destination: 'Athens',
},
id: '3',
raw: {
bytes: 456,
destination: 'Athens',
},
} as unknown as DataTableRecord);
await waitFor(() => {
expect(screen.getByTestId('esqlTableRowNavigation')).toBeInTheDocument();
});
});
it('doesnt display row navigation when there is only 1 row available', async () => {
renderComponent();
await waitFor(() => {
expect(screen.queryByTestId('esqlTableRowNavigation')).not.toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,235 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useMemo, useCallback } from 'react';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutResizable,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiPortal,
EuiPagination,
keys,
EuiButtonEmpty,
useEuiTheme,
useIsWithinMinBreakpoint,
} from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DataTableColumnsMeta } from '@kbn/unified-data-table';
import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { NotificationsStart } from '@kbn/core-notifications-browser';
export interface RowViewerProps {
toastNotifications?: NotificationsStart;
columns: string[];
columnsMeta?: DataTableColumnsMeta;
hit: DataTableRecord;
hits?: DataTableRecord[];
flyoutType?: 'push' | 'overlay';
dataView: DataView;
onAddColumn: (column: string) => void;
onClose: () => void;
onRemoveColumn: (column: string) => void;
setExpandedDoc: (doc?: DataTableRecord) => void;
}
function getIndexByDocId(hits: DataTableRecord[], id: string) {
return hits.findIndex((h) => {
return h.id === id;
});
}
export const FLYOUT_WIDTH_KEY = 'esqlTable:flyoutWidth';
/**
* Flyout displaying an expanded ES|QL row
*/
export function RowViewer({
hit,
hits,
dataView,
columns,
columnsMeta,
toastNotifications,
flyoutType = 'push',
onClose,
onRemoveColumn,
onAddColumn,
setExpandedDoc,
}: RowViewerProps) {
const { euiTheme } = useEuiTheme();
const isXlScreen = useIsWithinMinBreakpoint('xl');
const DEFAULT_WIDTH = euiTheme.base * 34;
const defaultWidth = DEFAULT_WIDTH;
const [flyoutWidth, setFlyoutWidth] = useLocalStorage(FLYOUT_WIDTH_KEY, defaultWidth);
const minWidth = euiTheme.base * 24;
const maxWidth = euiTheme.breakpoint.xl;
const actualHit = useMemo(() => hits?.find(({ id }) => id === hit?.id) || hit, [hit, hits]);
const pageCount = useMemo<number>(() => (hits ? hits.length : 0), [hits]);
const activePage = useMemo<number>(() => {
const id = hit.id;
if (!hits || pageCount <= 1) {
return -1;
}
return getIndexByDocId(hits, id);
}, [hits, hit, pageCount]);
const setPage = useCallback(
(index: number) => {
if (hits && hits[index]) {
setExpandedDoc(hits[index]);
}
},
[hits, setExpandedDoc]
);
const onKeyDown = useCallback(
(ev: React.KeyboardEvent) => {
const nodeName = get(ev, 'target.nodeName', null);
if (typeof nodeName === 'string' && nodeName.toLowerCase() === 'input') {
return;
}
if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) {
ev.preventDefault();
ev.stopPropagation();
setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1));
}
},
[activePage, setPage]
);
const addColumn = useCallback(
(columnName: string) => {
onAddColumn(columnName);
toastNotifications?.toasts?.addSuccess?.(
i18n.translate('esqlDataGrid.grid.flyout.toastColumnAdded', {
defaultMessage: `Column '{columnName}' was added`,
values: { columnName },
})
);
},
[onAddColumn, toastNotifications]
);
const removeColumn = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
toastNotifications?.toasts?.addSuccess?.(
i18n.translate('esqlDataGrid.grid.flyout.toastColumnRemoved', {
defaultMessage: `Column '{columnName}' was removed`,
values: { columnName },
})
);
},
[onRemoveColumn, toastNotifications]
);
const renderDefaultContent = useCallback(
() => (
<UnifiedDocViewer
columns={columns}
columnsMeta={columnsMeta}
dataView={dataView}
hit={actualHit}
onAddColumn={addColumn}
onRemoveColumn={removeColumn}
textBasedHits={hits}
/>
),
[actualHit, addColumn, columns, columnsMeta, dataView, hits, removeColumn]
);
const bodyContent = renderDefaultContent();
return (
<EuiPortal>
<EuiFlyoutResizable
onClose={onClose}
type={flyoutType}
size={flyoutWidth}
pushMinBreakpoint="xl"
data-test-subj="esqlRowDetailsFlyout"
onKeyDown={onKeyDown}
ownFocus={true}
minWidth={minWidth}
maxWidth={maxWidth}
onResize={setFlyoutWidth}
css={{
maxWidth: `${isXlScreen ? `calc(100vw - ${DEFAULT_WIDTH}px)` : '90vw'} !important`,
}}
paddingSize="m"
>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
direction="row"
alignItems="center"
gutterSize="m"
responsive={false}
wrap={true}
>
<EuiFlexItem grow={false}>
<EuiTitle
size="xs"
data-test-subj="docTableRowDetailsTitle"
css={css`
white-space: nowrap;
`}
>
<h2>
{i18n.translate('esqlDataGrid.grid.tableRow.docViewerEsqlDetailHeading', {
defaultMessage: 'Result',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
{activePage !== -1 && (
<EuiFlexItem data-test-subj={`esqlTableRowNavigation-${activePage}`}>
<EuiPagination
aria-label={i18n.translate('esqlDataGrid.grid.flyout.rowNavigation', {
defaultMessage: 'Row navigation',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={setPage}
compressed
data-test-subj="esqlTableRowNavigation"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>{bodyContent}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty
iconType="cross"
onClick={onClose}
flush="left"
data-test-subj="esqlRowDetailsFlyoutCloseBtn"
>
{i18n.translate('esqlDataGrid.grid.flyout.close', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyoutResizable>
</EuiPortal>
);
}
// eslint-disable-next-line import/no-default-export
export default RowViewer;

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { withSuspense } from '@kbn/shared-ux-utility';
import { lazy } from 'react';
export type { RowViewerProps } from './row_viewer';
export const RowViewer = withSuspense(lazy(() => import('./row_viewer')));

View file

@ -0,0 +1,33 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
},
"include": [
"../../typings/**/*",
"common/**/*",
"public/**/*",
],
"kbn_references": [
"@kbn/data-plugin",
"@kbn/es-types",
"@kbn/es-query",
"@kbn/discover-utils",
"@kbn/data-views-plugin",
"@kbn/expressions-plugin",
"@kbn/cell-actions",
"@kbn/unified-data-table",
"@kbn/kibana-utils-plugin",
"@kbn/kibana-react-plugin",
"@kbn/core",
"@kbn/ui-actions-plugin",
"@kbn/field-formats-plugin",
"@kbn/i18n",
"@kbn/unified-doc-viewer-plugin",
"@kbn/core-notifications-browser",
"@kbn/shared-ux-utility"
],
"exclude": [
"target/**/*",
]
}

View file

@ -816,6 +816,8 @@
"@kbn/esql-ast/*": ["packages/kbn-esql-ast/*"],
"@kbn/esql-ast-inspector-plugin": ["examples/esql_ast_inspector"],
"@kbn/esql-ast-inspector-plugin/*": ["examples/esql_ast_inspector/*"],
"@kbn/esql-datagrid": ["src/plugins/esql_datagrid"],
"@kbn/esql-datagrid/*": ["src/plugins/esql_datagrid/*"],
"@kbn/esql-utils": ["packages/kbn-esql-utils"],
"@kbn/esql-utils/*": ["packages/kbn-esql-utils/*"],
"@kbn/esql-validation-autocomplete": ["packages/kbn-esql-validation-autocomplete"],

View file

@ -24,9 +24,9 @@
"licensing",
"ml",
"alerting",
"features"
"features",
],
"requiredBundles": ["kibanaReact"],
"requiredBundles": ["kibanaReact", "esqlDataGrid"],
"optionalPlugins": ["cloud"],
"extraPublicDirs": []
}

View file

@ -31,6 +31,9 @@ describe('VisualizeESQL', () => {
toSpec: jest.fn(),
toMinimalSpec: jest.fn(),
isPersisted: jest.fn().mockReturnValue(false),
fields: {
getByName: jest.fn(),
},
})
),
};
@ -73,6 +76,7 @@ describe('VisualizeESQL', () => {
ObservabilityAIAssistantMultipaneFlyoutContext={
ObservabilityAIAssistantMultipaneFlyoutContext
}
rows={[]}
/>
</ObservabilityAIAssistantMultipaneFlyoutContext.Provider>
);
@ -138,8 +142,61 @@ describe('VisualizeESQL', () => {
}),
};
renderComponent({}, lensService, undefined, ['There is an error mate']);
await waitFor(() => expect(screen.findByTestId('observabilityAiAssistantErrorsList')));
});
it('should not display the table on first render', async () => {
const lensService = {
...lensPluginMock.createStartContract(),
stateHelperApi: jest.fn().mockResolvedValue({
formula: jest.fn(),
suggestions: jest.fn(),
}),
};
renderComponent({}, lensService);
// the button to render a table should be present
await waitFor(() =>
expect(screen.getByTestId('observabilityAiAssistantErrorsList')).toBeInTheDocument()
expect(screen.findByTestId('observabilityAiAssistantLensESQLDisplayTableButton'))
);
await waitFor(() =>
expect(screen.queryByTestId('observabilityAiAssistantESQLDataGrid')).not.toBeInTheDocument()
);
});
it('should display the table when user clicks the table button', async () => {
const lensService = {
...lensPluginMock.createStartContract(),
stateHelperApi: jest.fn().mockResolvedValue({
formula: jest.fn(),
suggestions: jest.fn(),
}),
};
renderComponent({}, lensService);
await waitFor(() => {
userEvent.click(screen.getByTestId('observabilityAiAssistantLensESQLDisplayTableButton'));
expect(screen.findByTestId('observabilityAiAssistantESQLDataGrid'));
});
});
it('should render the ESQLDataGrid if Lens returns a table', async () => {
const lensService = {
...lensPluginMock.createStartContract(),
stateHelperApi: jest.fn().mockResolvedValue({
formula: jest.fn(),
suggestions: jest.fn(),
}),
};
renderComponent(
{
attributes: {
visualizationType: 'lnsDatatable',
},
},
lensService
);
await waitFor(() => {
expect(screen.findByTestId('observabilityAiAssistantESQLDataGrid'));
});
});
});

View file

@ -15,6 +15,8 @@ import {
EuiText,
EuiDescriptionList,
} from '@elastic/eui';
import type { ESQLRow } from '@kbn/es-types';
import { ESQLDataGrid } from '@kbn/esql-datagrid/public';
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
import { getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
@ -63,6 +65,7 @@ interface VisualizeQueryResponsev0 {
interface VisualizeQueryResponsev1 {
data: {
columns: DatatableColumn[];
rows: ESQLRow[];
userOverrides?: unknown;
};
content: {
@ -82,6 +85,8 @@ interface VisualizeESQLProps {
uiActions: UiActionsStart;
/** Datatable columns as returned from the ES|QL _query api, slightly processed to be kibana compliant */
columns: DatatableColumn[];
/** Datatable rows as returned from the ES|QL _query api */
rows: ESQLRow[];
/** The ES|QL query */
query: string;
/** Actions handler */
@ -106,6 +111,7 @@ export function VisualizeESQL({
dataViews,
uiActions,
columns,
rows,
query,
onActionClick,
userOverrides,
@ -120,11 +126,17 @@ export function VisualizeESQL({
}, [lens]);
const dataViewAsync = useAsync(() => {
return getESQLAdHocDataview(indexPattern, dataViews);
return getESQLAdHocDataview(indexPattern, dataViews).then((dataView) => {
if (dataView.fields.getByName('@timestamp')?.type === 'date') {
dataView.timeFieldName = '@timestamp';
}
return dataView;
});
}, [indexPattern]);
const chatFlyoutSecondSlotHandler = useContext(ObservabilityAIAssistantMultipaneFlyoutContext);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
const [isTableVisible, setIsTableVisible] = useState(false);
const [lensInput, setLensInput] = useState<TypedLensByValueInput | undefined>(
userOverrides as TypedLensByValueInput
);
@ -238,88 +250,159 @@ export function VisualizeESQL({
if (!lensHelpersAsync.value || !dataViewAsync.value || !lensInput) {
return <EuiLoadingSpinner />;
}
// if the Lens suggestions api suggests a table then we want to render a Discover table instead
const isLensInputTable = lensInput?.attributes?.visualizationType === 'lnsDatatable';
const visualizationComponentDataTestSubj = isTableVisible
? 'observabilityAiAssistantESQLDataGrid'
: 'observabilityAiAssistantESQLLensChart';
return (
<>
<EuiFlexGroup direction="column">
{Boolean(errorMessages?.length) && (
<>
<EuiText size="s">
{i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.errorMessage', {
defaultMessage: 'There were some errors in the generated query',
})}
</EuiText>
<EuiDescriptionList data-test-subj="observabilityAiAssistantErrorsList">
{errorMessages?.map((error, index) => {
return (
<EuiDescriptionListDescription key={index}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="error" color="danger" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>{error}</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionListDescription>
);
})}
</EuiDescriptionList>
</>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
<EuiToolTip
content={i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.edit', {
defaultMessage: 'Edit visualization',
})}
>
<EuiButtonIcon
size="xs"
iconType="pencil"
onClick={() => {
chatFlyoutSecondSlotHandler?.setVisibility?.(true);
if (triggerOptions) {
uiActions.getTrigger('IN_APP_EMBEDDABLE_EDIT_TRIGGER').exec(triggerOptions);
}
}}
data-test-subj="observabilityAiAssistantLensESQLEditButton"
aria-label={i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.edit', {
defaultMessage: 'Edit visualization',
{!isLensInputTable && (
<EuiFlexGroup direction="column">
{Boolean(errorMessages?.length) && (
<>
<EuiText size="s">
{i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.errorMessage', {
defaultMessage: 'There were some errors in the generated query',
})}
/>
</EuiToolTip>
<EuiFlexItem grow={false}>
</EuiText>
<EuiDescriptionList data-test-subj="observabilityAiAssistantErrorsList">
{errorMessages?.map((error, index) => {
return (
<EuiDescriptionListDescription key={index}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="error" color="danger" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>{error}</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionListDescription>
);
})}
</EuiDescriptionList>
</>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiToolTip
content={
isTableVisible
? i18n.translate(
'xpack.observabilityAiAssistant.lensESQLFunction.visualization',
{
defaultMessage: 'Visualization',
}
)
: i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.table', {
defaultMessage: 'Table of results',
})
}
>
<EuiButtonIcon
size="xs"
iconType={isTableVisible ? 'visBarVerticalStacked' : 'tableDensityExpanded'}
onClick={() => setIsTableVisible(!isTableVisible)}
data-test-subj="observabilityAiAssistantLensESQLDisplayTableButton"
aria-label={
isTableVisible
? i18n.translate(
'xpack.observabilityAiAssistant.lensESQLFunction.displayChart',
{
defaultMessage: 'Display chart',
}
)
: i18n.translate(
'xpack.observabilityAiAssistant.lensESQLFunction.displayTable',
{
defaultMessage: 'Display results',
}
)
}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiToolTip
content={i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.save', {
defaultMessage: 'Save visualization',
content={i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.edit', {
defaultMessage: 'Edit visualization',
})}
>
<EuiButtonIcon
size="xs"
iconType="save"
onClick={() => setIsSaveModalOpen(true)}
data-test-subj="observabilityAiAssistantLensESQLSaveButton"
iconType="pencil"
onClick={() => {
chatFlyoutSecondSlotHandler?.setVisibility?.(true);
if (triggerOptions) {
uiActions.getTrigger('IN_APP_EMBEDDABLE_EDIT_TRIGGER').exec(triggerOptions);
}
}}
data-test-subj="observabilityAiAssistantLensESQLEditButton"
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.lensESQLFunction.save',
'xpack.observabilityAiAssistant.lensESQLFunction.edit',
{
defaultMessage: 'Save visualization',
defaultMessage: 'Edit visualization',
}
)}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem data-test-subj="observabilityAiAssistantESQLLensChart">
<lens.EmbeddableComponent
{...lensInput}
style={{
height: 240,
}}
onLoad={onLoad}
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.save', {
defaultMessage: 'Save visualization',
})}
>
<EuiButtonIcon
size="xs"
iconType="save"
onClick={() => setIsSaveModalOpen(true)}
data-test-subj="observabilityAiAssistantLensESQLSaveButton"
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.lensESQLFunction.save',
{
defaultMessage: 'Save visualization',
}
)}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem data-test-subj={visualizationComponentDataTestSubj}>
{isTableVisible ? (
<ESQLDataGrid
rows={rows}
columns={columns}
dataView={dataViewAsync.value}
query={{ esql: query }}
flyoutType="overlay"
isTableView
/>
) : (
<lens.EmbeddableComponent
{...lensInput}
style={{
height: 240,
}}
onLoad={onLoad}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
)}
{isLensInputTable && (
<div data-test-subj="observabilityAiAssistantESQLDataGrid">
<ESQLDataGrid
rows={rows}
columns={columns}
dataView={dataViewAsync.value}
query={{ esql: query }}
flyoutType="overlay"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
)}
{isSaveModalOpen ? (
<lens.SaveModalComponent
initialInput={lensInput}
@ -351,6 +434,7 @@ export function registerVisualizeQueryRenderFunction({
const typedResponse = response as VisualizeQueryResponse;
const columns = 'data' in typedResponse ? typedResponse.data.columns : typedResponse.content;
const rows = 'data' in typedResponse ? typedResponse.data.rows : [];
const errorMessages =
'content' in typedResponse && 'errorMessages' in typedResponse.content
? typedResponse.content.errorMessages
@ -420,6 +504,7 @@ export function registerVisualizeQueryRenderFunction({
dataViews={pluginsStart.dataViews}
uiActions={pluginsStart.uiActions}
columns={columns}
rows={rows}
query={trimmedQuery}
onActionClick={onActionClick}
userOverrides={userOverrides}

View file

@ -22,10 +22,9 @@ import {
} from '@kbn/observability-ai-assistant-plugin/common/utils/concatenate_chat_completion_chunks';
import { emitWithConcatenatedMessage } from '@kbn/observability-ai-assistant-plugin/common/utils/emit_with_concatenated_message';
import { createFunctionResponseMessage } from '@kbn/observability-ai-assistant-plugin/common/utils/create_function_response_message';
import { ESQLSearchReponse } from '@kbn/es-types';
import type { FunctionRegistrationParameters } from '..';
import { correctCommonEsqlMistakes } from './correct_common_esql_mistakes';
import { validateEsqlQuery } from './validate_esql_query';
import { runAndValidateEsqlQuery } from './validate_esql_query';
const readFile = promisify(Fs.readFile);
const readdir = promisify(Fs.readdir);
@ -108,7 +107,7 @@ export function registerQueryFunction({ functions, resources }: FunctionRegistra
},
async ({ arguments: { query } }) => {
const client = (await resources.context.core).elasticsearch.client.asCurrentUser;
const { error, errorMessages } = await validateEsqlQuery({
const { error, errorMessages, rows, columns } = await runAndValidateEsqlQuery({
query,
client,
});
@ -122,16 +121,12 @@ export function registerQueryFunction({ functions, resources }: FunctionRegistra
},
};
}
const response = (await client.transport.request({
method: 'POST',
path: '_query',
body: {
query,
},
})) as ESQLSearchReponse;
return {
content: response,
content: {
columns,
rows,
},
};
}
);

View file

@ -8,11 +8,11 @@
import { validateQuery } from '@kbn/esql-validation-autocomplete';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import type { ElasticsearchClient } from '@kbn/core/server';
import { ESQLSearchReponse } from '@kbn/es-types';
import { ESQLSearchReponse, ESQLRow } from '@kbn/es-types';
import { esFieldTypeToKibanaFieldType, type KBN_FIELD_TYPES } from '@kbn/field-types';
import { splitIntoCommands } from './correct_common_esql_mistakes';
export async function validateEsqlQuery({
export async function runAndValidateEsqlQuery({
query,
client,
}: {
@ -26,6 +26,7 @@ export async function validateEsqlQuery({
type: KBN_FIELD_TYPES;
};
}>;
rows?: ESQLRow[];
error?: Error;
errorMessages?: string[];
}> {
@ -47,15 +48,12 @@ export async function validateEsqlQuery({
return 'text' in error ? error.text : error.message;
});
// With limit 0 I get only the columns, it is much more performant
const performantQuery = `${query} | limit 0`;
return client.transport
.request({
method: 'POST',
path: '_query',
body: {
query: performantQuery,
query,
},
})
.then((res) => {
@ -68,7 +66,7 @@ export async function validateEsqlQuery({
meta: { type: esFieldTypeToKibanaFieldType(type) },
})) ?? [];
return { columns };
return { columns, rows: esqlResponse.values };
})
.catch((error) => {
return {

View file

@ -7,7 +7,7 @@
import { VisualizeESQLUserIntention } from '@kbn/observability-ai-assistant-plugin/common/functions/visualize_esql';
import { visualizeESQLFunction } from '../../common/functions/visualize_esql';
import { FunctionRegistrationParameters } from '.';
import { validateEsqlQuery } from './query/validate_esql_query';
import { runAndValidateEsqlQuery } from './query/validate_esql_query';
const getMessageForLLM = (
intention: VisualizeESQLUserIntention,
@ -28,7 +28,7 @@ export function registerVisualizeESQLFunction({
resources,
}: FunctionRegistrationParameters) {
functions.registerFunction(visualizeESQLFunction, async ({ arguments: { query, intention } }) => {
const { columns, errorMessages } = await validateEsqlQuery({
const { columns, errorMessages, rows } = await runAndValidateEsqlQuery({
query,
client: (await resources.context.core).elasticsearch.client.asCurrentUser,
});
@ -38,6 +38,7 @@ export function registerVisualizeESQLFunction({
return {
data: {
columns,
rows,
},
content: {
message,

View file

@ -69,6 +69,7 @@
"@kbn/task-manager-plugin",
"@kbn/cloud-plugin",
"@kbn/observability-plugin",
"@kbn/esql-datagrid",
"@kbn/alerting-comparators"
],
"exclude": ["target/**/*"]

View file

@ -4800,6 +4800,10 @@
version "0.0.0"
uid ""
"@kbn/esql-datagrid@link:src/plugins/esql_datagrid":
version "0.0.0"
uid ""
"@kbn/esql-utils@link:packages/kbn-esql-utils":
version "0.0.0"
uid ""