mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ES|QL] Creates controls for stats functions (#210170)
## Summary Closes https://github.com/elastic/kibana/issues/207029 Allows the creation of function controls. These are only available for STATS <img width="880" alt="image" src="https://github.com/user-attachments/assets/fe57c3e5-f42a-4d9c-95b3-4a5a12938821" /> ### Release notes Allows the creation of dynamic aggregations controls for ES|QL charts. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [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 - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
159910d06f
commit
f96b68dbac
19 changed files with 502 additions and 290 deletions
|
@ -317,6 +317,19 @@ export const ESQLEditor = memo(function ESQLEditor({
|
|||
);
|
||||
});
|
||||
|
||||
monaco.editor.registerCommand('esql.control.functions.create', async (...args) => {
|
||||
const position = editor1.current?.getPosition();
|
||||
await triggerControl(
|
||||
query.esql,
|
||||
ESQLVariableType.FUNCTIONS,
|
||||
position,
|
||||
uiActions,
|
||||
esqlVariables,
|
||||
onSaveControl,
|
||||
onCancelControl
|
||||
);
|
||||
});
|
||||
|
||||
const styles = esqlEditorStyles(
|
||||
theme.euiTheme,
|
||||
editorHeight,
|
||||
|
|
|
@ -82,6 +82,11 @@ describe('run query helpers', () => {
|
|||
value: 'go',
|
||||
type: ESQLVariableType.VALUES,
|
||||
},
|
||||
{
|
||||
key: 'function',
|
||||
value: 'count',
|
||||
type: ESQLVariableType.FUNCTIONS,
|
||||
},
|
||||
];
|
||||
const params = getNamedParams(query, time, variables);
|
||||
expect(params).toStrictEqual([
|
||||
|
@ -96,6 +101,11 @@ describe('run query helpers', () => {
|
|||
{
|
||||
agent_name: 'go',
|
||||
},
|
||||
{
|
||||
function: {
|
||||
identifier: 'count',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -119,9 +129,14 @@ describe('run query helpers', () => {
|
|||
value: 'go',
|
||||
type: ESQLVariableType.VALUES,
|
||||
},
|
||||
{
|
||||
key: 'function',
|
||||
value: 'count',
|
||||
type: ESQLVariableType.FUNCTIONS,
|
||||
},
|
||||
];
|
||||
const params = getNamedParams(query, time, variables);
|
||||
expect(params).toHaveLength(5);
|
||||
expect(params).toHaveLength(6);
|
||||
expect(params[0]).toHaveProperty('_tstart');
|
||||
expect(params[1]).toHaveProperty('_tend');
|
||||
expect(params[2]).toStrictEqual({
|
||||
|
@ -135,6 +150,12 @@ describe('run query helpers', () => {
|
|||
expect(params[4]).toStrictEqual({
|
||||
agent_name: 'go',
|
||||
});
|
||||
|
||||
expect(params[5]).toStrictEqual({
|
||||
function: {
|
||||
identifier: 'count',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export const getNamedParams = (
|
|||
const namedParams: ESQLSearchParams['params'] = getStartEndParams(query, timeRange);
|
||||
if (variables?.length) {
|
||||
variables?.forEach(({ key, value, type }) => {
|
||||
if (type === ESQLVariableType.FIELDS) {
|
||||
if (type === ESQLVariableType.FIELDS || type === ESQLVariableType.FUNCTIONS) {
|
||||
namedParams.push({ [key]: { identifier: value } });
|
||||
} else {
|
||||
namedParams.push({ [key]: value });
|
||||
|
|
|
@ -68,6 +68,7 @@ export {
|
|||
} from './src/shared/helpers';
|
||||
export { ENRICH_MODES } from './src/definitions/settings';
|
||||
export { timeUnits } from './src/definitions/literals';
|
||||
export { aggregationFunctionDefinitions } from './src/definitions/generated/aggregation_functions';
|
||||
export { getFunctionSignatures } from './src/definitions/helpers';
|
||||
|
||||
export {
|
||||
|
|
|
@ -373,7 +373,55 @@ describe('autocomplete.suggest', () => {
|
|||
});
|
||||
|
||||
describe('create control suggestion', () => {
|
||||
test('suggests `Create control` option', async () => {
|
||||
test('suggests `Create control` option for aggregations', async () => {
|
||||
const { suggest } = await setup();
|
||||
|
||||
const suggestions = await suggest('FROM a | STATS /', {
|
||||
callbacks: {
|
||||
canSuggestVariables: () => true,
|
||||
getVariablesByType: () => [],
|
||||
getColumnsFor: () => Promise.resolve([{ name: 'clientip', type: 'ip' }]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(suggestions).toContainEqual({
|
||||
label: 'Create control',
|
||||
text: '',
|
||||
kind: 'Issue',
|
||||
detail: 'Click to create',
|
||||
command: { id: 'esql.control.functions.create', title: 'Click to create' },
|
||||
sortText: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('suggests `?function` option', async () => {
|
||||
const { suggest } = await setup();
|
||||
|
||||
const suggestions = await suggest('FROM a | STATS var0 = /', {
|
||||
callbacks: {
|
||||
canSuggestVariables: () => true,
|
||||
getVariablesByType: () => [
|
||||
{
|
||||
key: 'function',
|
||||
value: 'avg',
|
||||
type: ESQLVariableType.FUNCTIONS,
|
||||
},
|
||||
],
|
||||
getColumnsFor: () => Promise.resolve([{ name: 'clientip', type: 'ip' }]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(suggestions).toContainEqual({
|
||||
label: '?function',
|
||||
text: '?function',
|
||||
kind: 'Constant',
|
||||
detail: 'Named parameter',
|
||||
command: undefined,
|
||||
sortText: '1A',
|
||||
});
|
||||
});
|
||||
|
||||
test('suggests `Create control` option for grouping', async () => {
|
||||
const { suggest } = await setup();
|
||||
|
||||
const suggestions = await suggest('FROM a | STATS BY /', {
|
||||
|
@ -390,7 +438,7 @@ describe('autocomplete.suggest', () => {
|
|||
kind: 'Issue',
|
||||
detail: 'Click to create',
|
||||
command: { id: 'esql.control.fields.create', title: 'Click to create' },
|
||||
sortText: '11A',
|
||||
sortText: '11',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -407,7 +407,7 @@ describe('WHERE <expression>', () => {
|
|||
kind: 'Issue',
|
||||
detail: 'Click to create',
|
||||
command: { id: 'esql.control.values.create', title: 'Click to create' },
|
||||
sortText: '11A',
|
||||
sortText: '11',
|
||||
rangeToReplace: { start: 31, end: 31 },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -220,9 +220,10 @@ export async function suggest(
|
|||
getFieldsByType,
|
||||
getFieldsMap,
|
||||
getPolicies,
|
||||
getPolicyMetadata,
|
||||
getVariablesByType,
|
||||
resourceRetriever?.getPreferences,
|
||||
resourceRetriever
|
||||
resourceRetriever,
|
||||
supportsControls
|
||||
);
|
||||
}
|
||||
if (astContext.type === 'setting') {
|
||||
|
@ -395,9 +396,10 @@ async function getSuggestionsWithinCommandExpression(
|
|||
getColumnsByType: GetColumnsByTypeFn,
|
||||
getFieldsMap: GetFieldsMapFn,
|
||||
getPolicies: GetPoliciesFn,
|
||||
getPolicyMetadata: GetPolicyMetadataFn,
|
||||
getVariablesByType?: (type: ESQLVariableType) => ESQLControlVariable[] | undefined,
|
||||
getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined>,
|
||||
callbacks?: ESQLCallbacks
|
||||
callbacks?: ESQLCallbacks,
|
||||
supportsControls?: boolean
|
||||
) {
|
||||
const commandDef = getCommandDefinition(command.name);
|
||||
|
||||
|
@ -424,6 +426,8 @@ async function getSuggestionsWithinCommandExpression(
|
|||
getSourcesFromQuery: (type) => getSourcesFromCommands(commands, type),
|
||||
previousCommands: commands,
|
||||
callbacks,
|
||||
getVariablesByType,
|
||||
supportsControls,
|
||||
});
|
||||
} else {
|
||||
// The deprecated path.
|
||||
|
@ -434,8 +438,7 @@ async function getSuggestionsWithinCommandExpression(
|
|||
getSources,
|
||||
getColumnsByType,
|
||||
getFieldsMap,
|
||||
getPolicies,
|
||||
getPolicyMetadata
|
||||
getPolicies
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -459,8 +462,7 @@ async function getExpressionSuggestionsByType(
|
|||
getSources: () => Promise<ESQLSourceResult[]>,
|
||||
getFieldsByType: GetColumnsByTypeFn,
|
||||
getFieldsMap: GetFieldsMapFn,
|
||||
getPolicies: GetPoliciesFn,
|
||||
getPolicyMetadata: GetPolicyMetadataFn
|
||||
getPolicies: GetPoliciesFn
|
||||
) {
|
||||
const commandDef = getCommandDefinition(command.name);
|
||||
const { argIndex, prevIndex, lastArg, nodeArg } = extractArgMeta(command, node);
|
||||
|
|
|
@ -13,7 +13,9 @@ import {
|
|||
TRIGGER_SUGGESTION_COMMAND,
|
||||
getNewVariableSuggestion,
|
||||
getFunctionSuggestions,
|
||||
getControlSuggestionIfSupported,
|
||||
} from '../../factories';
|
||||
import { ESQLVariableType } from '../../../shared/types';
|
||||
import { commaCompleteItem, pipeCompleteItem } from '../../complete_items';
|
||||
import { pushItUpInTheList } from '../../helper';
|
||||
import { byCompleteItem, getDateHistogramCompletionItem, getPosition } from './util';
|
||||
|
@ -24,6 +26,8 @@ export async function suggest({
|
|||
getColumnsByType,
|
||||
getSuggestedVariableName,
|
||||
getPreferences,
|
||||
getVariablesByType,
|
||||
supportsControls,
|
||||
}: CommandSuggestParams<'stats'>): Promise<SuggestionRawDefinition[]> {
|
||||
const pos = getPosition(innerText, command);
|
||||
|
||||
|
@ -31,16 +35,22 @@ export async function suggest({
|
|||
await getColumnsByType('any', [], { advanceCursor: true, openSuggestions: true }),
|
||||
true
|
||||
);
|
||||
const controlSuggestions = getControlSuggestionIfSupported(
|
||||
Boolean(supportsControls),
|
||||
ESQLVariableType.FUNCTIONS,
|
||||
getVariablesByType
|
||||
);
|
||||
|
||||
switch (pos) {
|
||||
case 'expression_without_assignment':
|
||||
return [
|
||||
...controlSuggestions,
|
||||
...getFunctionSuggestions({ command: 'stats' }),
|
||||
getNewVariableSuggestion(getSuggestedVariableName()),
|
||||
];
|
||||
|
||||
case 'expression_after_assignment':
|
||||
return [...getFunctionSuggestions({ command: 'stats' })];
|
||||
return [...controlSuggestions, ...getFunctionSuggestions({ command: 'stats' })];
|
||||
|
||||
case 'expression_complete':
|
||||
return [
|
||||
|
|
|
@ -583,6 +583,23 @@ export function getDateLiterals(options?: {
|
|||
];
|
||||
}
|
||||
|
||||
export function getControlSuggestionIfSupported(
|
||||
supportsControls: boolean,
|
||||
type: ESQLVariableType,
|
||||
getVariablesByType?: (type: ESQLVariableType) => ESQLControlVariable[] | undefined
|
||||
) {
|
||||
if (!supportsControls) {
|
||||
return [];
|
||||
}
|
||||
const variableType = type;
|
||||
const variables = getVariablesByType?.(variableType) ?? [];
|
||||
const controlSuggestion = getControlSuggestion(
|
||||
variableType,
|
||||
variables?.map((v) => `?${v.key}`)
|
||||
);
|
||||
return controlSuggestion;
|
||||
}
|
||||
|
||||
export function getControlSuggestion(
|
||||
type: ESQLVariableType,
|
||||
variables?: string[]
|
||||
|
@ -603,7 +620,7 @@ export function getControlSuggestion(
|
|||
defaultMessage: 'Click to create',
|
||||
}
|
||||
),
|
||||
sortText: '1A',
|
||||
sortText: '1',
|
||||
command: {
|
||||
id: `esql.control.${type}.create`,
|
||||
title: i18n.translate(
|
||||
|
|
|
@ -16,7 +16,12 @@ import type {
|
|||
ESQLSource,
|
||||
} from '@kbn/esql-ast';
|
||||
import { GetColumnsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types';
|
||||
import type { ESQLCallbacks, ESQLSourceResult } from '../shared/types';
|
||||
import type {
|
||||
ESQLCallbacks,
|
||||
ESQLControlVariable,
|
||||
ESQLVariableType,
|
||||
ESQLSourceResult,
|
||||
} from '../shared/types';
|
||||
|
||||
/**
|
||||
* All supported field types in ES|QL. This is all the types
|
||||
|
@ -245,6 +250,8 @@ export interface CommandSuggestParams<CommandName extends string> {
|
|||
*/
|
||||
previousCommands?: ESQLCommand[];
|
||||
callbacks?: ESQLCallbacks;
|
||||
getVariablesByType?: (type: ESQLVariableType) => ESQLControlVariable[] | undefined;
|
||||
supportsControls?: boolean;
|
||||
}
|
||||
|
||||
export type CommandSuggestFunction<CommandName extends string> = (
|
||||
|
|
|
@ -46,6 +46,7 @@ export enum ESQLVariableType {
|
|||
TIME_LITERAL = 'time_literal',
|
||||
FIELDS = 'fields',
|
||||
VALUES = 'values',
|
||||
FUNCTIONS = 'functions',
|
||||
}
|
||||
|
||||
export interface ESQLCallbacks {
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, within, fireEvent } from '@testing-library/react';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { ESQLVariableType } from '@kbn/esql-validation-autocomplete';
|
||||
import { FieldControlForm } from './field_control_form';
|
||||
import { ESQLControlState, EsqlControlType } from '../types';
|
||||
|
||||
jest.mock('@kbn/esql-utils', () => ({
|
||||
getESQLQueryColumnsRaw: jest.fn().mockResolvedValue([{ name: 'column1' }, { name: 'column2' }]),
|
||||
}));
|
||||
|
||||
describe('FieldControlForm', () => {
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
const searchMock = dataMock.search.search;
|
||||
|
||||
it('should default correctly if no initial state is given', async () => {
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<FieldControlForm
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// control type dropdown should be rendered and default to 'STATIC_VALUES'
|
||||
// no need to test further as the control type is disabled
|
||||
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
|
||||
const controlTypeInputPopover = await findByTestId('esqlControlTypeInputPopover');
|
||||
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');
|
||||
|
||||
// fields dropdown should be rendered with available fields column1 and column2
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlFieldsOptions');
|
||||
expect(fieldsOptionsDropdown).toBeInTheDocument();
|
||||
const fieldsOptionsDropdownSearchInput = within(fieldsOptionsDropdown).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
expect(fieldsOptionsDropdownSearchInput).toHaveValue('');
|
||||
expect(await findByTitle('column1')).toBeDefined();
|
||||
expect(await findByTitle('column2')).toBeDefined();
|
||||
|
||||
// variable label input should be rendered and with the default value (empty)
|
||||
expect(await findByTestId('esqlControlLabel')).toHaveValue('');
|
||||
|
||||
// control width dropdown should be rendered and default to 'MEDIUM'
|
||||
expect(await findByTestId('esqlControlMinimumWidth')).toBeInTheDocument();
|
||||
const pressedWidth = within(await findByTestId('esqlControlMinimumWidth')).getByTitle('Medium');
|
||||
expect(pressedWidth).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// control grow switch should be rendered and default to 'false'
|
||||
expect(await findByTestId('esqlControlGrow')).toBeInTheDocument();
|
||||
const growSwitch = await findByTestId('esqlControlGrow');
|
||||
expect(growSwitch).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should call the onCreateControl callback, if no initialState is given', async () => {
|
||||
const onCreateControlSpy = jest.fn();
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<FieldControlForm
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
// select the first field
|
||||
const fieldsOptionsDropdownSearchInput = within(
|
||||
await findByTestId('esqlFieldsOptions')
|
||||
).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
fireEvent.click(await findByTitle('column1'));
|
||||
// click on the create button
|
||||
fireEvent.click(await findByTestId('saveEsqlControlsFlyoutButton'));
|
||||
expect(onCreateControlSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the onCancelControl callback, if Cancel button is clicked', async () => {
|
||||
const onCancelControlSpy = jest.fn();
|
||||
const { findByTestId } = render(
|
||||
<FieldControlForm
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// click on the cancel button
|
||||
fireEvent.click(await findByTestId('cancelEsqlControlsFlyoutButton'));
|
||||
expect(onCancelControlSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default correctly if initial state is given', async () => {
|
||||
const initialState = {
|
||||
grow: true,
|
||||
width: 'small',
|
||||
title: 'my control',
|
||||
availableOptions: ['column2'],
|
||||
selectedOptions: ['column2'],
|
||||
variableName: 'myField',
|
||||
variableType: ESQLVariableType.FIELDS,
|
||||
esqlQuery: 'FROM foo | STATS BY',
|
||||
controlType: EsqlControlType.STATIC_VALUES,
|
||||
} as ESQLControlState;
|
||||
const { findByTestId } = render(
|
||||
<FieldControlForm
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('myField');
|
||||
|
||||
// fields dropdown should be rendered with column2 selected
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlFieldsOptions');
|
||||
const fieldsOptionsDropdownBadge = within(fieldsOptionsDropdown).getByTestId('column2');
|
||||
expect(fieldsOptionsDropdownBadge).toBeInTheDocument();
|
||||
|
||||
// variable label input should be rendered and with the default value (my control)
|
||||
expect(await findByTestId('esqlControlLabel')).toHaveValue('my control');
|
||||
|
||||
// control width dropdown should be rendered and default to 'MEDIUM'
|
||||
expect(await findByTestId('esqlControlMinimumWidth')).toBeInTheDocument();
|
||||
const pressedWidth = within(await findByTestId('esqlControlMinimumWidth')).getByTitle('Small');
|
||||
expect(pressedWidth).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// control grow switch should be rendered and default to 'false'
|
||||
expect(await findByTestId('esqlControlGrow')).toBeInTheDocument();
|
||||
const growSwitch = await findByTestId('esqlControlGrow');
|
||||
expect(growSwitch).toBeChecked();
|
||||
});
|
||||
|
||||
it('should call the onEditControl callback, if initialState is given', async () => {
|
||||
const initialState = {
|
||||
grow: true,
|
||||
width: 'small',
|
||||
title: 'my control',
|
||||
availableOptions: ['column2'],
|
||||
selectedOptions: ['column2'],
|
||||
variableName: 'myField',
|
||||
variableType: ESQLVariableType.FIELDS,
|
||||
esqlQuery: 'FROM foo | STATS BY',
|
||||
controlType: EsqlControlType.STATIC_VALUES,
|
||||
} as ESQLControlState;
|
||||
const onEditControlSpy = jest.fn();
|
||||
const { findByTestId, findByTitle } = render(
|
||||
<FieldControlForm
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
// select the first field
|
||||
const fieldsOptionsDropdownSearchInput = within(
|
||||
await findByTestId('esqlFieldsOptions')
|
||||
).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
fireEvent.click(await findByTitle('column1'));
|
||||
// click on the create button
|
||||
fireEvent.click(await findByTestId('saveEsqlControlsFlyoutButton'));
|
||||
expect(onEditControlSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -75,14 +75,14 @@ describe('helpers', () => {
|
|||
describe('getRecurrentVariableName', () => {
|
||||
it('should return a new name if the name already exists', () => {
|
||||
const name = 'field';
|
||||
const existingNames = ['field', 'field1', 'field2'];
|
||||
const existingNames = new Set(['field', 'field1', 'field2']);
|
||||
const newName = getRecurrentVariableName(name, existingNames);
|
||||
expect(newName).toBe('field3');
|
||||
});
|
||||
|
||||
it('should return the same name if the name does not exist', () => {
|
||||
const name = 'field';
|
||||
const existingNames = ['field1', 'field2'];
|
||||
const existingNames = new Set(['field1', 'field2']);
|
||||
const newName = getRecurrentVariableName(name, existingNames);
|
||||
expect(newName).toBe('field');
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { timeUnits } from '@kbn/esql-validation-autocomplete';
|
||||
import { timeUnits, ESQLVariableType } from '@kbn/esql-validation-autocomplete';
|
||||
|
||||
function inKnownTimeInterval(timeIntervalUnit: string): boolean {
|
||||
return timeUnits.some((unit) => unit === timeIntervalUnit.toLowerCase());
|
||||
|
@ -65,10 +65,23 @@ export const areValuesIntervalsValid = (values: string[]) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getRecurrentVariableName = (name: string, existingNames: string[]) => {
|
||||
export const getVariablePrefix = (variableType: ESQLVariableType) => {
|
||||
switch (variableType) {
|
||||
case ESQLVariableType.FIELDS:
|
||||
return 'field';
|
||||
case ESQLVariableType.FUNCTIONS:
|
||||
return 'function';
|
||||
case ESQLVariableType.TIME_LITERAL:
|
||||
return 'interval';
|
||||
default:
|
||||
return 'variable';
|
||||
}
|
||||
};
|
||||
|
||||
export const getRecurrentVariableName = (name: string, existingNames: Set<string>) => {
|
||||
let newName = name;
|
||||
let i = 1;
|
||||
while (existingNames.includes(newName)) {
|
||||
while (existingNames.has(newName)) {
|
||||
newName = `${name}${i}`;
|
||||
i++;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, within, fireEvent } from '@testing-library/react';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { ESQLVariableType } from '@kbn/esql-validation-autocomplete';
|
||||
import { IdentifierControlForm } from './identifier_control_form';
|
||||
import { ESQLControlState, EsqlControlType } from '../types';
|
||||
|
||||
jest.mock('@kbn/esql-utils', () => ({
|
||||
getESQLQueryColumnsRaw: jest.fn().mockResolvedValue([{ name: 'column1' }, { name: 'column2' }]),
|
||||
}));
|
||||
|
||||
describe('IdentifierControlForm', () => {
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
const searchMock = dataMock.search.search;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// control type dropdown should be rendered and default to 'STATIC_VALUES'
|
||||
// no need to test further as the control type is disabled
|
||||
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
|
||||
const controlTypeInputPopover = await findByTestId('esqlControlTypeInputPopover');
|
||||
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');
|
||||
|
||||
// fields dropdown should be rendered with available fields column1 and column2
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
|
||||
expect(fieldsOptionsDropdown).toBeInTheDocument();
|
||||
const fieldsOptionsDropdownSearchInput = within(fieldsOptionsDropdown).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
expect(fieldsOptionsDropdownSearchInput).toHaveValue('');
|
||||
expect(await findByTitle('column1')).toBeDefined();
|
||||
expect(await findByTitle('column2')).toBeDefined();
|
||||
|
||||
// variable label input should be rendered and with the default value (empty)
|
||||
expect(await findByTestId('esqlControlLabel')).toHaveValue('');
|
||||
|
||||
// control width dropdown should be rendered and default to 'MEDIUM'
|
||||
expect(await findByTestId('esqlControlMinimumWidth')).toBeInTheDocument();
|
||||
const pressedWidth = within(await findByTestId('esqlControlMinimumWidth')).getByTitle(
|
||||
'Medium'
|
||||
);
|
||||
expect(pressedWidth).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// control grow switch should be rendered and default to 'false'
|
||||
expect(await findByTestId('esqlControlGrow')).toBeInTheDocument();
|
||||
const growSwitch = await findByTestId('esqlControlGrow');
|
||||
expect(growSwitch).not.toBeChecked();
|
||||
});
|
||||
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
// select the first field
|
||||
const fieldsOptionsDropdownSearchInput = within(
|
||||
await findByTestId('esqlIdentifiersOptions')
|
||||
).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
fireEvent.click(await findByTitle('column1'));
|
||||
// click on the create button
|
||||
fireEvent.click(await findByTestId('saveEsqlControlsFlyoutButton'));
|
||||
expect(onCreateControlSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// click on the cancel button
|
||||
fireEvent.click(await findByTestId('cancelEsqlControlsFlyoutButton'));
|
||||
expect(onCancelControlSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default correctly if initial state is given', async () => {
|
||||
const initialState = {
|
||||
grow: true,
|
||||
width: 'small',
|
||||
title: 'my control',
|
||||
availableOptions: ['column2'],
|
||||
selectedOptions: ['column2'],
|
||||
variableName: 'myField',
|
||||
variableType: ESQLVariableType.FIELDS,
|
||||
esqlQuery: 'FROM foo | STATS BY',
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// variable name input should be rendered and with the default value
|
||||
expect(await findByTestId('esqlVariableName')).toHaveValue('myField');
|
||||
|
||||
// fields dropdown should be rendered with column2 selected
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
|
||||
const fieldsOptionsDropdownBadge = within(fieldsOptionsDropdown).getByTestId('column2');
|
||||
expect(fieldsOptionsDropdownBadge).toBeInTheDocument();
|
||||
|
||||
// variable label input should be rendered and with the default value (my control)
|
||||
expect(await findByTestId('esqlControlLabel')).toHaveValue('my control');
|
||||
|
||||
// control width dropdown should be rendered and default to 'MEDIUM'
|
||||
expect(await findByTestId('esqlControlMinimumWidth')).toBeInTheDocument();
|
||||
const pressedWidth = within(await findByTestId('esqlControlMinimumWidth')).getByTitle(
|
||||
'Small'
|
||||
);
|
||||
expect(pressedWidth).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// control grow switch should be rendered and default to 'false'
|
||||
expect(await findByTestId('esqlControlGrow')).toBeInTheDocument();
|
||||
const growSwitch = await findByTestId('esqlControlGrow');
|
||||
expect(growSwitch).toBeChecked();
|
||||
});
|
||||
|
||||
it('should call the onEditControl callback, if initialState is given', async () => {
|
||||
const initialState = {
|
||||
grow: true,
|
||||
width: 'small',
|
||||
title: 'my control',
|
||||
availableOptions: ['column2'],
|
||||
selectedOptions: ['column2'],
|
||||
variableName: 'myField',
|
||||
variableType: ESQLVariableType.FIELDS,
|
||||
esqlQuery: 'FROM foo | STATS BY',
|
||||
controlType: EsqlControlType.STATIC_VALUES,
|
||||
} 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={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
// select the first field
|
||||
const fieldsOptionsDropdownSearchInput = within(
|
||||
await findByTestId('esqlIdentifiersOptions')
|
||||
).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
fireEvent.click(await findByTitle('column1'));
|
||||
// click on the create button
|
||||
fireEvent.click(await findByTestId('saveEsqlControlsFlyoutButton'));
|
||||
expect(onEditControlSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
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={[]}
|
||||
/>
|
||||
);
|
||||
// control type dropdown should be rendered and default to 'STATIC_VALUES'
|
||||
expect(await findByTestId('esqlControlTypeDropdown')).toBeInTheDocument();
|
||||
const controlTypeInputPopover = await findByTestId('esqlControlTypeInputPopover');
|
||||
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');
|
||||
|
||||
// fields dropdown should be rendered with available functions
|
||||
const fieldsOptionsDropdown = await findByTestId('esqlIdentifiersOptions');
|
||||
expect(fieldsOptionsDropdown).toBeInTheDocument();
|
||||
const fieldsOptionsDropdownSearchInput = within(fieldsOptionsDropdown).getByRole('combobox');
|
||||
fireEvent.click(fieldsOptionsDropdownSearchInput);
|
||||
expect(await findByTitle('avg')).toBeDefined();
|
||||
expect(await findByTitle('median')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiComboBox,
|
||||
|
@ -19,7 +20,11 @@ import {
|
|||
import { css } from '@emotion/react';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import type { ISearchGeneric } from '@kbn/search-types';
|
||||
import { ESQLVariableType, ESQLControlVariable } from '@kbn/esql-validation-autocomplete';
|
||||
import {
|
||||
ESQLVariableType,
|
||||
ESQLControlVariable,
|
||||
aggregationFunctionDefinitions,
|
||||
} from '@kbn/esql-validation-autocomplete';
|
||||
import { getESQLQueryColumnsRaw } from '@kbn/esql-utils';
|
||||
import type { ESQLControlState, ControlWidthOptions } from '../types';
|
||||
import {
|
||||
|
@ -35,10 +40,11 @@ import {
|
|||
getFlyoutStyling,
|
||||
getQueryForFields,
|
||||
validateVariableName,
|
||||
getVariablePrefix,
|
||||
} from './helpers';
|
||||
import { EsqlControlType } from '../types';
|
||||
|
||||
interface FieldControlFormProps {
|
||||
interface IdentifierControlFormProps {
|
||||
search: ISearchGeneric;
|
||||
variableType: ESQLVariableType;
|
||||
queryString: string;
|
||||
|
@ -51,7 +57,7 @@ interface FieldControlFormProps {
|
|||
onCancelControl?: () => void;
|
||||
}
|
||||
|
||||
export function FieldControlForm({
|
||||
export function IdentifierControlForm({
|
||||
variableType,
|
||||
initialState,
|
||||
queryString,
|
||||
|
@ -62,23 +68,28 @@ export function FieldControlForm({
|
|||
onCancelControl,
|
||||
search,
|
||||
closeFlyout,
|
||||
}: FieldControlFormProps) {
|
||||
}: IdentifierControlFormProps) {
|
||||
const isMounted = useMountedState();
|
||||
const suggestedVariableName = useMemo(() => {
|
||||
const existingVariables = esqlVariables.filter((variable) => variable.type === variableType);
|
||||
const existingVariables = new Set(
|
||||
esqlVariables
|
||||
.filter((variable) => variable.type === variableType)
|
||||
.map((variable) => variable.key)
|
||||
);
|
||||
|
||||
return initialState
|
||||
? `${initialState.variableName}`
|
||||
: getRecurrentVariableName(
|
||||
'field',
|
||||
existingVariables.map((variable) => variable.key)
|
||||
);
|
||||
if (initialState) {
|
||||
return initialState.variableName;
|
||||
}
|
||||
|
||||
const variablePrefix = getVariablePrefix(variableType);
|
||||
return getRecurrentVariableName(variablePrefix, existingVariables);
|
||||
}, [esqlVariables, initialState, variableType]);
|
||||
|
||||
const [availableFieldsOptions, setAvailableFieldsOptions] = useState<EuiComboBoxOptionOption[]>(
|
||||
[]
|
||||
);
|
||||
const [availableIdentifiersOptions, setAvailableIdentifiersOptions] = useState<
|
||||
EuiComboBoxOptionOption[]
|
||||
>([]);
|
||||
|
||||
const [selectedFields, setSelectedFields] = useState<EuiComboBoxOptionOption[]>(
|
||||
const [selectedIdentifiers, setSelectedIdentifiers] = useState<EuiComboBoxOptionOption[]>(
|
||||
initialState
|
||||
? initialState.availableOptions.map((option) => {
|
||||
return {
|
||||
|
@ -97,36 +108,59 @@ export function FieldControlForm({
|
|||
|
||||
const isControlInEditMode = useMemo(() => !!initialState, [initialState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableFieldsOptions.length) {
|
||||
const queryForFields = getQueryForFields(queryString, cursorPosition);
|
||||
getESQLQueryColumnsRaw({
|
||||
esqlQuery: queryForFields,
|
||||
search,
|
||||
}).then((columns) => {
|
||||
setAvailableFieldsOptions(
|
||||
columns.map((col) => {
|
||||
return {
|
||||
label: col.name,
|
||||
key: col.name,
|
||||
'data-test-subj': col.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [availableFieldsOptions.length, variableType, cursorPosition, queryString, search]);
|
||||
useEffect(
|
||||
function initAvailableIdentifiersOptions() {
|
||||
if (availableIdentifiersOptions.length > 0) return;
|
||||
if (variableType === ESQLVariableType.FIELDS) {
|
||||
const queryForFields = getQueryForFields(queryString, cursorPosition);
|
||||
getESQLQueryColumnsRaw({
|
||||
esqlQuery: queryForFields,
|
||||
search,
|
||||
}).then((columns) => {
|
||||
if (isMounted()) {
|
||||
setAvailableIdentifiersOptions(
|
||||
columns.map((col) => {
|
||||
return {
|
||||
label: col.name,
|
||||
key: col.name,
|
||||
'data-test-subj': col.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (variableType === ESQLVariableType.FUNCTIONS) {
|
||||
const aggregatedFunctions = aggregationFunctionDefinitions.map((func) => {
|
||||
return {
|
||||
label: func.name,
|
||||
key: func.name,
|
||||
'data-test-subj': func.name,
|
||||
};
|
||||
});
|
||||
setAvailableIdentifiersOptions(aggregatedFunctions);
|
||||
}
|
||||
},
|
||||
[
|
||||
availableIdentifiersOptions.length,
|
||||
cursorPosition,
|
||||
isMounted,
|
||||
queryString,
|
||||
search,
|
||||
variableType,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const variableExists =
|
||||
esqlVariables.some((variable) => variable.key === variableName.replace('?', '')) &&
|
||||
!isControlInEditMode;
|
||||
|
||||
setFormIsInvalid(!selectedFields.length || !variableName || variableExists);
|
||||
}, [esqlVariables, isControlInEditMode, selectedFields.length, variableName]);
|
||||
setFormIsInvalid(!selectedIdentifiers.length || !variableName || variableExists);
|
||||
}, [esqlVariables, isControlInEditMode, selectedIdentifiers.length, variableName]);
|
||||
|
||||
const onFieldsChange = useCallback((selectedOptions: EuiComboBoxOptionOption[]) => {
|
||||
setSelectedFields(selectedOptions);
|
||||
const onIdentifiersChange = useCallback((selectedOptions: EuiComboBoxOptionOption[]) => {
|
||||
setSelectedIdentifiers(selectedOptions);
|
||||
}, []);
|
||||
|
||||
const onVariableNameChange = useCallback(
|
||||
|
@ -170,16 +204,16 @@ export function FieldControlForm({
|
|||
(option) => option.label.trim().toLowerCase() === normalizedSearchValue
|
||||
) === -1
|
||||
) {
|
||||
setAvailableFieldsOptions([...availableFieldsOptions, newOption]);
|
||||
setAvailableIdentifiersOptions((prev) => [...prev, newOption]);
|
||||
}
|
||||
|
||||
setSelectedFields((prevSelected) => [...prevSelected, newOption]);
|
||||
setSelectedIdentifiers((prevSelected) => [...prevSelected, newOption]);
|
||||
},
|
||||
[availableFieldsOptions]
|
||||
[]
|
||||
);
|
||||
|
||||
const onCreateFieldControl = useCallback(async () => {
|
||||
const availableOptions = selectedFields.map((field) => field.label);
|
||||
const availableOptions = selectedIdentifiers.map((field) => field.label);
|
||||
const state = {
|
||||
availableOptions,
|
||||
selectedOptions: [availableOptions[0]],
|
||||
|
@ -201,7 +235,7 @@ export function FieldControlForm({
|
|||
}
|
||||
closeFlyout();
|
||||
}, [
|
||||
selectedFields,
|
||||
selectedIdentifiers,
|
||||
minimumWidth,
|
||||
label,
|
||||
variableName,
|
||||
|
@ -246,11 +280,11 @@ export function FieldControlForm({
|
|||
placeholder={i18n.translate('esql.flyout.fieldsOptions.placeholder', {
|
||||
defaultMessage: 'Select or add values',
|
||||
})}
|
||||
options={availableFieldsOptions}
|
||||
selectedOptions={selectedFields}
|
||||
onChange={onFieldsChange}
|
||||
options={availableIdentifiersOptions}
|
||||
selectedOptions={selectedIdentifiers}
|
||||
onChange={onIdentifiersChange}
|
||||
onCreateOption={onCreateOption}
|
||||
data-test-subj="esqlFieldsOptions"
|
||||
data-test-subj="esqlIdentifiersOptions"
|
||||
fullWidth
|
||||
compressed
|
||||
/>
|
|
@ -13,7 +13,7 @@ import type { ISearchGeneric } from '@kbn/search-types';
|
|||
import { monaco } from '@kbn/monaco';
|
||||
import type { ESQLControlState } from '../types';
|
||||
import { ValueControlForm } from './value_control_form';
|
||||
import { FieldControlForm } from './field_control_form';
|
||||
import { IdentifierControlForm } from './identifier_control_form';
|
||||
import { updateQueryStringWithVariable } from './helpers';
|
||||
|
||||
interface ESQLControlsFlyoutProps {
|
||||
|
@ -71,9 +71,12 @@ export function ESQLControlsFlyout({
|
|||
search={search}
|
||||
/>
|
||||
);
|
||||
} else if (variableType === ESQLVariableType.FIELDS) {
|
||||
} else if (
|
||||
variableType === ESQLVariableType.FIELDS ||
|
||||
variableType === ESQLVariableType.FUNCTIONS
|
||||
) {
|
||||
return (
|
||||
<FieldControlForm
|
||||
<IdentifierControlForm
|
||||
variableType={variableType}
|
||||
esqlVariables={esqlVariables}
|
||||
queryString={queryString}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiComboBox,
|
||||
|
@ -43,6 +44,7 @@ import {
|
|||
getFlyoutStyling,
|
||||
areValuesIntervalsValid,
|
||||
validateVariableName,
|
||||
getVariablePrefix,
|
||||
} from './helpers';
|
||||
import { EsqlControlType } from '../types';
|
||||
import { ChooseColumnPopover } from './choose_column_popover';
|
||||
|
@ -72,6 +74,7 @@ export function ValueControlForm({
|
|||
onCreateControl,
|
||||
onEditControl,
|
||||
}: ValueControlFormProps) {
|
||||
const isMounted = useMountedState();
|
||||
const valuesField = useMemo(() => {
|
||||
if (variableType === ESQLVariableType.VALUES) {
|
||||
return getValuesFromQueryField(queryString);
|
||||
|
@ -79,31 +82,25 @@ export function ValueControlForm({
|
|||
return null;
|
||||
}, [variableType, queryString]);
|
||||
const suggestedVariableName = useMemo(() => {
|
||||
const existingVariables = esqlVariables.filter((variable) => variable.type === variableType);
|
||||
const existingVariables = new Set(
|
||||
esqlVariables
|
||||
.filter((variable) => variable.type === variableType)
|
||||
.map((variable) => variable.key)
|
||||
);
|
||||
|
||||
if (initialState) {
|
||||
return 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, '_');
|
||||
return getRecurrentVariableName(
|
||||
fieldVariableName,
|
||||
existingVariables.map((variable) => variable.key)
|
||||
);
|
||||
variablePrefix = fieldVariableName;
|
||||
}
|
||||
|
||||
if (variableType === ESQLVariableType.TIME_LITERAL) {
|
||||
return getRecurrentVariableName(
|
||||
'interval',
|
||||
existingVariables.map((variable) => variable.key)
|
||||
);
|
||||
}
|
||||
return getRecurrentVariableName(
|
||||
'variable',
|
||||
existingVariables.map((variable) => variable.key)
|
||||
);
|
||||
return getRecurrentVariableName(variablePrefix, existingVariables);
|
||||
}, [esqlVariables, initialState, valuesField, variableType]);
|
||||
|
||||
const [controlFlyoutType, setControlFlyoutType] = useState<EsqlControlType>(
|
||||
|
@ -243,6 +240,9 @@ export function ValueControlForm({
|
|||
filter: undefined,
|
||||
dropNullColumns: true,
|
||||
}).then((results) => {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
const columns = results.response.columns.map((col) => col.name);
|
||||
setQueryColumns(columns);
|
||||
|
||||
|
@ -267,7 +267,7 @@ export function ValueControlForm({
|
|||
setEsqlQueryErrors([e]);
|
||||
}
|
||||
},
|
||||
[search]
|
||||
[isMounted, search]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -69,8 +69,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
return await testSubjects.exists('create_esql_control_flyout');
|
||||
});
|
||||
|
||||
await comboBox.set('esqlFieldsOptions', 'geo.dest');
|
||||
await comboBox.set('esqlFieldsOptions', 'clientip');
|
||||
await comboBox.set('esqlIdentifiersOptions', 'geo.dest');
|
||||
await comboBox.set('esqlIdentifiersOptions', 'clientip');
|
||||
|
||||
// create the control
|
||||
await testSubjects.click('saveEsqlControlsFlyoutButton');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue