[8.x] [ES|QL] Dashboard variables follow ups (#208338) (#208521)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL] Dashboard variables follow ups
(#208338)](https://github.com/elastic/kibana/pull/208338)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Stratoula
Kalafateli","email":"efstratia.kalafateli@elastic.co"},"sourceCommit":{"committedDate":"2025-01-28T12:39:52Z","message":"[ES|QL]
Dashboard variables follow ups
(#208338)","sha":"fa7c477ff19855d32f5d8dd348a6b0ce50381c39","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Feature:ES|QL","Team:ESQL","backport:version","v8.18.0"],"title":"[ES|QL]
Dashboard variables follow
ups","number":208338,"url":"https://github.com/elastic/kibana/pull/208338","mergeCommit":{"message":"[ES|QL]
Dashboard variables follow ups
(#208338)","sha":"fa7c477ff19855d32f5d8dd348a6b0ce50381c39"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/208338","number":208338,"mergeCommit":{"message":"[ES|QL]
Dashboard variables follow ups
(#208338)","sha":"fa7c477ff19855d32f5d8dd348a6b0ce50381c39"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Stratoula Kalafateli 2025-01-28 16:05:56 +01:00 committed by GitHub
parent 64fc272dad
commit 580bb5a41d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 98 additions and 83 deletions

View file

@ -38,9 +38,12 @@ import memoize from 'lodash/memoize';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { css } from '@emotion/react';
import { ESQLRealField, ESQLControlVariable } from '@kbn/esql-validation-autocomplete';
import {
type ESQLRealField,
ESQLVariableType,
type ESQLControlVariable,
} from '@kbn/esql-validation-autocomplete';
import { FieldType } from '@kbn/esql-validation-autocomplete/src/definitions/types';
import { ESQLVariableType } from '@kbn/esql-validation-autocomplete';
import { EditorFooter } from './editor_footer';
import { fetchFieldsFromESQL } from './fetch_fields_from_esql';
import {

View file

@ -18,6 +18,7 @@ export {
getESQLWithSafeLimit,
appendToESQLQuery,
appendWhereClauseToESQLQuery,
appendStatsByToQuery,
getESQLQueryColumns,
getESQLQueryColumnsRaw,
getESQLResults,
@ -36,6 +37,7 @@ export {
TextBasedLanguages,
queryCannotBeSampled,
mapVariableToColumn,
getValuesFromQueryField,
} from './src';
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';

View file

@ -22,9 +22,14 @@ export {
retrieveMetadataColumns,
getQueryColumnsFromESQLQuery,
mapVariableToColumn,
getValuesFromQueryField,
} from './utils/query_parsing_helpers';
export { queryCannotBeSampled } from './utils/query_cannot_be_sampled';
export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query';
export {
appendToESQLQuery,
appendWhereClauseToESQLQuery,
appendStatsByToQuery,
} from './utils/append_to_query';
export {
getESQLQueryColumns,
getESQLQueryColumnsRaw,

View file

@ -7,7 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { appendToESQLQuery, appendWhereClauseToESQLQuery } from './append_to_query';
import {
appendToESQLQuery,
appendWhereClauseToESQLQuery,
appendStatsByToQuery,
} from './append_to_query';
describe('appendToQuery', () => {
describe('appendToESQLQuery', () => {
@ -175,4 +179,20 @@ and \`ip\`::string!="127.0.0.2/32"`
).toBeUndefined();
});
});
describe('appendStatsByToQuery', () => {
it('should append the stats by clause to the query', () => {
const queryString = 'FROM my_index';
const statsBy = 'my_field';
const updatedQueryString = appendStatsByToQuery(queryString, statsBy);
expect(updatedQueryString).toBe('FROM my_index\n| STATS BY my_field');
});
it('should append the stats by clause to the query with existing clauses', () => {
const queryString = 'FROM my_index | LIMIT 10 | STATS BY meow';
const statsBy = 'my_field';
const updatedQueryString = appendStatsByToQuery(queryString, statsBy);
expect(updatedQueryString).toBe('FROM my_index | LIMIT 10\n| STATS BY my_field');
});
});
});

View file

@ -8,6 +8,7 @@
*/
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse, mutate, BasicPrettyPrinter } from '@kbn/esql-ast';
// Append in a new line the appended text to take care of the case where the user adds a comment at the end of the query
// in these cases a base query such as "from index // comment" will result in errors or wrong data if we don't append in a new line
@ -98,3 +99,16 @@ export function appendWhereClauseToESQLQuery(
const whereClause = `| WHERE ${fieldName}${operator}${filterValue}`;
return appendToESQLQuery(baseESQLQuery, whereClause);
}
export const appendStatsByToQuery = (queryString: string, column: string) => {
const { root } = parse(queryString);
const lastCommand = root.commands[root.commands.length - 1];
if (lastCommand.name === 'stats') {
const statsCommand = lastCommand;
mutate.generic.commands.remove(root, statsCommand);
const queryWithoutStats = BasicPrettyPrinter.print(root);
return `${queryWithoutStats}\n| STATS BY ${column}`;
} else {
return `${queryString}\n| STATS BY ${column}`;
}
};

View file

@ -19,6 +19,7 @@ import {
retrieveMetadataColumns,
getQueryColumnsFromESQLQuery,
mapVariableToColumn,
getValuesFromQueryField,
} from './query_parsing_helpers';
describe('esql query helpers', () => {
@ -539,4 +540,18 @@ describe('esql query helpers', () => {
expect(mapVariableToColumn(esql, variables, columns)).toStrictEqual(expectedColumns);
});
});
describe('getValuesFromQueryField', () => {
it('should return the values from the query field', () => {
const queryString = 'FROM my_index | WHERE my_field ==';
const values = getValuesFromQueryField(queryString);
expect(values).toEqual('my_field');
});
it('should return the values from the query field with new lines', () => {
const queryString = 'FROM my_index \n| WHERE my_field >=';
const values = getValuesFromQueryField(queryString);
expect(values).toEqual('my_field');
});
});
});

View file

@ -180,3 +180,20 @@ export const mapVariableToColumn = (
});
return columns;
};
export const getValuesFromQueryField = (queryString: string) => {
const validQuery = `${queryString} ""`;
const { root } = parse(validQuery);
const lastCommand = root.commands[root.commands.length - 1];
const columns: ESQLColumn[] = [];
walk(lastCommand, {
visitColumn: (node) => columns.push(node),
});
const column = Walker.match(lastCommand, { type: 'column' });
if (column) {
return `${column.name}`;
}
};

View file

@ -9,7 +9,6 @@
export type { SuggestionRawDefinition, ItemKind } from './src/autocomplete/types';
export { ESQLVariableType, type ESQLControlVariable } from './src/shared/types';
export { inKnownTimeInterval } from './src/shared/helpers';
export type { CodeAction } from './src/code_actions/types';
export type {
FunctionDefinition,
@ -68,6 +67,7 @@ export {
isSingleItem,
} from './src/shared/helpers';
export { ENRICH_MODES } from './src/definitions/settings';
export { timeUnits } from './src/definitions/literals';
export { getFunctionSignatures } from './src/definitions/helpers';
export {

View file

@ -26,10 +26,9 @@ import { buildFunctionDocumentation } from './documentation_util';
import { DOUBLE_BACKTICK, SINGLE_TICK_REGEX } from '../shared/constants';
import { ESQLRealField } from '../validation/types';
import { isNumericType } from '../shared/esql_types';
import type { ESQLControlVariable } from '../shared/types';
import { getTestFunctions } from '../shared/test_functions';
import { builtinFunctions } from '../definitions/builtin';
import { ESQLVariableType } from '../shared/types';
import { ESQLVariableType, ESQLControlVariable } from '../shared/types';
const techPreviewLabel = i18n.translate(
'kbn-esql-validation-autocomplete.esql.autocomplete.techPreviewLabel',

View file

@ -6,8 +6,7 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ESQLControlVariable } from '@kbn/esql-validation-autocomplete';
import type { ESQLControlVariable } from '@kbn/esql-validation-autocomplete';
import { PublishingSubject } from '@kbn/presentation-publishing';
/**

View file

@ -14,7 +14,7 @@
"target/**/*",
],
"kbn_references": [
"@kbn/esql-validation-autocomplete",
"@kbn/presentation-publishing",
"@kbn/esql-validation-autocomplete",
]
}

View file

@ -12,8 +12,6 @@ import {
getQueryForFields,
areValuesIntervalsValid,
getRecurrentVariableName,
getValuesFromQueryField,
appendStatsByToQuery,
validateVariableName,
} from './helpers';
@ -90,36 +88,6 @@ describe('helpers', () => {
});
});
describe('getValuesFromQueryField', () => {
it('should return the values from the query field', () => {
const queryString = 'FROM my_index | WHERE my_field ==';
const values = getValuesFromQueryField(queryString);
expect(values).toEqual('my_field');
});
it('should return the values from the query field with new lines', () => {
const queryString = 'FROM my_index \n| WHERE my_field >=';
const values = getValuesFromQueryField(queryString);
expect(values).toEqual('my_field');
});
});
describe('appendStatsByToQuery', () => {
it('should append the stats by clause to the query', () => {
const queryString = 'FROM my_index';
const statsBy = 'my_field';
const updatedQueryString = appendStatsByToQuery(queryString, statsBy);
expect(updatedQueryString).toBe('FROM my_index\n| STATS BY my_field');
});
it('should append the stats by clause to the query with existing clauses', () => {
const queryString = 'FROM my_index | LIMIT 10 | STATS BY meow';
const statsBy = 'my_field';
const updatedQueryString = appendStatsByToQuery(queryString, statsBy);
expect(updatedQueryString).toBe('FROM my_index | LIMIT 10\n| STATS BY my_field');
});
});
describe('validateVariableName', () => {
it('should return the variable without special characters', () => {
const variable = validateVariableName('my_variable/123');

View file

@ -7,8 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { monaco } from '@kbn/monaco';
import { inKnownTimeInterval } from '@kbn/esql-validation-autocomplete';
import { type ESQLColumn, parse, walk, mutate, BasicPrettyPrinter, Walker } from '@kbn/esql-ast';
import { timeUnits } from '@kbn/esql-validation-autocomplete';
function inKnownTimeInterval(timeIntervalUnit: string): boolean {
return timeUnits.some((unit) => unit === timeIntervalUnit.toLowerCase());
}
export const updateQueryStringWithVariable = (
queryString: string,
@ -72,23 +75,6 @@ export const getRecurrentVariableName = (name: string, existingNames: string[])
return newName;
};
export const getValuesFromQueryField = (queryString: string) => {
const validQuery = `${queryString} ""`;
const { root } = parse(validQuery);
const lastCommand = root.commands[root.commands.length - 1];
const columns: ESQLColumn[] = [];
walk(lastCommand, {
visitColumn: (node) => columns.push(node),
});
const column = Walker.match(lastCommand, { type: 'column' });
if (column) {
return `${column.name}`;
}
};
export const getFlyoutStyling = () => {
return `
.euiFlyoutBody__overflow {
@ -103,19 +89,6 @@ export const getFlyoutStyling = () => {
`;
};
export const appendStatsByToQuery = (queryString: string, column: string) => {
const { root } = parse(queryString);
const lastCommand = root.commands[root.commands.length - 1];
if (lastCommand.name === 'stats') {
const statsCommand = lastCommand;
mutate.generic.commands.remove(root, statsCommand);
const queryWithoutStats = BasicPrettyPrinter.print(root);
return `${queryWithoutStats}\n| STATS BY ${column}`;
} else {
return `${queryString}\n| STATS BY ${column}`;
}
};
export const validateVariableName = (variableName: string) => {
let text = variableName
// variable name can only contain letters, numbers and underscores

View file

@ -37,6 +37,7 @@ jest.mock('@kbn/esql-utils', () => {
getIndexPatternFromESQLQuery: jest.fn().mockReturnValue('index1'),
getLimitFromESQLQuery: jest.fn().mockReturnValue(1000),
isQueryWrappedByPipes: jest.fn().mockReturnValue(false),
getValuesFromQueryField: jest.fn().mockReturnValue('field'),
};
});
@ -249,9 +250,6 @@ describe('ValueControlForm', () => {
`Values from a query`
);
// code editor should be rendered
expect(await findByTestId('ESQLEditor')).toBeInTheDocument();
// values preview panel should be rendered
expect(await findByTestId('esqlValuesPreview')).toBeInTheDocument();
});

View file

@ -21,9 +21,14 @@ import {
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ISearchGeneric } from '@kbn/search-types';
import ESQLEditor from '@kbn/esql-editor';
import { ESQLVariableType, ESQLControlVariable } from '@kbn/esql-validation-autocomplete';
import { getIndexPatternFromESQLQuery, getESQLResults } from '@kbn/esql-utils';
import {
getIndexPatternFromESQLQuery,
getESQLResults,
appendStatsByToQuery,
getValuesFromQueryField,
} from '@kbn/esql-utils';
import { ESQLLangEditor } from '../../../create_editor';
import type { ESQLControlState, ControlWidthOptions } from '../types';
import {
Header,
@ -35,9 +40,7 @@ import {
} from './shared_form_components';
import {
getRecurrentVariableName,
getValuesFromQueryField,
getFlyoutStyling,
appendStatsByToQuery,
areValuesIntervalsValid,
validateVariableName,
} from './helpers';
@ -366,7 +369,7 @@ export function ValueControlForm({
})}
fullWidth
>
<ESQLEditor
<ESQLLangEditor
query={{ esql: valuesQuery }}
onTextLangQueryChange={(q) => {
setValuesQuery(q.esql);

View file

@ -28,7 +28,6 @@
"@kbn/kibana-utils-plugin",
"@kbn/esql-validation-autocomplete",
"@kbn/monaco",
"@kbn/esql-ast",
"@kbn/search-types",
"@kbn/react-kibana-context-render",
"@kbn/react-kibana-mount",