From 9df1dcc8b3694a74b89f076bbc0e20b633f096cd Mon Sep 17 00:00:00 2001 From: Stratoula Date: Thu, 7 May 2026 08:59:48 +0200 Subject: [PATCH 1/2] [ES|QL] [Controls] Fixes fields available values not available after editing --- .../src/options_list_schema.ts | 8 ++++++++ .../esql_control/esql_control_manager.ts | 18 ++++++++++++++---- .../esql_control/get_esql_control_factory.tsx | 4 +++- .../identifier_control_form.test.tsx | 4 ++-- .../control_flyout/identifier_control_form.tsx | 5 +++++ 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/platform/packages/shared/controls/controls-schemas/src/options_list_schema.ts b/src/platform/packages/shared/controls/controls-schemas/src/options_list_schema.ts index fca8af23fd7a1..e8ef296fbde8b 100644 --- a/src/platform/packages/shared/controls/controls-schemas/src/options_list_schema.ts +++ b/src/platform/packages/shared/controls/controls-schemas/src/options_list_schema.ts @@ -192,6 +192,14 @@ export const optionsListESQLControlSchema = schema.discriminatedUnion('control_t description: 'A fixed list of option strings displayed in the control.', }, }), + source_esql_query: schema.maybe( + schema.string({ + meta: { + description: + 'The ES|QL query the control was created from. Not evaluated at runtime; persisted only so the available identifiers list (e.g. fields) can be repopulated when the control is edited.', + }, + }) + ), }, { meta: { diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.ts b/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.ts index 619a5fa812b9d..f206c93e590f6 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.ts +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.ts @@ -27,7 +27,7 @@ import type { OptionsListSelection, } from '@kbn/controls-schemas'; import type { TimeRange } from '@kbn/es-query'; -import type { ESQLControlVariable, QueryESQLControl } from '@kbn/esql-types'; +import type { ESQLControlVariable, QueryESQLControl, StaticESQLControl } from '@kbn/esql-types'; import { EsqlControlType, ESQLVariableType, @@ -66,7 +66,7 @@ type ComparatorsReturnType & (ControlType extends EsqlControlType.STATIC_VALUES - ? {} + ? Pick : Pick) >; @@ -83,7 +83,7 @@ export const getSelectionComparators = ; }; @@ -109,6 +109,9 @@ export function initializeESQLControlManager( initialState.variable_type as ESQLVariableType ); const esqlQuery$ = new BehaviorSubject(isEsqlQueryControl ? initialState.esql_query : ''); + const sourceEsqlQuery$ = new BehaviorSubject( + isStaticControl ? initialState.source_esql_query : undefined + ); let valuesColumnType: string | undefined; const totalCardinality$ = new BehaviorSubject( isStaticControl ? initialState.available_options.length : 0 @@ -317,7 +320,8 @@ export function initializeESQLControlManager( variableName$, singleSelect$, variableType$, - esqlQuery$ + esqlQuery$, + sourceEsqlQuery$ ).pipe(map(() => undefined)), reinitializeState: (lastSaved?: ESQLOptionsListRuntimeState) => { setSelectedOptions(lastSaved?.selected_options ?? []); @@ -331,6 +335,9 @@ export function initializeESQLControlManager( hasInitialFetch = false; availableOptions$.next(lastSaved?.available_options ?? []); esqlQuery$.next(isQueryESQLControl(lastSaved) ? lastSaved.esql_query : ''); + sourceEsqlQuery$.next( + isStaticESQLControl(lastSaved) ? lastSaved.source_esql_query : undefined + ); }, getLatestState: (): OptionsListESQLControlState => { return { @@ -343,6 +350,9 @@ export function initializeESQLControlManager( ? { control_type: EsqlControlType.STATIC_VALUES, available_options: availableOptions$.getValue(), + ...(sourceEsqlQuery$.getValue() !== undefined && { + source_esql_query: sourceEsqlQuery$.getValue(), + }), } : { control_type: EsqlControlType.VALUES_FROM_QUERY, esql_query: esqlQuery$.getValue() }), }; diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx index 76c5a86a2d6d7..7e20020e7a62f 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx @@ -119,7 +119,9 @@ export const getESQLControlFactory = < }; try { await uiActionsService.executeTriggerActions('ESQL_CONTROL_TRIGGER', { - queryString: isStaticESQLControl(nextState) ? '' : nextState.esql_query, + queryString: isStaticESQLControl(nextState) + ? nextState.source_esql_query ?? '' + : nextState.esql_query, variableType: nextState.variable_type, controlType: nextState.control_type, esqlVariables: variablesInParent, diff --git a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.test.tsx b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.test.tsx index 587f42565ab16..5d39f17f7e7fa 100644 --- a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.test.tsx +++ b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.test.tsx @@ -155,7 +155,7 @@ describe('IdentifierControlForm', () => { selected_options: ['column2'], variable_name: 'myField', variable_type: ESQLVariableType.FIELDS, - esql_query: 'FROM foo | STATS BY', + source_esql_query: 'FROM foo | STATS BY', control_type: EsqlControlType.STATIC_VALUES, } as OptionsListESQLControlState; const { findByTestId } = render( @@ -186,7 +186,7 @@ describe('IdentifierControlForm', () => { selected_options: ['column2'], variable_name: 'myField', variable_type: ESQLVariableType.FIELDS, - esql_query: 'FROM foo | STATS BY', + source_esql_query: 'FROM foo | STATS BY', control_type: EsqlControlType.STATIC_VALUES, } as OptionsListESQLControlState; const onEditControlSpy = jest.fn(); diff --git a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.tsx b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.tsx index 69bdcb270ef46..be80630fe8d2f 100644 --- a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.tsx +++ b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/identifier_control_form.tsx @@ -157,6 +157,11 @@ export function IdentifierControlForm({ variable_name: variableNameWithoutQuestionmark, variable_type: variableType, control_type: EsqlControlType.STATIC_VALUES as StaticESQLControl['control_type'], + // Persist the originating query for FIELDS controls so the available + // identifiers list can be repopulated when the control is edited. + ...(variableType === ESQLVariableType.FIELDS && queryString + ? { source_esql_query: queryString } + : {}), }; if (!isEqual(state, initialState)) { setControlState(state); From 3f6e7b75c7aa6d73aeb45f0c03088b292d3a26b3 Mon Sep 17 00:00:00 2001 From: Stratoula Date: Thu, 7 May 2026 09:07:36 +0200 Subject: [PATCH 2/2] Add a test to safeguard for another regression --- .../get_esql_control_factory.test.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx index c5da6d06129b2..dd1150463dde1 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx @@ -36,6 +36,14 @@ jest.mock('./utils/get_esql_single_column_values', () => { }; }); +const mockExecuteTriggerActions = jest.fn(); +jest.mock('../../services/kibana_services', () => ({ + ...jest.requireActual('../../services/kibana_services'), + uiActionsService: { + executeTriggerActions: (...args: unknown[]) => mockExecuteTriggerActions(...args), + }, +})); + describe('ESQLControlApi', () => { beforeEach(() => { jest.resetAllMocks(); @@ -100,6 +108,37 @@ describe('ESQLControlApi', () => { }); }); + describe('editing a STATIC fields control', () => { + test('onEdit forwards the persisted source_esql_query as queryString', async () => { + const sourceQuery = 'FROM logs | KEEP '; + const initialState: OptionsListESQLControlState = { + ...DEFAULT_ESQL_OPTIONS_LIST_STATE, + selected_options: ['column1'], + available_options: ['column1'], + variable_name: 'myField', + variable_type: ESQLVariableType.FIELDS, + control_type: EsqlControlType.STATIC_VALUES, + source_esql_query: sourceQuery, + }; + const { api } = await factory.buildEmbeddable({ + initializeDrilldownsManager: jest.fn(), + initialState, + finalizeApi, + uuid, + parentApi: dashboardApi, + }); + await api.onEdit(); + expect(mockExecuteTriggerActions).toHaveBeenCalledWith( + 'ESQL_CONTROL_TRIGGER', + expect.objectContaining({ + queryString: sourceQuery, + controlType: EsqlControlType.STATIC_VALUES, + variableType: ESQLVariableType.FIELDS, + }) + ); + }); + }); + describe('values from query', () => { test('should update on load and fetch', async () => { const initialState: OptionsListESQLControlState = {