mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
e4bd1b22e6
commit
5593a5692b
4 changed files with 80 additions and 55 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue