mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
696190db60
commit
1bf4fcd6d4
19 changed files with 486 additions and 46 deletions
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export {
|
|||
getESQLQueryColumns,
|
||||
getESQLQueryColumnsRaw,
|
||||
getESQLResults,
|
||||
formatESQLColumns,
|
||||
getTimeFieldFromESQLQuery,
|
||||
getStartEndParams,
|
||||
hasStartEndParams,
|
||||
|
|
|
@ -22,6 +22,7 @@ export {
|
|||
getESQLQueryColumns,
|
||||
getESQLQueryColumnsRaw,
|
||||
getESQLResults,
|
||||
formatESQLColumns,
|
||||
getStartEndParams,
|
||||
hasStartEndParams,
|
||||
} from './utils/run_query';
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"requiredPlugins": [
|
||||
"data",
|
||||
"uiActions",
|
||||
"fieldFormats"
|
||||
"fieldFormats",
|
||||
"share",
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
"@kbn/unified-doc-viewer-plugin",
|
||||
"@kbn/core-notifications-browser",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/i18n",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"embeddable",
|
||||
"fieldFormats",
|
||||
"charts",
|
||||
"esqlDataGrid",
|
||||
"esql",
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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'),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -66,6 +66,7 @@ const openInlineLensConfigEditor = (
|
|||
'data-test-subj': 'customizeLens',
|
||||
className: 'lnsConfigPanel__overlay',
|
||||
hideCloseButton: true,
|
||||
isResizable: true,
|
||||
onClose: (overlayRef) => {
|
||||
overlayTracker?.clearOverlays();
|
||||
overlayRef.close();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -111,6 +111,8 @@
|
|||
"@kbn/licensing-plugin",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/es-types",
|
||||
"@kbn/esql-datagrid",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue