[ES|QL] Refactor and improve the variables configuration (#216469)

## Summary

Part of https://github.com/elastic/kibana/issues/213877

This PR doent introduce a new feature. It is mostly a refactoring to
allow us to give a better UX when a user during creation changes the
variable name from ??value to ?value and vice versa. The biggest change
is that we move 2 components from the individual form (value,
identifier) to index as now they can be shared and they own the same
functionality regardless the control type.

It is required to move to the next step, the creation of controls by
just typing a questionmark

### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Stratoula Kalafateli 2025-04-04 07:50:34 +02:00 committed by GitHub
parent 5bab6cf836
commit 94f0e694e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 568 additions and 520 deletions

View file

@ -581,6 +581,24 @@ describe('esql query helpers', () => {
} as monaco.Position);
expect(values).toEqual('my_field');
});
it('should return undefined if no column is found', () => {
const queryString = 'FROM my_index | STATS COUNT() ';
const values = getValuesFromQueryField(queryString, {
lineNumber: 1,
column: 31,
} as monaco.Position);
expect(values).toEqual(undefined);
});
it('should return undefined if the column is *', () => {
const queryString = 'FROM my_index | STATS COUNT(*) ';
const values = getValuesFromQueryField(queryString, {
lineNumber: 1,
column: 31,
} as monaco.Position);
expect(values).toEqual(undefined);
});
});
describe('fixESQLQueryWithVariables', () => {

View file

@ -224,7 +224,7 @@ export const getValuesFromQueryField = (queryString: string, cursorPosition?: mo
const column = Walker.match(lastCommand, { type: 'column' });
if (column) {
if (column && column.name && column.name !== '*') {
return `${column.name}`;
}
};

View file

@ -7,12 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { monaco } from '@kbn/monaco';
import { ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types';
import {
updateQueryStringWithVariable,
getQueryForFields,
areValuesIntervalsValid,
getRecurrentVariableName,
validateVariableName,
checkVariableExistence,
} from './helpers';
describe('helpers', () => {
@ -114,4 +116,27 @@ describe('helpers', () => {
expect(variable).toBe('??my_variable');
});
});
describe('checkVariableExistence', () => {
it('should return true if the variable exists', () => {
const variables = [
{ key: 'my_variable', type: ESQLVariableType.VALUES, value: 'value1' },
{ key: 'my_variable2', type: ESQLVariableType.FIELDS, value: 'value2' },
] as ESQLControlVariable[];
const variableName = '?my_variable';
const exists = checkVariableExistence(variables, variableName);
expect(exists).toBe(true);
});
it('should return false if the variable does not exist', () => {
const variables = [
{ key: 'my_variable', type: ESQLVariableType.VALUES, value: 'value1' },
{ key: 'my_variable2', type: ESQLVariableType.FIELDS, value: 'value2' },
] as ESQLControlVariable[];
// here ?variable2 is different from ??variable2
const variableName = '?my_variable2';
const exists = checkVariableExistence(variables, variableName);
expect(exists).toBe(false);
});
});
});

View file

@ -7,8 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { monaco } from '@kbn/monaco';
import { ESQLVariableType } from '@kbn/esql-types';
import { ESQLVariableType, ESQLControlVariable } from '@kbn/esql-types';
import { timeUnits } from '@kbn/esql-validation-autocomplete';
import { VariableNamePrefix } from '../types';
function inKnownTimeInterval(timeIntervalUnit: string): boolean {
return timeUnits.some((unit) => unit === timeIntervalUnit.toLowerCase());
@ -65,7 +66,7 @@ export const areValuesIntervalsValid = (values: string[]) => {
});
};
export const getVariablePrefix = (variableType: ESQLVariableType) => {
export const getVariableSuggestion = (variableType: ESQLVariableType) => {
switch (variableType) {
case ESQLVariableType.FIELDS:
return 'field';
@ -147,3 +148,41 @@ export const getVariableTypeFromQuery = (str: string, variableType: ESQLVariable
return variableType;
};
export const getVariableNamePrefix = (type: ESQLVariableType) => {
switch (type) {
case ESQLVariableType.FIELDS:
case ESQLVariableType.FUNCTIONS:
return VariableNamePrefix.IDENTIFIER;
case ESQLVariableType.VALUES:
case ESQLVariableType.TIME_LITERAL:
default:
return VariableNamePrefix.VALUE;
}
};
export const checkVariableExistence = (
esqlVariables: ESQLControlVariable[],
variableName: string
): boolean => {
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
const match = variableName.match(/^(\?*)/);
const leadingQuestionMarksCount = match ? match[0].length : 0;
return esqlVariables.some((variable) => {
const prefix = getVariableNamePrefix(variable.type);
if (leadingQuestionMarksCount === 2) {
if (prefix === VariableNamePrefix.IDENTIFIER) {
return variable.key === variableNameWithoutQuestionmark;
}
return false;
}
if (leadingQuestionMarksCount === 1) {
if (prefix === VariableNamePrefix.VALUE) {
return variable.key === variableNameWithoutQuestionmark;
}
return false;
}
return false;
});
};

View file

@ -13,11 +13,12 @@ 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';
import { IdentifierControlForm } from './identifier_control_form';
import { ESQLControlsFlyout } from '.';
import { ESQLControlState, EsqlControlType } from '../types';
jest.mock('@kbn/esql-utils', () => ({
getESQLQueryColumnsRaw: jest.fn().mockResolvedValue([{ name: 'column1' }, { name: 'column2' }]),
getValuesFromQueryField: jest.fn().mockReturnValue('field'),
}));
describe('IdentifierControlForm', () => {
@ -32,12 +33,12 @@ describe('IdentifierControlForm', () => {
it('should default correctly if no initial state is given', async () => {
const { findByTestId, findByTitle } = render(
<IntlProvider locale="en">
<IdentifierControlForm
variableType={ESQLVariableType.FIELDS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FIELDS}
queryString="FROM foo | STATS BY"
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
esqlVariables={[]}
@ -78,16 +79,45 @@ describe('IdentifierControlForm', () => {
expect(growSwitch).not.toBeChecked();
});
it('should be able to change in value type', async () => {
const { findByTestId } = render(
<IntlProvider locale="en">
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FIELDS}
queryString="FROM foo | STATS BY"
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
esqlVariables={[]}
/>
</IntlProvider>
);
// variable name input should be rendered and with the default value
expect(await findByTestId('esqlVariableName')).toHaveValue('??field');
// change the variable name to ?value
const variableNameInput = await findByTestId('esqlVariableName');
fireEvent.change(variableNameInput, { target: { value: '?value' } });
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
const controlTypeInputPopover = await findByTestId('esqlControlTypeInputPopover');
expect(within(controlTypeInputPopover).getByRole('combobox')).toHaveValue(`Static values`);
// values dropdown should be rendered
const valuesOptionsDropdown = await findByTestId('esqlValuesOptions');
expect(valuesOptionsDropdown).toBeInTheDocument();
});
it('should call the onCreateControl callback, if no initialState is given', async () => {
const onCreateControlSpy = jest.fn();
const { findByTestId, findByTitle } = render(
<IntlProvider locale="en">
<IdentifierControlForm
variableType={ESQLVariableType.FIELDS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FIELDS}
queryString="FROM foo | STATS BY"
onCreateControl={onCreateControlSpy}
onSaveControl={onCreateControlSpy}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
esqlVariables={[]}
@ -110,13 +140,12 @@ describe('IdentifierControlForm', () => {
const onCancelControlSpy = jest.fn();
const { findByTestId } = render(
<IntlProvider locale="en">
<IdentifierControlForm
variableType={ESQLVariableType.FIELDS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FIELDS}
queryString="FROM foo | STATS BY"
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
onCancelControl={onCancelControlSpy}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
esqlVariables={[]}
@ -142,12 +171,12 @@ describe('IdentifierControlForm', () => {
} as ESQLControlState;
const { findByTestId } = render(
<IntlProvider locale="en">
<IdentifierControlForm
variableType={ESQLVariableType.FIELDS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FIELDS}
queryString="FROM foo | STATS BY"
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
initialState={initialState}
@ -194,12 +223,12 @@ describe('IdentifierControlForm', () => {
const onEditControlSpy = jest.fn();
const { findByTestId, findByTitle } = render(
<IntlProvider locale="en">
<IdentifierControlForm
variableType={ESQLVariableType.FIELDS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FIELDS}
queryString="FROM foo | STATS BY"
onCreateControl={jest.fn()}
onSaveControl={onEditControlSpy}
closeFlyout={jest.fn()}
onEditControl={onEditControlSpy}
onCancelControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 19, lineNumber: 1 } as monaco.Position}
initialState={initialState}
@ -224,12 +253,12 @@ describe('IdentifierControlForm', () => {
it('should default correctly if no initial state is given', async () => {
const { findByTestId, findByTitle } = render(
<IntlProvider locale="en">
<IdentifierControlForm
variableType={ESQLVariableType.FUNCTIONS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.FUNCTIONS}
queryString="FROM foo | STATS "
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
cursorPosition={{ column: 17, lineNumber: 1 } as monaco.Position}
esqlVariables={[]}

View file

@ -7,86 +7,46 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import useMountedState from 'react-use/lib/useMountedState';
import { i18n } from '@kbn/i18n';
import { isEqual } from 'lodash';
import {
EuiComboBox,
EuiFormRow,
EuiFlyoutBody,
type EuiSwitchEvent,
type EuiComboBoxOptionOption,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { monaco } from '@kbn/monaco';
import type { ISearchGeneric } from '@kbn/search-types';
import { ESQLVariableType, type ESQLControlVariable } from '@kbn/esql-types';
import { ESQLVariableType } from '@kbn/esql-types';
import { aggFunctionDefinitions } from '@kbn/esql-validation-autocomplete';
import { getESQLQueryColumnsRaw } from '@kbn/esql-utils';
import type { ESQLControlState, ControlWidthOptions } from '../types';
import {
Header,
Footer,
ControlWidth,
ControlType,
VariableName,
ControlLabel,
} from './shared_form_components';
import {
getRecurrentVariableName,
getFlyoutStyling,
getQueryForFields,
validateVariableName,
getVariablePrefix,
getVariableTypeFromQuery,
} from './helpers';
import { ControlWidth, ControlLabel } from './shared_form_components';
import { getQueryForFields } from './helpers';
import { EsqlControlType } from '../types';
interface IdentifierControlFormProps {
search: ISearchGeneric;
variableType: ESQLVariableType;
variableName: string;
queryString: string;
esqlVariables: ESQLControlVariable[];
closeFlyout: () => void;
onCreateControl: (state: ESQLControlState, variableName: string) => void;
onEditControl: (state: ESQLControlState) => void;
setControlState: (state: ESQLControlState) => void;
cursorPosition?: monaco.Position;
initialState?: ESQLControlState;
onCancelControl?: () => void;
}
const IDENTIFIER_VARIABLE_PREFIX = '??';
export function IdentifierControlForm({
variableType,
variableName,
initialState,
queryString,
esqlVariables,
cursorPosition,
onCreateControl,
onEditControl,
onCancelControl,
setControlState,
search,
closeFlyout,
}: IdentifierControlFormProps) {
const isMounted = useMountedState();
const suggestedVariableName = useMemo(() => {
const existingVariables = new Set(
esqlVariables
.filter((variable) => variable.type === variableType)
.map((variable) => variable.key)
);
if (initialState) {
return `${IDENTIFIER_VARIABLE_PREFIX}${initialState.variableName}`;
}
const variablePrefix = getVariablePrefix(variableType);
return `${IDENTIFIER_VARIABLE_PREFIX}${getRecurrentVariableName(
variablePrefix,
existingVariables
)}`;
}, [esqlVariables, initialState, variableType]);
const [availableIdentifiersOptions, setAvailableIdentifiersOptions] = useState<
EuiComboBoxOptionOption[]
@ -103,14 +63,10 @@ export function IdentifierControlForm({
})
: []
);
const [formIsInvalid, setFormIsInvalid] = useState(false);
const [variableName, setVariableName] = useState(suggestedVariableName);
const [label, setLabel] = useState(initialState?.title ?? '');
const [minimumWidth, setMinimumWidth] = useState(initialState?.width ?? 'medium');
const [grow, setGrow] = useState(initialState?.grow ?? false);
const isControlInEditMode = useMemo(() => !!initialState, [initialState]);
useEffect(
function initAvailableIdentifiersOptions() {
if (availableIdentifiersOptions.length > 0) return;
@ -154,27 +110,10 @@ export function IdentifierControlForm({
]
);
useEffect(() => {
const variableExists =
esqlVariables.some(
(variable) => variable.key === variableName.replace(IDENTIFIER_VARIABLE_PREFIX, '')
) && !isControlInEditMode;
setFormIsInvalid(!selectedIdentifiers.length || !variableName || variableExists);
}, [esqlVariables, isControlInEditMode, selectedIdentifiers.length, variableName]);
const onIdentifiersChange = useCallback((selectedOptions: EuiComboBoxOptionOption[]) => {
setSelectedIdentifiers(selectedOptions);
}, []);
const onVariableNameChange = useCallback(
(e: { target: { value: React.SetStateAction<string> } }) => {
const text = validateVariableName(String(e.target.value), IDENTIFIER_VARIABLE_PREFIX);
setVariableName(text);
},
[]
);
const onLabelChange = useCallback((e: { target: { value: React.SetStateAction<string> } }) => {
setLabel(e.target.value);
}, []);
@ -216,8 +155,8 @@ export function IdentifierControlForm({
[]
);
const onCreateFieldControl = useCallback(async () => {
const availableOptions = selectedIdentifiers.map((field) => field.label);
useEffect(() => {
const availableOptions = selectedIdentifiers.map((value) => value.label);
// removes the double question mark from the variable name
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
const state = {
@ -226,92 +165,58 @@ export function IdentifierControlForm({
width: minimumWidth,
title: label || variableNameWithoutQuestionmark,
variableName: variableNameWithoutQuestionmark,
variableType: getVariableTypeFromQuery(variableName, variableType),
controlType: EsqlControlType.STATIC_VALUES,
variableType,
esqlQuery: queryString,
controlType: EsqlControlType.STATIC_VALUES,
grow,
};
if (availableOptions.length) {
if (!isControlInEditMode) {
await onCreateControl(state, variableName);
} else {
onEditControl(state);
}
if (!isEqual(state, initialState)) {
setControlState(state);
}
closeFlyout();
}, [
selectedIdentifiers,
minimumWidth,
grow,
initialState,
label,
minimumWidth,
queryString,
selectedIdentifiers,
setControlState,
variableName,
variableType,
queryString,
grow,
isControlInEditMode,
closeFlyout,
onCreateControl,
onEditControl,
]);
const styling = useMemo(() => getFlyoutStyling(), []);
return (
<>
<Header isInEditMode={isControlInEditMode} />
<EuiFlyoutBody
css={css`
${styling}
`}
<EuiFormRow
label={i18n.translate('esql.flyout.values.label', {
defaultMessage: 'Values',
})}
fullWidth
>
<ControlType isDisabled initialControlFlyoutType={EsqlControlType.STATIC_VALUES} />
<VariableName
variableName={variableName}
isControlInEditMode={isControlInEditMode}
onVariableNameChange={onVariableNameChange}
esqlVariables={esqlVariables}
/>
<EuiFormRow
label={i18n.translate('esql.flyout.values.label', {
defaultMessage: 'Values',
<EuiComboBox
aria-label={i18n.translate('esql.flyout.fieldsOptions.placeholder', {
defaultMessage: 'Select or add values',
})}
placeholder={i18n.translate('esql.flyout.fieldsOptions.placeholder', {
defaultMessage: 'Select or add values',
})}
options={availableIdentifiersOptions}
selectedOptions={selectedIdentifiers}
onChange={onIdentifiersChange}
onCreateOption={onCreateOption}
data-test-subj="esqlIdentifiersOptions"
fullWidth
>
<EuiComboBox
aria-label={i18n.translate('esql.flyout.fieldsOptions.placeholder', {
defaultMessage: 'Select or add values',
})}
placeholder={i18n.translate('esql.flyout.fieldsOptions.placeholder', {
defaultMessage: 'Select or add values',
})}
options={availableIdentifiersOptions}
selectedOptions={selectedIdentifiers}
onChange={onIdentifiersChange}
onCreateOption={onCreateOption}
data-test-subj="esqlIdentifiersOptions"
fullWidth
compressed
/>
</EuiFormRow>
<ControlLabel label={label} onLabelChange={onLabelChange} />
<ControlWidth
minimumWidth={minimumWidth}
grow={grow}
onMinimumSizeChange={onMinimumSizeChange}
onGrowChange={onGrowChange}
compressed
/>
</EuiFlyoutBody>
<Footer
isControlInEditMode={isControlInEditMode}
variableName={variableName}
onCancelControl={onCancelControl}
isSaveDisabled={formIsInvalid}
closeFlyout={closeFlyout}
onCreateControl={onCreateFieldControl}
</EuiFormRow>
<ControlLabel label={label} onLabelChange={onLabelChange} />
<ControlWidth
minimumWidth={minimumWidth}
grow={grow}
onMinimumSizeChange={onMinimumSizeChange}
onGrowChange={onGrowChange}
/>
</>
);

View file

@ -7,18 +7,32 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback } from 'react';
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { EuiFlyoutBody } from '@elastic/eui';
import { css } from '@emotion/react';
import { ESQLVariableType, type ESQLControlVariable } from '@kbn/esql-types';
import { getValuesFromQueryField } from '@kbn/esql-utils';
import type { ISearchGeneric } from '@kbn/search-types';
import { monaco } from '@kbn/monaco';
import type { ESQLControlState } from '../types';
import { type ESQLControlState, EsqlControlType, VariableNamePrefix } from '../types';
import { ValueControlForm } from './value_control_form';
import { Header, ControlType, VariableName, Footer } from './shared_form_components';
import { IdentifierControlForm } from './identifier_control_form';
import { updateQueryStringWithVariable } from './helpers';
import {
updateQueryStringWithVariable,
getFlyoutStyling,
getVariableSuggestion,
getRecurrentVariableName,
validateVariableName,
areValuesIntervalsValid,
getVariableTypeFromQuery,
getVariableNamePrefix,
checkVariableExistence,
} from './helpers';
interface ESQLControlsFlyoutProps {
search: ISearchGeneric;
variableType: ESQLVariableType;
initialVariableType: ESQLVariableType;
queryString: string;
esqlVariables: ESQLControlVariable[];
onSaveControl?: (controlState: ESQLControlState, updatedQuery: string) => Promise<void>;
@ -30,7 +44,7 @@ interface ESQLControlsFlyoutProps {
export function ESQLControlsFlyout({
search,
variableType,
initialVariableType,
queryString,
esqlVariables,
onSaveControl,
@ -39,58 +53,177 @@ export function ESQLControlsFlyout({
initialState,
closeFlyout,
}: ESQLControlsFlyoutProps) {
const onCreateControl = useCallback(
async (state: ESQLControlState, variableName: string) => {
if (cursorPosition) {
const query = updateQueryStringWithVariable(queryString, variableName, cursorPosition);
// ?? or ?
const [variableNamePrefix, setVariableNamePrefix] = useState(
getVariableNamePrefix(initialVariableType)
);
const valuesField = useMemo(() => {
if (initialVariableType === ESQLVariableType.VALUES) {
return getValuesFromQueryField(queryString);
}
return undefined;
}, [initialVariableType, queryString]);
await onSaveControl?.(state, query);
const isControlInEditMode = useMemo(() => !!initialState, [initialState]);
const styling = useMemo(() => getFlyoutStyling(), []);
const suggestedVariableName = useMemo(() => {
const existingVariables = new Set(
esqlVariables
.filter((variable) => variable.type === initialVariableType)
.map((variable) => variable.key)
);
if (initialState) {
return `${variableNamePrefix}${initialState.variableName}`;
}
let variableNameSuggestion = getVariableSuggestion(initialVariableType);
if (valuesField && initialVariableType === ESQLVariableType.VALUES) {
// variables names can't have special characters, only underscore
const fieldVariableName = valuesField.replace(/[^a-zA-Z0-9]/g, '_');
variableNameSuggestion = fieldVariableName;
}
return `${variableNamePrefix}${getRecurrentVariableName(
variableNameSuggestion,
existingVariables
)}`;
}, [esqlVariables, initialState, valuesField, variableNamePrefix, initialVariableType]);
const [controlFlyoutType, setControlFlyoutType] = useState<EsqlControlType>(
initialState?.controlType ??
(initialVariableType === ESQLVariableType.VALUES
? EsqlControlType.VALUES_FROM_QUERY
: EsqlControlType.STATIC_VALUES)
);
const [variableName, setVariableName] = useState(suggestedVariableName);
const [variableType, setVariableType] = useState<ESQLVariableType>(initialVariableType);
const [formIsInvalid, setFormIsInvalid] = useState(false);
const [controlState, setControlState] = useState<ESQLControlState | undefined>(initialState);
const areValuesValid = useMemo(() => {
const available = controlState?.availableOptions ?? [];
return variableType === ESQLVariableType.TIME_LITERAL
? areValuesIntervalsValid(available.map((option) => option))
: true;
}, [variableType, controlState?.availableOptions]);
const onVariableNameChange = useCallback(
(e: { target: { value: React.SetStateAction<string> } }) => {
const text = validateVariableName(String(e.target.value), variableNamePrefix);
setVariableName(text);
const newType = getVariableTypeFromQuery(text, variableType);
setVariableType(newType);
setVariableNamePrefix(getVariableNamePrefix(newType));
if (
controlFlyoutType === EsqlControlType.VALUES_FROM_QUERY &&
newType !== ESQLVariableType.VALUES
) {
setControlFlyoutType(EsqlControlType.STATIC_VALUES);
}
},
[cursorPosition, onSaveControl, queryString]
[controlFlyoutType, variableNamePrefix, variableType]
);
const onEditControl = useCallback(
async (state: ESQLControlState) => {
await onSaveControl?.(state, '');
},
[onSaveControl]
);
useEffect(() => {
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
const variableExists = checkVariableExistence(esqlVariables, variableName);
setFormIsInvalid(
!variableNameWithoutQuestionmark ||
variableExists ||
!areValuesValid ||
!controlState?.availableOptions.length
);
}, [
areValuesValid,
controlState?.availableOptions.length,
esqlVariables,
variableName,
variableType,
]);
if (variableType === ESQLVariableType.VALUES || variableType === ESQLVariableType.TIME_LITERAL) {
return (
const onFlyoutTypeChange = useCallback((controlType: EsqlControlType) => {
setControlFlyoutType(controlType);
}, []);
const onCreateControl = useCallback(async () => {
if (controlState && controlState.availableOptions.length) {
if (!isControlInEditMode) {
if (cursorPosition) {
const query = updateQueryStringWithVariable(queryString, variableName, cursorPosition);
await onSaveControl?.(controlState, query);
}
} else {
await onSaveControl?.(controlState, '');
}
}
closeFlyout();
}, [
controlState,
closeFlyout,
isControlInEditMode,
cursorPosition,
queryString,
variableName,
onSaveControl,
]);
const formBody =
variableNamePrefix === VariableNamePrefix.VALUE ? (
<ValueControlForm
queryString={queryString}
esqlVariables={esqlVariables}
variableName={variableName}
controlFlyoutType={controlFlyoutType}
variableType={variableType}
closeFlyout={closeFlyout}
onCancelControl={onCancelControl}
initialState={initialState}
onCreateControl={onCreateControl}
onEditControl={onEditControl}
setControlState={setControlState}
search={search}
cursorPosition={cursorPosition}
valuesRetrieval={valuesField}
/>
);
} else if (
variableType === ESQLVariableType.FIELDS ||
variableType === ESQLVariableType.FUNCTIONS
) {
return (
) : (
<IdentifierControlForm
variableType={variableType}
esqlVariables={esqlVariables}
variableName={variableName}
queryString={queryString}
onCancelControl={onCancelControl}
onCreateControl={onCreateControl}
onEditControl={onEditControl}
setControlState={setControlState}
initialState={initialState}
closeFlyout={closeFlyout}
search={search}
cursorPosition={cursorPosition}
/>
);
}
return null;
return (
<>
<Header isInEditMode={isControlInEditMode} />
<EuiFlyoutBody
css={css`
${styling}
`}
>
<ControlType
isDisabled={variableType !== ESQLVariableType.VALUES}
initialControlFlyoutType={controlFlyoutType}
onFlyoutTypeChange={onFlyoutTypeChange}
/>
<VariableName
variableName={variableName}
isControlInEditMode={isControlInEditMode}
onVariableNameChange={onVariableNameChange}
esqlVariables={esqlVariables}
/>
{formBody}
</EuiFlyoutBody>
<Footer
isControlInEditMode={isControlInEditMode}
variableName={variableName}
onCancelControl={onCancelControl}
isSaveDisabled={formIsInvalid}
closeFlyout={closeFlyout}
onCreateControl={onCreateControl}
/>
</>
);
}

View file

@ -36,6 +36,7 @@ import {
EuiCode,
} from '@elastic/eui';
import { EsqlControlType } from '../types';
import { checkVariableExistence } from './helpers';
const controlTypeOptions = [
{
@ -171,13 +172,21 @@ export function VariableName({
}}
/>
);
const isDisabledTooltipText = i18n.translate('esql.flyout.variableName.disabledTooltip', {
defaultMessage: 'You cant edit a control name after its been created.',
});
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
const variableExists =
esqlVariables.some((variable) => variable.key === variableName.replace('?', '')) &&
!isControlInEditMode;
checkVariableExistence(esqlVariables, variableName) && !isControlInEditMode;
const errorMessage = !variableNameWithoutQuestionmark
? i18n.translate('esql.flyout.variableName.error', {
defaultMessage: 'Variable name is required',
})
: variableExists
? i18n.translate('esql.flyout.variableNameExists.error', {
defaultMessage: 'Variable name already exists',
})
: undefined;
return (
<EuiFormRow
label={i18n.translate('esql.flyout.variableName.label', {
@ -186,18 +195,8 @@ export function VariableName({
helpText={helpText}
fullWidth
autoFocus
isInvalid={!variableName || variableExists}
error={
!variableName
? i18n.translate('esql.flyout.variableName.error', {
defaultMessage: 'Variable name is required',
})
: variableExists
? i18n.translate('esql.flyout.variableNameExists.error', {
defaultMessage: 'Variable name already exists',
})
: undefined
}
isInvalid={!variableNameWithoutQuestionmark || variableExists}
error={errorMessage}
>
<EuiToolTip
content={isControlInEditMode ? isDisabledTooltipText : tooltipContent}
@ -210,6 +209,7 @@ export function VariableName({
placeholder={i18n.translate('esql.flyout.variableName.placeholder', {
defaultMessage: 'Set a variable name',
})}
disabled={isControlInEditMode}
value={variableName}
onChange={onVariableNameChange}
aria-label={i18n.translate('esql.flyout.variableName.placeholder', {
@ -217,7 +217,6 @@ export function VariableName({
})}
data-test-subj="esqlVariableName"
fullWidth
disabled={isControlInEditMode}
compressed
/>
</EuiToolTip>

View file

@ -11,11 +11,12 @@ import React from 'react';
import { render, within, fireEvent } from '@testing-library/react';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { IUiSettingsClient } from '@kbn/core/public';
import { monaco } from '@kbn/monaco';
import { coreMock } from '@kbn/core/server/mocks';
import { ESQLVariableType } from '@kbn/esql-types';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { ValueControlForm } from './value_control_form';
import { ESQLControlsFlyout } from '.';
import { EsqlControlType, ESQLControlState } from '../types';
jest.mock('@kbn/esql-utils', () => {
@ -38,6 +39,7 @@ jest.mock('@kbn/esql-utils', () => {
getLimitFromESQLQuery: jest.fn().mockReturnValue(1000),
isQueryWrappedByPipes: jest.fn().mockReturnValue(false),
getValuesFromQueryField: jest.fn().mockReturnValue('field'),
getESQLQueryColumnsRaw: jest.fn().mockResolvedValue([{ name: 'column1' }, { name: 'column2' }]),
};
});
@ -63,12 +65,12 @@ describe('ValueControlForm', () => {
it('should default correctly if no initial state is given for an interval variable type', async () => {
const { findByTestId, findByTitle } = render(
<IntlProvider locale="en">
<ValueControlForm
variableType={ESQLVariableType.TIME_LITERAL}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.TIME_LITERAL}
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
esqlVariables={[]}
/>
@ -111,14 +113,15 @@ describe('ValueControlForm', () => {
const onCreateControlSpy = jest.fn();
const { findByTestId, findByTitle } = render(
<IntlProvider locale="en">
<ValueControlForm
variableType={ESQLVariableType.TIME_LITERAL}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.TIME_LITERAL}
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
onCreateControl={onCreateControlSpy}
onSaveControl={onCreateControlSpy}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
esqlVariables={[]}
cursorPosition={{ lineNumber: 1, column: 1 } as monaco.Position}
/>
</IntlProvider>
);
@ -138,13 +141,12 @@ describe('ValueControlForm', () => {
const onCancelControlSpy = jest.fn();
const { findByTestId } = render(
<IntlProvider locale="en">
<ValueControlForm
variableType={ESQLVariableType.TIME_LITERAL}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.TIME_LITERAL}
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
onCreateControl={jest.fn()}
onCancelControl={onCancelControlSpy}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={onCancelControlSpy}
search={searchMock}
esqlVariables={[]}
/>
@ -169,12 +171,12 @@ describe('ValueControlForm', () => {
} as ESQLControlState;
const { findByTestId } = render(
<IntlProvider locale="en">
<ValueControlForm
variableType={ESQLVariableType.TIME_LITERAL}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.TIME_LITERAL}
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
initialState={initialState}
esqlVariables={[]}
@ -220,15 +222,16 @@ describe('ValueControlForm', () => {
const onEditControlSpy = jest.fn();
const { findByTestId } = render(
<IntlProvider locale="en">
<ValueControlForm
variableType={ESQLVariableType.FIELDS}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.TIME_LITERAL}
queryString="FROM foo | STATS BY BUCKET(@timestamp,)"
onCreateControl={jest.fn()}
onSaveControl={onEditControlSpy}
closeFlyout={jest.fn()}
onEditControl={onEditControlSpy}
onCancelControl={jest.fn()}
search={searchMock}
initialState={initialState}
esqlVariables={[]}
cursorPosition={{ lineNumber: 1, column: 1 } as monaco.Position}
/>
</IntlProvider>
);
@ -242,12 +245,12 @@ describe('ValueControlForm', () => {
const { findByTestId } = render(
<KibanaContextProvider services={services}>
<IntlProvider locale="en">
<ValueControlForm
variableType={ESQLVariableType.VALUES}
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.VALUES}
queryString="FROM foo | WHERE field =="
onCreateControl={jest.fn()}
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onEditControl={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
esqlVariables={[]}
/>
@ -264,6 +267,34 @@ describe('ValueControlForm', () => {
// values preview panel should be rendered
expect(await findByTestId('esqlValuesPreview')).toBeInTheDocument();
});
it('should be able to change in fields type', async () => {
const { findByTestId } = render(
<IntlProvider locale="en">
<ESQLControlsFlyout
initialVariableType={ESQLVariableType.VALUES}
queryString="FROM foo | WHERE field =="
onSaveControl={jest.fn()}
closeFlyout={jest.fn()}
onCancelControl={jest.fn()}
search={searchMock}
esqlVariables={[]}
/>
</IntlProvider>
);
// variable name input should be rendered and with the default value
expect(await findByTestId('esqlVariableName')).toHaveValue('?field');
// change the variable name to ?value
const variableNameInput = await findByTestId('esqlVariableName');
fireEvent.change(variableNameInput, { target: { value: '??field' } });
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
const controlTypeInputPopover = await findByTestId('esqlControlTypeInputPopover');
expect(within(controlTypeInputPopover).getByRole('combobox')).toHaveValue(`Static values`);
// identifiers dropdown should be rendered
const identifiersOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
expect(identifiersOptionsDropdown).toBeInTheDocument();
});
});
});
});

View file

@ -7,14 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import useMountedState from 'react-use/lib/useMountedState';
import { i18n } from '@kbn/i18n';
import { isEqual } from 'lodash';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
EuiFlyoutBody,
EuiCallOut,
type EuiSwitchEvent,
EuiPanel,
@ -23,98 +23,41 @@ import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ISearchGeneric } from '@kbn/search-types';
import { ESQLVariableType } from '@kbn/esql-types';
import { ESQLControlVariable } from '@kbn/esql-types';
import { monaco } from '@kbn/monaco';
import {
getIndexPatternFromESQLQuery,
getESQLResults,
appendStatsByToQuery,
getValuesFromQueryField,
} from '@kbn/esql-utils';
import { ESQLLangEditor } from '../../../create_editor';
import type { ESQLControlState, ControlWidthOptions } from '../types';
import {
Header,
Footer,
ControlWidth,
ControlType,
VariableName,
ControlLabel,
} from './shared_form_components';
import {
getRecurrentVariableName,
getFlyoutStyling,
areValuesIntervalsValid,
validateVariableName,
getVariablePrefix,
getVariableTypeFromQuery,
} from './helpers';
import { ControlWidth, ControlLabel } from './shared_form_components';
import { EsqlControlType } from '../types';
import { ChooseColumnPopover } from './choose_column_popover';
interface ValueControlFormProps {
search: ISearchGeneric;
variableType: ESQLVariableType;
variableName: string;
controlFlyoutType: EsqlControlType;
queryString: string;
esqlVariables: ESQLControlVariable[];
closeFlyout: () => void;
onCreateControl: (state: ESQLControlState, variableName: string) => void;
onEditControl: (state: ESQLControlState) => void;
onCancelControl?: () => void;
setControlState: (state: ESQLControlState) => void;
initialState?: ESQLControlState;
cursorPosition?: monaco.Position;
valuesRetrieval?: string;
}
const SUGGESTED_INTERVAL_VALUES = ['5 minutes', '1 hour', '1 day', '1 week', '1 month'];
const VALUE_VARIABLE_PREFIX = '?';
export function ValueControlForm({
variableType,
initialState,
onCancelControl,
queryString,
esqlVariables,
variableName,
controlFlyoutType,
search,
closeFlyout,
onCreateControl,
onEditControl,
cursorPosition,
setControlState,
valuesRetrieval,
}: ValueControlFormProps) {
const isMounted = useMountedState();
const valuesField = useMemo(() => {
if (variableType === ESQLVariableType.VALUES) {
return getValuesFromQueryField(queryString, cursorPosition);
}
return null;
}, [variableType, queryString, cursorPosition]);
const suggestedVariableName = useMemo(() => {
const existingVariables = new Set(
esqlVariables
.filter((variable) => variable.type === variableType)
.map((variable) => variable.key)
);
if (initialState) {
return `${VALUE_VARIABLE_PREFIX}${initialState.variableName}`;
}
let variablePrefix = getVariablePrefix(variableType);
if (valuesField && variableType === ESQLVariableType.VALUES) {
// variables names can't have special characters, only underscore
const fieldVariableName = valuesField.replace(/[^a-zA-Z0-9]/g, '_');
variablePrefix = fieldVariableName;
}
return `${VALUE_VARIABLE_PREFIX}${getRecurrentVariableName(variablePrefix, existingVariables)}`;
}, [esqlVariables, initialState, valuesField, variableType]);
const [controlFlyoutType, setControlFlyoutType] = useState<EsqlControlType>(
initialState?.controlType ??
(variableType === ESQLVariableType.TIME_LITERAL
? EsqlControlType.STATIC_VALUES
: EsqlControlType.VALUES_FROM_QUERY)
);
const [availableValuesOptions, setAvailableValuesOptions] = useState<EuiComboBoxOptionOption[]>(
variableType === ESQLVariableType.TIME_LITERAL
@ -144,49 +87,17 @@ export function ValueControlForm({
variableType === ESQLVariableType.VALUES ? initialState?.esqlQuery ?? '' : ''
);
const [esqlQueryErrors, setEsqlQueryErrors] = useState<Error[] | undefined>();
const [formIsInvalid, setFormIsInvalid] = useState(false);
const [queryColumns, setQueryColumns] = useState<string[]>(valuesField ? [valuesField] : []);
const [variableName, setVariableName] = useState(suggestedVariableName);
const [queryColumns, setQueryColumns] = useState<string[]>(
valuesRetrieval ? [valuesRetrieval] : []
);
const [label, setLabel] = useState(initialState?.title ?? '');
const [minimumWidth, setMinimumWidth] = useState(initialState?.width ?? 'medium');
const [grow, setGrow] = useState(initialState?.grow ?? false);
const isControlInEditMode = useMemo(() => !!initialState, [initialState]);
const areValuesValid = useMemo(() => {
return variableType === ESQLVariableType.TIME_LITERAL
? areValuesIntervalsValid(selectedValues.map((option) => option.label))
: true;
}, [variableType, selectedValues]);
useEffect(() => {
const variableExists =
esqlVariables.some((variable) => variable.key === variableName.replace('?', '')) &&
!isControlInEditMode;
setFormIsInvalid(!variableName || variableExists || !areValuesValid || !selectedValues.length);
}, [
areValuesValid,
esqlVariables,
isControlInEditMode,
selectedValues.length,
valuesQuery,
variableName,
]);
const onValuesChange = useCallback((selectedOptions: EuiComboBoxOptionOption[]) => {
setSelectedValues(selectedOptions);
}, []);
const onFlyoutTypeChange = useCallback(
(type: EsqlControlType) => {
setControlFlyoutType(type);
if (type !== controlFlyoutType && variableType === ESQLVariableType.TIME_LITERAL) {
setSelectedValues([]);
}
},
[controlFlyoutType, variableType]
);
const onCreateOption = useCallback(
(searchValue: string, flattenedOptions: EuiComboBoxOptionOption[] = []) => {
if (!searchValue) {
@ -214,14 +125,6 @@ export function ValueControlForm({
[availableValuesOptions]
);
const onVariableNameChange = useCallback(
(e: { target: { value: React.SetStateAction<string> } }) => {
const text = validateVariableName(String(e.target.value), VALUE_VARIABLE_PREFIX);
setVariableName(text);
},
[]
);
const onLabelChange = useCallback((e: { target: { value: React.SetStateAction<string> } }) => {
setLabel(e.target.value);
}, []);
@ -280,11 +183,11 @@ export function ValueControlForm({
if (
!selectedValues?.length &&
controlFlyoutType === EsqlControlType.VALUES_FROM_QUERY &&
valuesField
valuesRetrieval
) {
const queryForValues =
suggestedVariableName !== ''
? `FROM ${getIndexPatternFromESQLQuery(queryString)} | STATS BY ${valuesField}`
variableName !== ''
? `FROM ${getIndexPatternFromESQLQuery(queryString)} | STATS BY ${valuesRetrieval}`
: '';
onValuesQuerySubmit(queryForValues);
}
@ -293,12 +196,11 @@ export function ValueControlForm({
onValuesQuerySubmit,
queryString,
selectedValues?.length,
suggestedVariableName,
valuesField,
valuesRetrieval,
variableName,
]);
const onCreateValueControl = useCallback(async () => {
useEffect(() => {
const availableOptions = selectedValues.map((value) => value.label);
// removes the question mark from the variable name
const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, '');
@ -308,34 +210,26 @@ export function ValueControlForm({
width: minimumWidth,
title: label || variableNameWithoutQuestionmark,
variableName: variableNameWithoutQuestionmark,
variableType: getVariableTypeFromQuery(variableName, variableType),
variableType,
esqlQuery: valuesQuery || queryString,
controlType: controlFlyoutType,
grow,
};
if (availableOptions.length) {
if (!isControlInEditMode) {
await onCreateControl(state, variableName);
} else {
onEditControl(state);
}
if (!isEqual(state, initialState)) {
setControlState(state);
}
closeFlyout();
}, [
selectedValues,
controlFlyoutType,
minimumWidth,
grow,
initialState,
label,
minimumWidth,
queryString,
selectedValues,
setControlState,
valuesQuery,
variableName,
variableType,
valuesQuery,
queryString,
grow,
closeFlyout,
isControlInEditMode,
onCreateControl,
onEditControl,
]);
const updateQuery = useCallback(
@ -346,149 +240,119 @@ export function ValueControlForm({
[onValuesQuerySubmit, valuesQuery]
);
const styling = useMemo(() => getFlyoutStyling(), []);
return (
<>
<Header isInEditMode={isControlInEditMode} />
<EuiFlyoutBody
css={css`
${styling}
`}
>
<ControlType
isDisabled={false}
initialControlFlyoutType={controlFlyoutType}
onFlyoutTypeChange={onFlyoutTypeChange}
/>
<VariableName
variableName={variableName}
isControlInEditMode={isControlInEditMode}
onVariableNameChange={onVariableNameChange}
esqlVariables={esqlVariables}
/>
{controlFlyoutType === EsqlControlType.VALUES_FROM_QUERY && (
<>
<EuiFormRow
label={i18n.translate('esql.flyout.valuesQueryEditor.label', {
defaultMessage: 'Values query',
})}
fullWidth
>
<ESQLLangEditor
query={{ esql: valuesQuery }}
onTextLangQueryChange={(q) => {
setValuesQuery(q.esql);
}}
hideTimeFilterInfo={true}
disableAutoFocus={true}
errors={esqlQueryErrors}
editorIsInline
hideRunQueryText
onTextLangQuerySubmit={async (q, a) => {
if (q) {
await onValuesQuerySubmit(q.esql);
}
}}
isDisabled={false}
isLoading={false}
/>
</EuiFormRow>
{queryColumns.length > 0 && (
<EuiFormRow
label={i18n.translate('esql.flyout.previewValues.placeholder', {
defaultMessage: 'Values preview',
})}
fullWidth
>
{queryColumns.length === 1 ? (
<EuiPanel
paddingSize="s"
color="primary"
css={css`
white-space: wrap;
overflow-y: auto;
max-height: 200px;
`}
data-test-subj="esqlValuesPreview"
>
{selectedValues.map((value) => value.label).join(', ')}
</EuiPanel>
) : (
<EuiCallOut
title={i18n.translate('esql.flyout.displayMultipleColsCallout.title', {
defaultMessage: 'Your query must return a single column',
})}
color="warning"
iconType="warning"
size="s"
data-test-subj="esqlMoreThanOneColumnCallout"
>
<p>
<FormattedMessage
id="esql.flyout.displayMultipleColsCallout.description"
defaultMessage="Your query is currently returning {totalColumns} columns. Choose column {chooseColumnPopover} or use {boldText}."
values={{
totalColumns: queryColumns.length,
boldText: <strong>STATS BY</strong>,
chooseColumnPopover: (
<ChooseColumnPopover columns={queryColumns} updateQuery={updateQuery} />
),
}}
/>
</p>
</EuiCallOut>
)}
</EuiFormRow>
)}
</>
)}
{controlFlyoutType === EsqlControlType.STATIC_VALUES && (
{controlFlyoutType === EsqlControlType.VALUES_FROM_QUERY && (
<>
<EuiFormRow
label={i18n.translate('esql.flyout.values.label', {
defaultMessage: 'Values',
label={i18n.translate('esql.flyout.valuesQueryEditor.label', {
defaultMessage: 'Values query',
})}
fullWidth
>
<EuiComboBox
aria-label={i18n.translate('esql.flyout.values.placeholder', {
defaultMessage: 'Select or add values',
})}
placeholder={i18n.translate('esql.flyout.values.placeholder', {
defaultMessage: 'Select or add values',
})}
data-test-subj="esqlValuesOptions"
options={availableValuesOptions}
selectedOptions={selectedValues}
onChange={onValuesChange}
onCreateOption={onCreateOption}
fullWidth
compressed
css={css`
max-height: 200px;
overflow-y: auto;
`}
<ESQLLangEditor
query={{ esql: valuesQuery }}
onTextLangQueryChange={(q) => {
setValuesQuery(q.esql);
}}
hideTimeFilterInfo={true}
disableAutoFocus={true}
errors={esqlQueryErrors}
editorIsInline
hideRunQueryText
onTextLangQuerySubmit={async (q, a) => {
if (q) {
await onValuesQuerySubmit(q.esql);
}
}}
isDisabled={false}
isLoading={false}
/>
</EuiFormRow>
)}
<ControlLabel label={label} onLabelChange={onLabelChange} />
{queryColumns.length > 0 && (
<EuiFormRow
label={i18n.translate('esql.flyout.previewValues.placeholder', {
defaultMessage: 'Values preview',
})}
fullWidth
>
{queryColumns.length === 1 ? (
<EuiPanel
paddingSize="s"
color="primary"
css={css`
white-space: wrap;
overflow-y: auto;
max-height: 200px;
`}
data-test-subj="esqlValuesPreview"
>
{selectedValues.map((value) => value.label).join(', ')}
</EuiPanel>
) : (
<EuiCallOut
title={i18n.translate('esql.flyout.displayMultipleColsCallout.title', {
defaultMessage: 'Your query must return a single column',
})}
color="warning"
iconType="warning"
size="s"
data-test-subj="esqlMoreThanOneColumnCallout"
>
<p>
<FormattedMessage
id="esql.flyout.displayMultipleColsCallout.description"
defaultMessage="Your query is currently returning {totalColumns} columns. Choose column {chooseColumnPopover} or use {boldText}."
values={{
totalColumns: queryColumns.length,
boldText: <strong>STATS BY</strong>,
chooseColumnPopover: (
<ChooseColumnPopover columns={queryColumns} updateQuery={updateQuery} />
),
}}
/>
</p>
</EuiCallOut>
)}
</EuiFormRow>
)}
</>
)}
{controlFlyoutType === EsqlControlType.STATIC_VALUES && (
<EuiFormRow
label={i18n.translate('esql.flyout.values.label', {
defaultMessage: 'Values',
})}
fullWidth
>
<EuiComboBox
aria-label={i18n.translate('esql.flyout.values.placeholder', {
defaultMessage: 'Select or add values',
})}
placeholder={i18n.translate('esql.flyout.values.placeholder', {
defaultMessage: 'Select or add values',
})}
data-test-subj="esqlValuesOptions"
options={availableValuesOptions}
selectedOptions={selectedValues}
onChange={onValuesChange}
onCreateOption={onCreateOption}
fullWidth
compressed
css={css`
max-height: 200px;
overflow-y: auto;
`}
/>
</EuiFormRow>
)}
<ControlLabel label={label} onLabelChange={onLabelChange} />
<ControlWidth
minimumWidth={minimumWidth}
grow={grow}
onMinimumSizeChange={onMinimumSizeChange}
onGrowChange={onGrowChange}
/>
</EuiFlyoutBody>
<Footer
isControlInEditMode={isControlInEditMode}
variableName={variableName}
onCancelControl={onCancelControl}
isSaveDisabled={formIsInvalid}
closeFlyout={closeFlyout}
onCreateControl={onCreateValueControl}
<ControlWidth
minimumWidth={minimumWidth}
grow={grow}
onMinimumSizeChange={onMinimumSizeChange}
onGrowChange={onGrowChange}
/>
</>
);

View file

@ -72,7 +72,7 @@ export async function executeAction({
<LazyControlFlyout
queryString={queryString}
search={search}
variableType={variableType}
initialVariableType={variableType}
closeFlyout={() => {
handle.close();
}}

View file

@ -13,6 +13,11 @@ export enum EsqlControlType {
VALUES_FROM_QUERY = 'VALUES_FROM_QUERY',
}
export enum VariableNamePrefix {
IDENTIFIER = '??',
VALUE = '?',
}
export type ControlWidthOptions = 'small' | 'medium' | 'large';
export interface ESQLControlState {