mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[ES|QL] Distinguish the functions/fields vs the values on the query level (#213916)
## Summary Closes https://github.com/elastic/kibana/issues/209731 This PR is based on the change made here https://github.com/elastic/elasticsearch/pull/122459 The main difference is that: - Functions and fields should now be added as ?? (instead of ?) - The payload to ES is the same regardless if you send a value or a field/function In order to accommodate this the following changes were made: - Now the variable name in the control form displays the ? or ?? (it didnt display them before) <img width="428" alt="image" src="https://github.com/user-attachments/assets/1381ba4a-591c-47f2-af93-30d54fe7a639" /> - The previous created charts with the old format are bwc (this means that they should load correctly when you checkout in this PR (a helper function has been created to ensure it)  ### Release notes Now the fields / functions variables are being described with ?? in the query. The values variables use ? as before. ### 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 - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
51932b6065
commit
b477afb783
28 changed files with 451 additions and 217 deletions
|
@ -29,6 +29,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import { ESQLLang, ESQL_LANG_ID, monaco, type ESQLCallbacks } from '@kbn/monaco';
|
||||
import memoize from 'lodash/memoize';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { fixESQLQueryWithVariables } from '@kbn/esql-utils';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { css } from '@emotion/react';
|
||||
import { ESQLVariableType, type ESQLControlVariable } from '@kbn/esql-types';
|
||||
|
@ -123,9 +124,14 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
data,
|
||||
} = kibana.services;
|
||||
|
||||
const fixedQuery = useMemo(
|
||||
() => fixESQLQueryWithVariables(query.esql, esqlVariables),
|
||||
[esqlVariables, query.esql]
|
||||
);
|
||||
|
||||
const variablesService = kibana.services?.esql?.variablesService;
|
||||
const histogramBarTarget = uiSettings?.get('histogram:barTarget') ?? 50;
|
||||
const [code, setCode] = useState<string>(query.esql ?? '');
|
||||
const [code, setCode] = useState<string>(fixedQuery ?? '');
|
||||
// To make server side errors less "sticky", register the state of the code when submitting
|
||||
const [codeWhenSubmitted, setCodeStateOnSubmission] = useState(code);
|
||||
const [editorHeight, setEditorHeight] = useState(
|
||||
|
@ -212,11 +218,11 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
|
||||
useEffect(() => {
|
||||
if (editor1.current) {
|
||||
if (code !== query.esql) {
|
||||
setCode(query.esql);
|
||||
if (code !== fixedQuery) {
|
||||
setCode(fixedQuery);
|
||||
}
|
||||
}
|
||||
}, [code, query.esql]);
|
||||
}, [code, fixedQuery]);
|
||||
|
||||
// Enable the variables service if the feature is supported in the consumer app
|
||||
useEffect(() => {
|
||||
|
@ -285,7 +291,7 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
monaco.editor.registerCommand('esql.control.time_literal.create', async (...args) => {
|
||||
const position = editor1.current?.getPosition();
|
||||
await triggerControl(
|
||||
query.esql,
|
||||
fixedQuery,
|
||||
ESQLVariableType.TIME_LITERAL,
|
||||
position,
|
||||
uiActions,
|
||||
|
@ -298,7 +304,7 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
monaco.editor.registerCommand('esql.control.fields.create', async (...args) => {
|
||||
const position = editor1.current?.getPosition();
|
||||
await triggerControl(
|
||||
query.esql,
|
||||
fixedQuery,
|
||||
ESQLVariableType.FIELDS,
|
||||
position,
|
||||
uiActions,
|
||||
|
@ -311,7 +317,7 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
monaco.editor.registerCommand('esql.control.values.create', async (...args) => {
|
||||
const position = editor1.current?.getPosition();
|
||||
await triggerControl(
|
||||
query.esql,
|
||||
fixedQuery,
|
||||
ESQLVariableType.VALUES,
|
||||
position,
|
||||
uiActions,
|
||||
|
@ -324,7 +330,7 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
monaco.editor.registerCommand('esql.control.functions.create', async (...args) => {
|
||||
const position = editor1.current?.getPosition();
|
||||
await triggerControl(
|
||||
query.esql,
|
||||
fixedQuery,
|
||||
ESQLVariableType.FUNCTIONS,
|
||||
position,
|
||||
uiActions,
|
||||
|
@ -440,7 +446,7 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
const esqlCallbacks: ESQLCallbacks = useMemo(() => {
|
||||
const callbacks: ESQLCallbacks = {
|
||||
getSources: async () => {
|
||||
clearCacheWhenOld(dataSourcesCache, query.esql);
|
||||
clearCacheWhenOld(dataSourcesCache, fixedQuery);
|
||||
const sources = await memoizedSources(dataViews, core).result;
|
||||
return sources;
|
||||
},
|
||||
|
@ -505,7 +511,7 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
fieldsMetadata,
|
||||
kibana.services?.esql?.getJoinIndicesAutocomplete,
|
||||
dataSourcesCache,
|
||||
query.esql,
|
||||
fixedQuery,
|
||||
memoizedSources,
|
||||
dataViews,
|
||||
core,
|
||||
|
|
|
@ -39,6 +39,7 @@ export {
|
|||
mapVariableToColumn,
|
||||
getValuesFromQueryField,
|
||||
getESQLQueryVariables,
|
||||
fixESQLQueryWithVariables,
|
||||
} from './src';
|
||||
|
||||
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';
|
||||
|
|
|
@ -23,6 +23,7 @@ export {
|
|||
mapVariableToColumn,
|
||||
getValuesFromQueryField,
|
||||
getESQLQueryVariables,
|
||||
fixESQLQueryWithVariables,
|
||||
} from './utils/query_parsing_helpers';
|
||||
export { queryCannotBeSampled } from './utils/query_cannot_be_sampled';
|
||||
export {
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
getQueryColumnsFromESQLQuery,
|
||||
mapVariableToColumn,
|
||||
getValuesFromQueryField,
|
||||
fixESQLQueryWithVariables,
|
||||
} from './query_parsing_helpers';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
|
||||
|
@ -581,4 +582,104 @@ describe('esql query helpers', () => {
|
|||
expect(values).toEqual('my_field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fixESQLQueryWithVariables', () => {
|
||||
it('should return the query as is if no variables are given', () => {
|
||||
const esql = 'FROM my_index | STATS COUNT(?field)';
|
||||
const variables: ESQLControlVariable[] = [];
|
||||
expect(fixESQLQueryWithVariables(esql, variables)).toEqual(esql);
|
||||
});
|
||||
|
||||
it('should return the query as is if no fields or functions variables are given', () => {
|
||||
const esql = 'FROM my_index | WHERE field == ?value';
|
||||
const variables: ESQLControlVariable[] = [
|
||||
{
|
||||
key: 'interval',
|
||||
value: '5 minutes',
|
||||
type: ESQLVariableType.TIME_LITERAL,
|
||||
},
|
||||
{
|
||||
key: 'agent_name',
|
||||
value: 'go',
|
||||
type: ESQLVariableType.VALUES,
|
||||
},
|
||||
];
|
||||
expect(fixESQLQueryWithVariables(esql, variables)).toEqual(esql);
|
||||
});
|
||||
|
||||
it('should return the query as is if fields or functions variables are given but they are already used with ??', () => {
|
||||
const esql = 'FROM my_index | STATS COUNT(??field)';
|
||||
const variables: ESQLControlVariable[] = [
|
||||
{
|
||||
key: 'interval',
|
||||
value: '5 minutes',
|
||||
type: ESQLVariableType.TIME_LITERAL,
|
||||
},
|
||||
{
|
||||
key: 'agent_name',
|
||||
value: 'go',
|
||||
type: ESQLVariableType.VALUES,
|
||||
},
|
||||
{
|
||||
key: 'field',
|
||||
value: 'bytes',
|
||||
type: ESQLVariableType.FIELDS,
|
||||
},
|
||||
];
|
||||
expect(fixESQLQueryWithVariables(esql, variables)).toEqual(esql);
|
||||
});
|
||||
|
||||
it('should fix the query if fields or functions variables are given and they are already used with ?', () => {
|
||||
const esql = 'FROM my_index | STATS COUNT(?field)';
|
||||
const expected = 'FROM my_index | STATS COUNT(??field)';
|
||||
const variables: ESQLControlVariable[] = [
|
||||
{
|
||||
key: 'interval',
|
||||
value: '5 minutes',
|
||||
type: ESQLVariableType.TIME_LITERAL,
|
||||
},
|
||||
{
|
||||
key: 'agent_name',
|
||||
value: 'go',
|
||||
type: ESQLVariableType.VALUES,
|
||||
},
|
||||
{
|
||||
key: 'field',
|
||||
value: 'bytes',
|
||||
type: ESQLVariableType.FIELDS,
|
||||
},
|
||||
];
|
||||
expect(fixESQLQueryWithVariables(esql, variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should fix a query with multiple variables', () => {
|
||||
const esql =
|
||||
'FROM my_index | STATS COUNT(?field) by ?breakdownField | WHERE agent.name == ?agent_name';
|
||||
const expected =
|
||||
'FROM my_index | STATS COUNT(??field) by ??breakdownField | WHERE agent.name == ?agent_name';
|
||||
const variables: ESQLControlVariable[] = [
|
||||
{
|
||||
key: 'interval',
|
||||
value: '5 minutes',
|
||||
type: ESQLVariableType.TIME_LITERAL,
|
||||
},
|
||||
{
|
||||
key: 'agent_name',
|
||||
value: 'go',
|
||||
type: ESQLVariableType.VALUES,
|
||||
},
|
||||
{
|
||||
key: 'field',
|
||||
value: 'bytes',
|
||||
type: ESQLVariableType.FIELDS,
|
||||
},
|
||||
{
|
||||
key: 'breakdownField',
|
||||
value: 'clientip',
|
||||
type: ESQLVariableType.FIELDS,
|
||||
},
|
||||
];
|
||||
expect(fixESQLQueryWithVariables(esql, variables)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
|||
ESQLInlineCast,
|
||||
ESQLCommandOption,
|
||||
} from '@kbn/esql-ast';
|
||||
import type { ESQLControlVariable } from '@kbn/esql-types';
|
||||
import { type ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types';
|
||||
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
|
||||
|
@ -161,7 +161,7 @@ export const getQueryColumnsFromESQLQuery = (esql: string): string[] => {
|
|||
export const getESQLQueryVariables = (esql: string): string[] => {
|
||||
const { root } = parse(esql);
|
||||
const usedVariablesInQuery = Walker.params(root);
|
||||
return usedVariablesInQuery.map((v) => v.text.replace('?', ''));
|
||||
return usedVariablesInQuery.map((v) => v.text.replace(/^\?+/, ''));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -228,3 +228,36 @@ export const getValuesFromQueryField = (queryString: string, cursorPosition?: mo
|
|||
return `${column.name}`;
|
||||
}
|
||||
};
|
||||
|
||||
// this is for backward compatibility, if the query is of fields or functions type
|
||||
// and the query is not set with ?? in the query, we should set it
|
||||
// https://github.com/elastic/elasticsearch/pull/122459
|
||||
export const fixESQLQueryWithVariables = (
|
||||
queryString: string,
|
||||
esqlVariables?: ESQLControlVariable[]
|
||||
) => {
|
||||
const currentVariables = getESQLQueryVariables(queryString);
|
||||
if (!currentVariables.length) {
|
||||
return queryString;
|
||||
}
|
||||
|
||||
// filter out the variables that are not used in the query
|
||||
// and that they are not of type FIELDS or FUNCTIONS
|
||||
const identifierTypeVariables = esqlVariables?.filter(
|
||||
(variable) =>
|
||||
currentVariables.includes(variable.key) &&
|
||||
(variable.type === ESQLVariableType.FIELDS || variable.type === ESQLVariableType.FUNCTIONS)
|
||||
);
|
||||
|
||||
// check if they are set with ?? or ? in the query
|
||||
// replace only if there is only one ? in front of the variable
|
||||
if (identifierTypeVariables?.length) {
|
||||
identifierTypeVariables.forEach((variable) => {
|
||||
const regex = new RegExp(`(?<!\\?)\\?${variable.key}`);
|
||||
queryString = queryString.replace(regex, `??${variable.key}`);
|
||||
});
|
||||
return queryString;
|
||||
}
|
||||
|
||||
return queryString;
|
||||
};
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('run query helpers', () => {
|
|||
|
||||
it('should return the variables if given', () => {
|
||||
const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' };
|
||||
const query = 'FROM foo | KEEP ?field | WHERE agent.name = ?agent_name';
|
||||
const query = 'FROM foo | KEEP ??field | WHERE agent.name = ?agent_name';
|
||||
const variables = [
|
||||
{
|
||||
key: 'field',
|
||||
|
@ -91,9 +91,7 @@ describe('run query helpers', () => {
|
|||
const params = getNamedParams(query, time, variables);
|
||||
expect(params).toStrictEqual([
|
||||
{
|
||||
field: {
|
||||
identifier: 'clientip',
|
||||
},
|
||||
field: 'clientip',
|
||||
},
|
||||
{
|
||||
interval: '5 minutes',
|
||||
|
@ -102,9 +100,7 @@ describe('run query helpers', () => {
|
|||
agent_name: 'go',
|
||||
},
|
||||
{
|
||||
function: {
|
||||
identifier: 'count',
|
||||
},
|
||||
function: 'count',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -112,7 +108,7 @@ describe('run query helpers', () => {
|
|||
it('should return the variables and named params if given', () => {
|
||||
const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' };
|
||||
const query =
|
||||
'FROM foo | KEEP ?field | WHERE agent.name = ?agent_name AND time < ?_tend amd time > ?_tstart';
|
||||
'FROM foo | KEEP ??field | WHERE agent.name = ?agent_name AND time < ?_tend amd time > ?_tstart';
|
||||
const variables = [
|
||||
{
|
||||
key: 'field',
|
||||
|
@ -140,9 +136,7 @@ describe('run query helpers', () => {
|
|||
expect(params[0]).toHaveProperty('_tstart');
|
||||
expect(params[1]).toHaveProperty('_tend');
|
||||
expect(params[2]).toStrictEqual({
|
||||
field: {
|
||||
identifier: 'clientip',
|
||||
},
|
||||
field: 'clientip',
|
||||
});
|
||||
expect(params[3]).toStrictEqual({
|
||||
interval: '5 minutes',
|
||||
|
@ -152,9 +146,7 @@ describe('run query helpers', () => {
|
|||
});
|
||||
|
||||
expect(params[5]).toStrictEqual({
|
||||
function: {
|
||||
identifier: 'count',
|
||||
},
|
||||
function: 'count',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ import { esFieldTypeToKibanaFieldType } from '@kbn/field-types';
|
|||
import type { ESQLColumn, ESQLSearchResponse, ESQLSearchParams } from '@kbn/es-types';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { type ESQLControlVariable } from '@kbn/esql-types';
|
||||
import { ESQLVariableType } from '@kbn/esql-types';
|
||||
|
||||
export const hasStartEndParams = (query: string) => /\?_tstart|\?_tend/i.test(query);
|
||||
|
||||
|
@ -47,12 +46,8 @@ export const getNamedParams = (
|
|||
) => {
|
||||
const namedParams: ESQLSearchParams['params'] = getStartEndParams(query, timeRange);
|
||||
if (variables?.length) {
|
||||
variables?.forEach(({ key, value, type }) => {
|
||||
if (type === ESQLVariableType.FIELDS || type === ESQLVariableType.FUNCTIONS) {
|
||||
namedParams.push({ [key]: { identifier: value } });
|
||||
} else {
|
||||
namedParams.push({ [key]: value });
|
||||
}
|
||||
variables?.forEach(({ key, value }) => {
|
||||
namedParams.push({ [key]: value });
|
||||
});
|
||||
}
|
||||
return namedParams;
|
||||
|
|
|
@ -405,7 +405,7 @@ describe('autocomplete.suggest', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('suggests `?function` option', async () => {
|
||||
test('suggests `??function` option', async () => {
|
||||
const { suggest } = await setup();
|
||||
|
||||
const suggestions = await suggest('FROM a | STATS var0 = /', {
|
||||
|
@ -423,8 +423,8 @@ describe('autocomplete.suggest', () => {
|
|||
});
|
||||
|
||||
expect(suggestions).toContainEqual({
|
||||
label: '?function',
|
||||
text: '?function',
|
||||
label: '??function',
|
||||
text: '??function',
|
||||
kind: 'Constant',
|
||||
detail: 'Named parameter',
|
||||
command: undefined,
|
||||
|
@ -453,7 +453,7 @@ describe('autocomplete.suggest', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('suggests `?field` option', async () => {
|
||||
test('suggests `??field` option', async () => {
|
||||
const { suggest } = await setup();
|
||||
|
||||
const suggestions = await suggest('FROM a | STATS BY /', {
|
||||
|
@ -471,8 +471,8 @@ describe('autocomplete.suggest', () => {
|
|||
});
|
||||
|
||||
expect(suggestions).toContainEqual({
|
||||
label: '?field',
|
||||
text: '?field',
|
||||
label: '??field',
|
||||
text: '??field',
|
||||
kind: 'Constant',
|
||||
detail: 'Named parameter',
|
||||
command: undefined,
|
||||
|
|
|
@ -36,6 +36,11 @@ const techPreviewLabel = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const getVariablePrefix = (variableType: ESQLVariableType) =>
|
||||
variableType === ESQLVariableType.FIELDS || variableType === ESQLVariableType.FUNCTIONS
|
||||
? '??'
|
||||
: '?';
|
||||
|
||||
const allFunctions = memoize(
|
||||
() =>
|
||||
aggFunctionDefinitions
|
||||
|
@ -228,7 +233,7 @@ export const buildFieldsDefinitionsWithMetadata = (
|
|||
const controlSuggestions = fields.length
|
||||
? getControlSuggestion(
|
||||
variableType,
|
||||
variables?.map((v) => `?${v.key}`)
|
||||
variables?.map((v) => `${getVariablePrefix(variableType)}${v.key}`)
|
||||
)
|
||||
: [];
|
||||
suggestions.push(...controlSuggestions);
|
||||
|
@ -487,11 +492,10 @@ export function getControlSuggestionIfSupported(
|
|||
if (!supportsControls) {
|
||||
return [];
|
||||
}
|
||||
const variableType = type;
|
||||
const variables = getVariables?.()?.filter((variable) => variable.type === variableType) ?? [];
|
||||
const variables = getVariables?.()?.filter((variable) => variable.type === type) ?? [];
|
||||
const controlSuggestion = getControlSuggestion(
|
||||
variableType,
|
||||
variables?.map((v) => `?${v.key}`)
|
||||
type,
|
||||
variables?.map((v) => `${getVariablePrefix(type)}${v.key}`)
|
||||
);
|
||||
return controlSuggestion;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
|
|||
await expectErrors('from a_index | stats var0 = avg(doubleField), count(*)', []);
|
||||
await expectErrors(`from a_index | stats sum(case(false, 0, 1))`, []);
|
||||
await expectErrors(`from a_index | stats var0 = sum( case(false, 0, 1))`, []);
|
||||
await expectErrors('from a_index | stats ??func(doubleField)', []);
|
||||
await expectErrors('from a_index | stats avg(??field)', []);
|
||||
|
||||
// "or" must accept "null"
|
||||
await expectErrors('from a_index | stats count(textField == "a" or null)', []);
|
||||
|
@ -170,7 +172,7 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
|
|||
'from a_index | stats avg(doubleField), percentile(doubleField, 50) + 1 by ipField',
|
||||
[]
|
||||
);
|
||||
await expectErrors('from a_index | stats ?func(doubleField)', []);
|
||||
await expectErrors('from a_index | stats avg(doubleField) by ??field', []);
|
||||
for (const op of ['+', '-', '*', '/', '%']) {
|
||||
await expectErrors(
|
||||
`from a_index | stats avg(doubleField) ${op} percentile(doubleField, 50) BY ipField`,
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
getParamAtPosition,
|
||||
extractSingularType,
|
||||
isArrayType,
|
||||
isParametrized,
|
||||
} from '../shared/helpers';
|
||||
import { getMessageFromId, errors } from './errors';
|
||||
import { getMaxMinNumberOfParams, collapseWrongArgumentTypeMessages } from './helpers';
|
||||
|
@ -487,7 +488,7 @@ function validateFunctionColumnArg(
|
|||
parentCommand: string
|
||||
) {
|
||||
const messages: ESQLMessage[] = [];
|
||||
if (!(isColumnItem(actualArg) || isIdentifier(actualArg))) {
|
||||
if (!(isColumnItem(actualArg) || isIdentifier(actualArg)) || isParametrized(actualArg)) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
|
|
@ -393,7 +393,7 @@ export function validateColumnForCommand(
|
|||
if (!references.variables.has(column.name) && !isParametrized(column)) {
|
||||
messages.push(errors.unknownColumn(column));
|
||||
}
|
||||
} else if (!getColumnExists(column, references)) {
|
||||
} else if (!getColumnExists(column, references) && !isParametrized(column)) {
|
||||
messages.push(errors.unknownColumn(column));
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import type {
|
|||
} from '@kbn/expressions-plugin/common';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { getNamedParams, mapVariableToColumn } from '@kbn/esql-utils';
|
||||
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
|
||||
import { getIndexPatternFromESQLQuery, fixESQLQueryWithVariables } from '@kbn/esql-utils';
|
||||
import { zipObject } from 'lodash';
|
||||
import { catchError, defer, map, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||
import { buildEsQuery, type Filter } from '@kbn/es-query';
|
||||
|
@ -181,8 +181,12 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
|
|||
})
|
||||
).pipe(
|
||||
switchMap(({ search, uiSettings }) => {
|
||||
// this is for backward compatibility, if the query is of fields or functions type
|
||||
// and the query is not set with ?? in the query, we should set it
|
||||
// https://github.com/elastic/elasticsearch/pull/122459
|
||||
const fixedQuery = fixESQLQueryWithVariables(query, input?.esqlVariables ?? []);
|
||||
const params: ESQLSearchParams = {
|
||||
query,
|
||||
query: fixedQuery,
|
||||
// time_zone: timezone,
|
||||
locale,
|
||||
include_ccs_metadata: true,
|
||||
|
@ -192,7 +196,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
|
|||
uiSettings as Parameters<typeof getEsQueryConfig>[0]
|
||||
);
|
||||
|
||||
const namedParams = getNamedParams(query, input.timeRange, input.esqlVariables);
|
||||
const namedParams = getNamedParams(fixedQuery, input.timeRange, input.esqlVariables);
|
||||
|
||||
if (namedParams.length) {
|
||||
params.params = namedParams;
|
||||
|
@ -362,8 +366,9 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
|
|||
isNull: hasEmptyColumns ? !lookup.has(name) : false,
|
||||
})) ?? [];
|
||||
|
||||
const fixedQuery = fixESQLQueryWithVariables(query, input?.esqlVariables ?? []);
|
||||
const updatedWithVariablesColumns = mapVariableToColumn(
|
||||
query,
|
||||
fixedQuery,
|
||||
input?.esqlVariables ?? [],
|
||||
allColumns as DatatableColumn[]
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@ describe('helpers', () => {
|
|||
describe('updateQueryStringWithVariable', () => {
|
||||
it('should update the query string with the variable for an one line query string', () => {
|
||||
const queryString = 'FROM my_index | STATS BY ';
|
||||
const variable = 'my_variable';
|
||||
const variable = '?my_variable';
|
||||
const cursorPosition = { column: 26, lineNumber: 1 } as monaco.Position;
|
||||
const updatedQueryString = updateQueryStringWithVariable(
|
||||
queryString,
|
||||
|
@ -31,7 +31,7 @@ describe('helpers', () => {
|
|||
|
||||
it('should update the query string with the variable for multiline query string', () => {
|
||||
const queryString = 'FROM my_index \n| STATS BY ';
|
||||
const variable = 'my_variable';
|
||||
const variable = '?my_variable';
|
||||
const cursorPosition = { column: 12, lineNumber: 2 } as monaco.Position;
|
||||
const updatedQueryString = updateQueryStringWithVariable(
|
||||
queryString,
|
||||
|
@ -90,18 +90,28 @@ describe('helpers', () => {
|
|||
|
||||
describe('validateVariableName', () => {
|
||||
it('should return the variable without special characters', () => {
|
||||
const variable = validateVariableName('my_variable/123');
|
||||
expect(variable).toBe('my_variable123');
|
||||
const variable = validateVariableName('?my_variable/123', '?');
|
||||
expect(variable).toBe('?my_variable123');
|
||||
});
|
||||
|
||||
it('should remove the questionarks', () => {
|
||||
const variable = validateVariableName('?my_variable');
|
||||
expect(variable).toBe('my_variable');
|
||||
it('should add questionarks if they dont exist', () => {
|
||||
const variable = validateVariableName('my_variable', '?');
|
||||
expect(variable).toBe('?my_variable');
|
||||
});
|
||||
|
||||
it('should remove the _ in the first char', () => {
|
||||
const variable = validateVariableName('?_my_variable');
|
||||
expect(variable).toBe('my_variable');
|
||||
it('should remove the _ after the ? prefix', () => {
|
||||
const variable = validateVariableName('?_my_variable', '?');
|
||||
expect(variable).toBe('?my_variable');
|
||||
});
|
||||
|
||||
it('should remove the _ after the ?? prefix', () => {
|
||||
const variable = validateVariableName('??_my_variable', '??');
|
||||
expect(variable).toBe('??my_variable');
|
||||
});
|
||||
|
||||
it('should not allow more than 2 questiomarks', () => {
|
||||
const variable = validateVariableName('???my_variable', '??');
|
||||
expect(variable).toBe('??my_variable');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,6 @@ export const updateQueryStringWithVariable = (
|
|||
variable: string,
|
||||
cursorPosition: monaco.Position
|
||||
) => {
|
||||
const variableName = `?${variable}`;
|
||||
const cursorColumn = cursorPosition?.column ?? 0;
|
||||
const cursorLine = cursorPosition?.lineNumber ?? 0;
|
||||
const lines = queryString.split('\n');
|
||||
|
@ -29,7 +28,7 @@ export const updateQueryStringWithVariable = (
|
|||
const queryPartToBeUpdated = queryArray[cursorLine - 1];
|
||||
const queryWithVariable = [
|
||||
queryPartToBeUpdated.slice(0, cursorColumn - 1),
|
||||
variableName,
|
||||
variable,
|
||||
queryPartToBeUpdated.slice(cursorColumn - 1),
|
||||
].join('');
|
||||
queryArray[cursorLine - 1] = queryWithVariable;
|
||||
|
@ -38,7 +37,7 @@ export const updateQueryStringWithVariable = (
|
|||
|
||||
return [
|
||||
queryString.slice(0, cursorColumn - 1),
|
||||
variableName,
|
||||
variable,
|
||||
queryString.slice(cursorColumn - 1),
|
||||
].join('');
|
||||
};
|
||||
|
@ -103,13 +102,48 @@ export const getFlyoutStyling = () => {
|
|||
`;
|
||||
};
|
||||
|
||||
export const validateVariableName = (variableName: string) => {
|
||||
export const validateVariableName = (variableName: string, prefix: '??' | '?') => {
|
||||
let text = variableName
|
||||
// variable name can only contain letters, numbers and underscores
|
||||
.replace(/[^a-zA-Z0-9_]/g, '');
|
||||
if (text.charAt(0) === '_') {
|
||||
text = text.substring(1);
|
||||
// variable name can only contain letters, numbers, underscores and questionmarks
|
||||
.replace(/[^a-zA-Z0-9_?]/g, '');
|
||||
|
||||
if (!text.startsWith('?')) {
|
||||
text = `?${text}`;
|
||||
}
|
||||
|
||||
const match = text.match(/^(\?*)/);
|
||||
const leadingQuestionMarksCount = match ? match[0].length : 0;
|
||||
|
||||
if (leadingQuestionMarksCount > 2) {
|
||||
text = '??'.concat(text.substring(leadingQuestionMarksCount));
|
||||
}
|
||||
|
||||
// Remove unnecessary leading underscores
|
||||
if (text.charAt(prefix.length) === '_') {
|
||||
text = `${prefix}${text.substring(prefix.length + 1)}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const getVariableTypeFromQuery = (str: string, variableType: ESQLVariableType) => {
|
||||
const match = str.match(/^(\?*)/);
|
||||
const leadingQuestionMarksCount = match ? match[0].length : 0;
|
||||
if (
|
||||
leadingQuestionMarksCount === 2 &&
|
||||
variableType !== ESQLVariableType.FIELDS &&
|
||||
variableType !== ESQLVariableType.FUNCTIONS
|
||||
) {
|
||||
return ESQLVariableType.FIELDS;
|
||||
}
|
||||
|
||||
if (
|
||||
leadingQuestionMarksCount === 1 &&
|
||||
variableType !== ESQLVariableType.TIME_LITERAL &&
|
||||
variableType !== ESQLVariableType.VALUES
|
||||
) {
|
||||
return ESQLVariableType.VALUES;
|
||||
}
|
||||
|
||||
return variableType;
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render, within, fireEvent } from '@testing-library/react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { ESQLVariableType } from '@kbn/esql-types';
|
||||
|
@ -30,16 +31,18 @@ describe('IdentifierControlForm', () => {
|
|||
describe('Field type', () => {
|
||||
it('should default correctly if no initial state is given', async () => {
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// control type dropdown should be rendered and default to 'STATIC_VALUES'
|
||||
// no need to test further as the control type is disabled
|
||||
|
@ -48,7 +51,7 @@ describe('IdentifierControlForm', () => {
|
|||
expect(within(controlTypeInputPopover).getByRole('combobox')).toHaveValue(`Static values`);
|
||||
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('field');
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('??field');
|
||||
|
||||
// fields dropdown should be rendered with available fields column1 and column2
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
|
||||
|
@ -78,16 +81,18 @@ describe('IdentifierControlForm', () => {
|
|||
it('should call the onCreateControl callback, if no initialState is given', async () => {
|
||||
const onCreateControlSpy = jest.fn();
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={onCreateControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={onCreateControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
// select the first field
|
||||
|
@ -104,17 +109,19 @@ describe('IdentifierControlForm', () => {
|
|||
it('should call the onCancelControl callback, if Cancel button is clicked', async () => {
|
||||
const onCancelControlSpy = jest.fn();
|
||||
const { findByTestId } = render(
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
onCancelControl={onCancelControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
onCancelControl={onCancelControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// click on the cancel button
|
||||
fireEvent.click(await findByTestId('cancelEsqlControlsFlyoutButton'));
|
||||
|
@ -134,20 +141,22 @@ describe('IdentifierControlForm', () => {
|
|||
controlType: EsqlControlType.STATIC_VALUES,
|
||||
} as ESQLControlState;
|
||||
const { findByTestId } = render(
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('myField');
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('??myField');
|
||||
|
||||
// fields dropdown should be rendered with column2 selected
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
|
||||
|
@ -184,17 +193,19 @@ describe('IdentifierControlForm', () => {
|
|||
} as ESQLControlState;
|
||||
const onEditControlSpy = jest.fn();
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={onEditControlSpy}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={onEditControlSpy}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
// select the first field
|
||||
|
@ -212,16 +223,18 @@ describe('IdentifierControlForm', () => {
|
|||
describe('Functions type', () => {
|
||||
it('should default correctly if no initial state is given', async () => {
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FUNCTIONS}
|
||||
queryString="FROM foo | STATS "
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 17, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<IdentifierControlForm
|
||||
variableType={ESQLVariableType.FUNCTIONS}
|
||||
queryString="FROM foo | STATS "
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
cursorPosition={{ column: 17, lineNumber: 1 } as monaco.Position}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// control type dropdown should be rendered and default to 'STATIC_VALUES'
|
||||
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
|
||||
|
@ -229,7 +242,7 @@ describe('IdentifierControlForm', () => {
|
|||
expect(within(controlTypeInputPopover).getByRole('combobox')).toHaveValue(`Static values`);
|
||||
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('function');
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('??function');
|
||||
|
||||
// fields dropdown should be rendered with available functions
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
getQueryForFields,
|
||||
validateVariableName,
|
||||
getVariablePrefix,
|
||||
getVariableTypeFromQuery,
|
||||
} from './helpers';
|
||||
import { EsqlControlType } from '../types';
|
||||
|
||||
|
@ -54,6 +55,8 @@ interface IdentifierControlFormProps {
|
|||
onCancelControl?: () => void;
|
||||
}
|
||||
|
||||
const IDENTIFIER_VARIABLE_PREFIX = '??';
|
||||
|
||||
export function IdentifierControlForm({
|
||||
variableType,
|
||||
initialState,
|
||||
|
@ -75,11 +78,14 @@ export function IdentifierControlForm({
|
|||
);
|
||||
|
||||
if (initialState) {
|
||||
return initialState.variableName;
|
||||
return `${IDENTIFIER_VARIABLE_PREFIX}${initialState.variableName}`;
|
||||
}
|
||||
|
||||
const variablePrefix = getVariablePrefix(variableType);
|
||||
return getRecurrentVariableName(variablePrefix, existingVariables);
|
||||
return `${IDENTIFIER_VARIABLE_PREFIX}${getRecurrentVariableName(
|
||||
variablePrefix,
|
||||
existingVariables
|
||||
)}`;
|
||||
}, [esqlVariables, initialState, variableType]);
|
||||
|
||||
const [availableIdentifiersOptions, setAvailableIdentifiersOptions] = useState<
|
||||
|
@ -150,8 +156,9 @@ export function IdentifierControlForm({
|
|||
|
||||
useEffect(() => {
|
||||
const variableExists =
|
||||
esqlVariables.some((variable) => variable.key === variableName.replace('?', '')) &&
|
||||
!isControlInEditMode;
|
||||
esqlVariables.some(
|
||||
(variable) => variable.key === variableName.replace(IDENTIFIER_VARIABLE_PREFIX, '')
|
||||
) && !isControlInEditMode;
|
||||
|
||||
setFormIsInvalid(!selectedIdentifiers.length || !variableName || variableExists);
|
||||
}, [esqlVariables, isControlInEditMode, selectedIdentifiers.length, variableName]);
|
||||
|
@ -162,7 +169,7 @@ export function IdentifierControlForm({
|
|||
|
||||
const onVariableNameChange = useCallback(
|
||||
(e: { target: { value: React.SetStateAction<string> } }) => {
|
||||
const text = validateVariableName(String(e.target.value));
|
||||
const text = validateVariableName(String(e.target.value), IDENTIFIER_VARIABLE_PREFIX);
|
||||
setVariableName(text);
|
||||
},
|
||||
[]
|
||||
|
@ -211,13 +218,15 @@ export function IdentifierControlForm({
|
|||
|
||||
const onCreateFieldControl = useCallback(async () => {
|
||||
const availableOptions = selectedIdentifiers.map((field) => field.label);
|
||||
// removes the double question mark from the variable name
|
||||
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
|
||||
const state = {
|
||||
availableOptions,
|
||||
selectedOptions: [availableOptions[0]],
|
||||
width: minimumWidth,
|
||||
title: label || variableName,
|
||||
variableName,
|
||||
variableType,
|
||||
title: label || variableNameWithoutQuestionmark,
|
||||
variableName: variableNameWithoutQuestionmark,
|
||||
variableType: getVariableTypeFromQuery(variableName, variableType),
|
||||
controlType: EsqlControlType.STATIC_VALUES,
|
||||
esqlQuery: queryString,
|
||||
grow,
|
||||
|
|
|
@ -11,6 +11,7 @@ import React, { useCallback } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { ESQLControlVariable } from '@kbn/esql-types';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { TooltipWrapper } from '@kbn/visualization-utils';
|
||||
import {
|
||||
EuiFieldText,
|
||||
|
@ -32,6 +33,7 @@ import {
|
|||
EuiToolTip,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import { EsqlControlType } from '../types';
|
||||
|
||||
|
@ -151,9 +153,25 @@ export function VariableName({
|
|||
esqlVariables?: ESQLControlVariable[];
|
||||
onVariableNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
const genericContent = i18n.translate('esql.flyout.variableName.helpText', {
|
||||
defaultMessage: 'This name will be prefaced with a "?" in the editor',
|
||||
const tooltipContent = i18n.translate('esql.flyout.variableName.tooltipText', {
|
||||
defaultMessage:
|
||||
'Start your control name with ? to replace values or with ?? to replace field names or functions.',
|
||||
});
|
||||
|
||||
const helpText = (
|
||||
<FormattedMessage
|
||||
id="esql.flyout.variableName.helpText"
|
||||
defaultMessage="Start your control name with {valuesPrefix} to replace {valuesBold} or with {fieldsPrefix} to replace {fieldsBold} or {functionsBold}."
|
||||
values={{
|
||||
valuesPrefix: <EuiCode>?</EuiCode>,
|
||||
fieldsPrefix: <EuiCode>??</EuiCode>,
|
||||
valuesBold: <strong>values</strong>,
|
||||
fieldsBold: <strong>fields</strong>,
|
||||
functionsBold: <strong>functions</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const isDisabledTooltipText = i18n.translate('esql.flyout.variableName.disabledTooltip', {
|
||||
defaultMessage: 'You can’t edit a control name after it’s been created.',
|
||||
});
|
||||
|
@ -165,9 +183,7 @@ export function VariableName({
|
|||
label={i18n.translate('esql.flyout.variableName.label', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
helpText={i18n.translate('esql.flyout.variableName.helpText', {
|
||||
defaultMessage: 'This name will be prefaced with a "?" in the editor',
|
||||
})}
|
||||
helpText={helpText}
|
||||
fullWidth
|
||||
autoFocus
|
||||
isInvalid={!variableName || variableExists}
|
||||
|
@ -184,7 +200,7 @@ export function VariableName({
|
|||
}
|
||||
>
|
||||
<EuiToolTip
|
||||
content={isControlInEditMode ? isDisabledTooltipText : genericContent}
|
||||
content={isControlInEditMode ? isDisabledTooltipText : tooltipContent}
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
|
|
|
@ -62,15 +62,17 @@ describe('ValueControlForm', () => {
|
|||
describe('Interval type', () => {
|
||||
it('should default correctly if no initial state is given for an interval variable type', async () => {
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// control type dropdown should be rendered and default to 'STATIC_VALUES'
|
||||
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
|
||||
|
@ -78,7 +80,7 @@ describe('ValueControlForm', () => {
|
|||
expect(within(controlTypeInputPopover).getByRole('combobox')).toHaveValue(`Static values`);
|
||||
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('interval');
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('?interval');
|
||||
|
||||
// values dropdown should be rendered
|
||||
const valuesOptionsDropdown = await findByTestId('esqlValuesOptions');
|
||||
|
@ -108,15 +110,17 @@ describe('ValueControlForm', () => {
|
|||
it('should call the onCreateControl callback, if no initialState is given', async () => {
|
||||
const onCreateControlSpy = jest.fn();
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={onCreateControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={onCreateControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
// select the first interval
|
||||
|
@ -133,16 +137,18 @@ describe('ValueControlForm', () => {
|
|||
it('should call the onCancelControl callback, if Cancel button is clicked', async () => {
|
||||
const onCancelControlSpy = jest.fn();
|
||||
const { findByTestId } = render(
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
onCancelControl={onCancelControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
onCancelControl={onCancelControlSpy}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// click on the cancel button
|
||||
fireEvent.click(await findByTestId('cancelEsqlControlsFlyoutButton'));
|
||||
|
@ -162,19 +168,21 @@ describe('ValueControlForm', () => {
|
|||
controlType: EsqlControlType.STATIC_VALUES,
|
||||
} as ESQLControlState;
|
||||
const { findByTestId } = render(
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.TIME_LITERAL}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={jest.fn()}
|
||||
search={searchMock}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('myInterval');
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('?myInterval');
|
||||
|
||||
// values dropdown should be rendered with column2 selected
|
||||
const valuesOptionsDropdown = await findByTestId('esqlValuesOptions');
|
||||
|
@ -211,16 +219,18 @@ describe('ValueControlForm', () => {
|
|||
} as ESQLControlState;
|
||||
const onEditControlSpy = jest.fn();
|
||||
const { findByTestId } = render(
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={onEditControlSpy}
|
||||
search={searchMock}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
<IntlProvider locale="en">
|
||||
<ValueControlForm
|
||||
variableType={ESQLVariableType.FIELDS}
|
||||
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
|
||||
onCreateControl={jest.fn()}
|
||||
closeFlyout={jest.fn()}
|
||||
onEditControl={onEditControlSpy}
|
||||
search={searchMock}
|
||||
initialState={initialState}
|
||||
esqlVariables={[]}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
// click on the create button
|
||||
fireEvent.click(await findByTestId('saveEsqlControlsFlyoutButton'));
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
areValuesIntervalsValid,
|
||||
validateVariableName,
|
||||
getVariablePrefix,
|
||||
getVariableTypeFromQuery,
|
||||
} from './helpers';
|
||||
import { EsqlControlType } from '../types';
|
||||
import { ChooseColumnPopover } from './choose_column_popover';
|
||||
|
@ -65,6 +66,7 @@ interface ValueControlFormProps {
|
|||
}
|
||||
|
||||
const SUGGESTED_INTERVAL_VALUES = ['5 minutes', '1 hour', '1 day', '1 week', '1 month'];
|
||||
const VALUE_VARIABLE_PREFIX = '?';
|
||||
|
||||
export function ValueControlForm({
|
||||
variableType,
|
||||
|
@ -93,7 +95,7 @@ export function ValueControlForm({
|
|||
);
|
||||
|
||||
if (initialState) {
|
||||
return initialState.variableName;
|
||||
return `${VALUE_VARIABLE_PREFIX}${initialState.variableName}`;
|
||||
}
|
||||
|
||||
let variablePrefix = getVariablePrefix(variableType);
|
||||
|
@ -104,7 +106,7 @@ export function ValueControlForm({
|
|||
variablePrefix = fieldVariableName;
|
||||
}
|
||||
|
||||
return getRecurrentVariableName(variablePrefix, existingVariables);
|
||||
return `${VALUE_VARIABLE_PREFIX}${getRecurrentVariableName(variablePrefix, existingVariables)}`;
|
||||
}, [esqlVariables, initialState, valuesField, variableType]);
|
||||
|
||||
const [controlFlyoutType, setControlFlyoutType] = useState<EsqlControlType>(
|
||||
|
@ -214,7 +216,7 @@ export function ValueControlForm({
|
|||
|
||||
const onVariableNameChange = useCallback(
|
||||
(e: { target: { value: React.SetStateAction<string> } }) => {
|
||||
const text = validateVariableName(String(e.target.value));
|
||||
const text = validateVariableName(String(e.target.value), VALUE_VARIABLE_PREFIX);
|
||||
setVariableName(text);
|
||||
},
|
||||
[]
|
||||
|
@ -298,13 +300,15 @@ export function ValueControlForm({
|
|||
|
||||
const onCreateValueControl = useCallback(async () => {
|
||||
const availableOptions = selectedValues.map((value) => value.label);
|
||||
// removes the question mark from the variable name
|
||||
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
|
||||
const state = {
|
||||
availableOptions,
|
||||
selectedOptions: [availableOptions[0]],
|
||||
width: minimumWidth,
|
||||
title: label || variableName,
|
||||
variableName,
|
||||
variableType,
|
||||
title: label || variableNameWithoutQuestionmark,
|
||||
variableName: variableNameWithoutQuestionmark,
|
||||
variableType: getVariableTypeFromQuery(variableName, variableType),
|
||||
esqlQuery: valuesQuery || queryString,
|
||||
controlType: controlFlyoutType,
|
||||
grow,
|
||||
|
|
|
@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// Check Lens editor has been updated accordingly
|
||||
const editorValue = await esql.getEsqlEditorQuery();
|
||||
expect(editorValue).to.contain('FROM logstash* | STATS COUNT(*) BY ?field');
|
||||
expect(editorValue).to.contain('FROM logstash* | STATS COUNT(*) BY ??field');
|
||||
});
|
||||
|
||||
it('should update the Lens chart accordingly', async () => {
|
||||
|
|
|
@ -3014,7 +3014,6 @@
|
|||
"esql.flyout.valuesQueryEditor.label": "Recherche de valeurs",
|
||||
"esql.flyout.variableName.disabledTooltip": "Vous ne pouvez pas modifier un nom de contrôle après sa création.",
|
||||
"esql.flyout.variableName.error": "Le nom de variable est requis",
|
||||
"esql.flyout.variableName.helpText": "Ce nom sera précédé d'un \"?\" dans l'éditeur.",
|
||||
"esql.flyout.variableName.label": "Nom",
|
||||
"esql.flyout.variableName.placeholder": "Définir un nom de variable",
|
||||
"esql.flyout.variableNameExists.error": "Ce nom de variable existe déjà",
|
||||
|
|
|
@ -3009,7 +3009,6 @@
|
|||
"esql.flyout.valuesQueryEditor.label": "値クエリ",
|
||||
"esql.flyout.variableName.disabledTooltip": "作成した後にコントロール名を編集することはできません。",
|
||||
"esql.flyout.variableName.error": "変数名は必須です",
|
||||
"esql.flyout.variableName.helpText": "エディターでは、この名前の前に?が付けられます",
|
||||
"esql.flyout.variableName.label": "名前",
|
||||
"esql.flyout.variableName.placeholder": "変数名を設定",
|
||||
"esql.flyout.variableNameExists.error": "変数名はすでに存在します",
|
||||
|
|
|
@ -3014,7 +3014,6 @@
|
|||
"esql.flyout.valuesQueryEditor.label": "值查询",
|
||||
"esql.flyout.variableName.disabledTooltip": "创建控件后即无法编辑控件名称。",
|
||||
"esql.flyout.variableName.error": "变量名称必填",
|
||||
"esql.flyout.variableName.helpText": "在编辑器中,此名称将以“?”开头",
|
||||
"esql.flyout.variableName.label": "名称",
|
||||
"esql.flyout.variableName.placeholder": "设置变量名称",
|
||||
"esql.flyout.variableNameExists.error": "变量名称已存在",
|
||||
|
|
|
@ -62,7 +62,7 @@ export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) {
|
|||
const columns = table.columns.map((col) => {
|
||||
return {
|
||||
id: col.variable ?? col.id,
|
||||
name: col.variable ? `?${col.variable}` : col.name,
|
||||
name: col.variable ? `??${col.variable}` : col.name,
|
||||
meta: col?.meta ?? { type: 'number' },
|
||||
variable: col.variable,
|
||||
compatible:
|
||||
|
|
|
@ -138,7 +138,7 @@ describe('onDrop', () => {
|
|||
field: 'field',
|
||||
id: 'field',
|
||||
humanData: {
|
||||
label: '?field',
|
||||
label: '??field',
|
||||
},
|
||||
},
|
||||
target: {
|
||||
|
@ -151,7 +151,7 @@ describe('onDrop', () => {
|
|||
layerNumber: 1,
|
||||
position: 1,
|
||||
label: 'Empty dimension',
|
||||
nextLabel: '?field',
|
||||
nextLabel: '??field',
|
||||
canDuplicate: false,
|
||||
},
|
||||
columnId: 'empty',
|
||||
|
@ -163,7 +163,7 @@ describe('onDrop', () => {
|
|||
column1,
|
||||
column2,
|
||||
column3,
|
||||
{ columnId: 'empty', fieldName: '?field', meta: { type: 'number' }, variable: 'field' },
|
||||
{ columnId: 'empty', fieldName: '??field', meta: { type: 'number' }, variable: 'field' },
|
||||
];
|
||||
expect(onDrop(props)).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -209,7 +209,7 @@ describe('onDrop', () => {
|
|||
field: 'field',
|
||||
id: 'field',
|
||||
humanData: {
|
||||
label: '?field',
|
||||
label: '??field',
|
||||
},
|
||||
},
|
||||
dropType: 'field_replace' as DropType,
|
||||
|
@ -217,7 +217,7 @@ describe('onDrop', () => {
|
|||
const expectedColumns = [
|
||||
column1,
|
||||
column2,
|
||||
{ columnId: 'columnId3', fieldName: '?field', meta: { type: 'number' }, variable: 'field' },
|
||||
{ columnId: 'columnId3', fieldName: '??field', meta: { type: 'number' }, variable: 'field' },
|
||||
];
|
||||
expect(onDrop(props)).toEqual(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -34,7 +34,7 @@ export const onDrop = (props: DatasourceDimensionDropHandlerProps<TextBasedPriva
|
|||
const targetField = allColumns.find((f) => f.columnId === target.columnId);
|
||||
const newColumn = {
|
||||
columnId: target.columnId,
|
||||
fieldName: sourceField?.variable ? `?${sourceField.variable}` : sourceField?.fieldName ?? '',
|
||||
fieldName: sourceField?.variable ? `??${sourceField.variable}` : sourceField?.fieldName ?? '',
|
||||
meta: sourceField?.meta,
|
||||
variable: sourceField?.variable,
|
||||
};
|
||||
|
|
|
@ -235,7 +235,7 @@ export function getTextBasedDatasource({
|
|||
);
|
||||
return {
|
||||
columnId: c.variable ?? c.id,
|
||||
fieldName: c.variable ? `?${c.variable}` : c.id,
|
||||
fieldName: c.variable ? `??${c.variable}` : c.id,
|
||||
variable: c.variable,
|
||||
label: c.name,
|
||||
customLabel: c.id !== c.name,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue