[ES|QL] Adds the limit information to the editor footer (#190498)

## Summary

Closes https://github.com/elastic/kibana/issues/188991

Adds indicator of the applied limit.

<img width="678" alt="image"
src="https://github.com/user-attachments/assets/4f909b3e-d8c2-4a51-afd3-084aa7f6e7e9">


### Checklist

- [ ] [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
This commit is contained in:
Stratoula Kalafateli 2024-08-16 22:47:56 +02:00 committed by GitHub
parent e4bd1b22e6
commit 5593a5692b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 80 additions and 55 deletions

View file

@ -77,17 +77,17 @@ describe('esql query helpers', () => {
describe('getLimitFromESQLQuery', () => {
it('should return default limit when ES|QL query is empty', () => {
const limit = getLimitFromESQLQuery('');
expect(limit).toBe(500);
expect(limit).toBe(1000);
});
it('should return default limit when ES|QL query does not contain LIMIT command', () => {
const limit = getLimitFromESQLQuery('FROM foo');
expect(limit).toBe(500);
expect(limit).toBe(1000);
});
it('should return default limit when ES|QL query contains invalid LIMIT command', () => {
const limit = getLimitFromESQLQuery('FROM foo | LIMIT iAmNotANumber');
expect(limit).toBe(500);
expect(limit).toBe(1000);
});
it('should return limit when ES|QL query contains LIMIT command', () => {
@ -95,7 +95,7 @@ describe('esql query helpers', () => {
expect(limit).toBe(10000);
});
it('should return last limit when ES|QL query contains multiple LIMIT command', () => {
it('should return minimum limit when ES|QL query contains multiple LIMIT command', () => {
const limit = getLimitFromESQLQuery('FROM foo | LIMIT 200 | LIMIT 0');
expect(limit).toBe(0);
});

View file

@ -8,7 +8,7 @@
import type { ESQLSource, ESQLFunction, ESQLColumn, ESQLSingleAstItem } from '@kbn/esql-ast';
import { getAstAndSyntaxErrors, Walker, walk } from '@kbn/esql-ast';
const DEFAULT_ESQL_LIMIT = 500;
const DEFAULT_ESQL_LIMIT = 1000;
// retrieves the index pattern from the aggregate query for ES|QL using ast parsing
export function getIndexPatternFromESQLQuery(esql?: string) {
@ -40,14 +40,27 @@ export function hasTransformationalCommand(esql?: string) {
}
export function getLimitFromESQLQuery(esql: string): number {
const limitCommands = esql.match(new RegExp(/LIMIT\s[0-9]+/, 'ig'));
if (!limitCommands) {
const { ast } = getAstAndSyntaxErrors(esql);
const limitCommands = ast.filter(({ name }) => name === 'limit');
if (!limitCommands || !limitCommands.length) {
return DEFAULT_ESQL_LIMIT;
}
const limits: number[] = [];
walk(ast, {
visitLiteral: (node) => {
if (!isNaN(Number(node.value))) {
limits.push(Number(node.value));
}
},
});
if (!limits.length) {
return DEFAULT_ESQL_LIMIT;
}
const lastIndex = limitCommands.length - 1;
const split = limitCommands[lastIndex].split(' ');
return parseInt(split[1], 10);
// ES returns always the smallest limit
return Math.min(...limits);
}
export function removeDropCommandsFromESQLQuery(esql?: string): string {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { memo, useState, useCallback, useEffect } from 'react';
import React, { memo, useState, useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCode } from '@elastic/eui';
@ -16,6 +16,7 @@ import {
LanguageDocumentationPopover,
type LanguageDocumentationSections,
} from '@kbn/language-documentation-popover';
import { getLimitFromESQLQuery } from '@kbn/esql-utils';
import { type MonacoMessage, getDocumentationSections } from '../helpers';
import { ErrorsWarningsFooterPopover } from './errors_warnings_popover';
import { QueryHistoryAction, QueryHistory } from './query_history';
@ -98,6 +99,8 @@ export const EditorFooter = memo(function EditorFooter({
[runQuery, updateQuery]
);
const limit = useMemo(() => getLimitFromESQLQuery(code), [code]);
useEffect(() => {
async function getDocumentation() {
const sections = await getDocumentationSections('esql');
@ -126,9 +129,16 @@ export const EditorFooter = memo(function EditorFooter({
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
<EuiFlexGroup
gutterSize="none"
responsive={false}
alignItems="center"
css={css`
gap: 12px;
`}
>
<QueryWrapComponent code={code} updateQuery={updateQuery} />
<EuiFlexItem grow={false} style={{ marginRight: '8px' }}>
<EuiFlexItem grow={false}>
<EuiText
size="xs"
color="subdued"
@ -144,7 +154,7 @@ export const EditorFooter = memo(function EditorFooter({
</EuiFlexItem>
{/* If there is no space and no @timestamp detected hide the information */}
{(detectedTimestamp || !isSpaceReduced) && !hideTimeFilterInfo && (
<EuiFlexItem grow={false} style={{ marginRight: '16px' }}>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<EuiText
@ -175,6 +185,35 @@ export const EditorFooter = memo(function EditorFooter({
</EuiFlexGroup>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<EuiText
size="xs"
color="subdued"
data-test-subj="TextBasedLangEditor-limit-info"
>
<p>
{isSpaceReduced
? i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.limitInfoReduced',
{
defaultMessage: 'LIMIT {limit}',
values: { limit },
}
)
: i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.limitInfo',
{
defaultMessage: 'LIMIT {limit} rows',
values: { limit },
}
)}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{errors && errors.length > 0 && (
<ErrorsWarningsFooterPopover
isPopoverOpen={isErrorPopoverOpen}

View file

@ -71,22 +71,14 @@ describe('TextBasedLanguagesEditor', () => {
});
it('should render the date info with no @timestamp found', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(
component.find('[data-test-subj="TextBasedLangEditor-date-info"]').at(0).text()
).toStrictEqual('@timestamp not found');
});
it('should render the feedback link', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(component.find('[data-test-subj="TextBasedLangEditor-feedback-link"]').length).not.toBe(
0
);
@ -95,7 +87,6 @@ describe('TextBasedLanguagesEditor', () => {
it('should not render the date info if hideTimeFilterInfo is set to true', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
hideTimeFilterInfo: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
@ -105,7 +96,6 @@ describe('TextBasedLanguagesEditor', () => {
it('should render the date info with @timestamp found if detectedTimestamp is given', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
detectedTimestamp: '@timestamp',
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
@ -114,10 +104,16 @@ describe('TextBasedLanguagesEditor', () => {
).toStrictEqual('@timestamp found');
});
it('should render the limit information', async () => {
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(
component.find('[data-test-subj="TextBasedLangEditor-limit-info"]').at(0).text()
).toStrictEqual('LIMIT 1000 rows');
});
it('should render the query history action if isLoading is defined', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
isLoading: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
@ -128,11 +124,7 @@ describe('TextBasedLanguagesEditor', () => {
});
it('should not render the query history action if isLoading is undefined', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(
component.find('[data-test-subj="TextBasedLangEditor-toggle-query-history-button-container"]')
.length
@ -142,7 +134,6 @@ describe('TextBasedLanguagesEditor', () => {
it('should not render the query history action if hideQueryHistory is set to true', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
hideQueryHistory: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
@ -153,13 +144,9 @@ describe('TextBasedLanguagesEditor', () => {
});
it('should render the correct buttons for the expanded code editor mode', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
let component: ReactWrapper;
await act(async () => {
component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
});
component!.update();
expect(
@ -171,20 +158,12 @@ describe('TextBasedLanguagesEditor', () => {
});
it('should render the resize for the expanded code editor mode', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(component.find('[data-test-subj="TextBasedLangEditor-resize"]').length).not.toBe(0);
});
it('should render the footer for the expanded code editor mode', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(component.find('[data-test-subj="TextBasedLangEditor-footer"]').length).not.toBe(0);
expect(component.find('[data-test-subj="TextBasedLangEditor-footer-lines"]').at(0).text()).toBe(
'1 line'
@ -192,18 +171,13 @@ describe('TextBasedLanguagesEditor', () => {
});
it('should render the run query text', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(component.find('[data-test-subj="TextBasedLangEditor-run-query"]').length).not.toBe(0);
});
it('should not render the run query text if the hideRunQueryText prop is set to true', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
hideRunQueryText: true,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
@ -214,7 +188,6 @@ describe('TextBasedLanguagesEditor', () => {
const onTextLangQuerySubmit = jest.fn();
const newProps = {
...props,
isCodeEditorExpanded: true,
hideRunQueryText: true,
editorIsInline: true,
onTextLangQuerySubmit,