[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)


![meow](https://github.com/user-attachments/assets/a1863b5b-e113-494a-9231-e16386876e91)


### 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:
Stratoula Kalafateli 2025-03-28 16:34:37 +01:00 committed by GitHub
parent 51932b6065
commit b477afb783
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 451 additions and 217 deletions

View file

@ -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,

View file

@ -39,6 +39,7 @@ export {
mapVariableToColumn,
getValuesFromQueryField,
getESQLQueryVariables,
fixESQLQueryWithVariables,
} from './src';
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';

View file

@ -23,6 +23,7 @@ export {
mapVariableToColumn,
getValuesFromQueryField,
getESQLQueryVariables,
fixESQLQueryWithVariables,
} from './utils/query_parsing_helpers';
export { queryCannotBeSampled } from './utils/query_cannot_be_sampled';
export {

View file

@ -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);
});
});
});

View file

@ -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;
};

View file

@ -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',
});
});
});

View file

@ -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;

View file

@ -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,

View file

@ -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;
}

View file

@ -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`,

View file

@ -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;
}

View file

@ -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));
}

View file

@ -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[]
);

View file

@ -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');
});
});
});

View file

@ -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;
};

View file

@ -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');

View file

@ -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,

View file

@ -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 cant edit a control name after its 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%;
`}

View file

@ -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'));

View file

@ -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,

View file

@ -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 () => {

View file

@ -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à",

View file

@ -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": "変数名はすでに存在します",

View file

@ -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": "变量名称已存在",

View file

@ -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:

View file

@ -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({

View file

@ -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,
};

View file

@ -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,