diff --git a/src/platform/packages/shared/controls/controls-constants/src/control_constants.ts b/src/platform/packages/shared/controls/controls-constants/src/control_constants.ts index aa38568bf7649..953577fc2792f 100644 --- a/src/platform/packages/shared/controls/controls-constants/src/control_constants.ts +++ b/src/platform/packages/shared/controls/controls-constants/src/control_constants.ts @@ -8,10 +8,10 @@ */ // Do not change constant values - part of public REST APIs -export const TIME_SLIDER_CONTROL = 'time_slider_control'; -export const RANGE_SLIDER_CONTROL = 'range_slider_control'; -export const OPTIONS_LIST_CONTROL = 'options_list_control'; export const ESQL_CONTROL = 'esql_control'; +export const OPTIONS_LIST_CONTROL = 'options_list_control'; +export const RANGE_SLIDER_CONTROL = 'range_slider_control'; +export const TIME_SLIDER_CONTROL = 'time_slider_control'; export const DEFAULT_DATA_CONTROL_STATE = { use_global_filters: true, diff --git a/src/platform/packages/shared/controls/controls-schemas/index.ts b/src/platform/packages/shared/controls/controls-schemas/index.ts index 135778800129a..a9c4b4f2efe42 100644 --- a/src/platform/packages/shared/controls/controls-schemas/index.ts +++ b/src/platform/packages/shared/controls/controls-schemas/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { controlsGroupSchema } from './src/controls_group_schema'; +export { getControlsGroupSchema } from './src/controls_group_schema'; export type { ControlsGroupState, diff --git a/src/platform/packages/shared/controls/controls-schemas/src/control_schema.ts b/src/platform/packages/shared/controls/controls-schemas/src/control_schema.ts index f82872e64f488..e5e5f3b276a06 100644 --- a/src/platform/packages/shared/controls/controls-schemas/src/control_schema.ts +++ b/src/platform/packages/shared/controls/controls-schemas/src/control_schema.ts @@ -10,16 +10,14 @@ import { schema } from '@kbn/config-schema'; import { DEFAULT_DATA_CONTROL_STATE } from '@kbn/controls-constants'; -export const controlSchema = schema.object( - { - title: schema.maybe( - schema.string({ meta: { description: 'A human-readable title for the control' } }) - ), - }, - { unknowns: 'allow' } -); +export const controlTitleSchema = schema.object({ + title: schema.maybe( + schema.string({ meta: { description: 'A human-readable title for the control' } }) + ), +}); -export const dataControlSchema = controlSchema.extends({ +export const dataControlSchema = schema.object({ + ...controlTitleSchema.getPropSchemas(), data_view_id: schema.string({ meta: { description: 'The ID of the data view that the control is tied to' }, // this will generate a reference }), diff --git a/src/platform/packages/shared/controls/controls-schemas/src/controls_group_schema.ts b/src/platform/packages/shared/controls/controls-schemas/src/controls_group_schema.ts index 3723ca5e85b3a..ee0e74852a753 100644 --- a/src/platform/packages/shared/controls/controls-schemas/src/controls_group_schema.ts +++ b/src/platform/packages/shared/controls/controls-schemas/src/controls_group_schema.ts @@ -43,67 +43,67 @@ export const pinnedControlSchema = schema.object({ }), }); -export const controlsGroupSchema = schema.arrayOf( - // order will be determined by the array - schema.oneOf([ - schema - .allOf( - [ - schema.object({ type: schema.literal(OPTIONS_LIST_CONTROL) }), - schema.object({ config: optionsListDSLControlSchema }), - pinnedControlSchema, - ], +export const getControlsGroupSchema = () => { + const pinnedControl = pinnedControlSchema.getPropSchemas(); + return schema.arrayOf( + /** + * - keep types in alphabetical order for the sake of documentation + * - control order will be determined by the array + */ + schema.discriminatedUnion('type', [ + schema.object( + { + type: schema.literal(ESQL_CONTROL), + config: optionsListESQLControlSchema, + ...pinnedControl, + }, + { + meta: { + title: ESQL_CONTROL, + }, + } + ), + schema.object( + { + type: schema.literal(OPTIONS_LIST_CONTROL), + config: optionsListDSLControlSchema, + ...pinnedControl, + }, { meta: { title: OPTIONS_LIST_CONTROL, }, } - ) - .extendsDeep({ unknowns: 'allow' }), // allows for legacy unknowns such as `parentField` and `enhancements` - schema - .allOf( - [ - schema.object({ type: schema.literal(RANGE_SLIDER_CONTROL) }), - schema.object({ config: rangeSliderControlSchema }), - pinnedControlSchema, - ], + ), + schema.object( + { + type: schema.literal(RANGE_SLIDER_CONTROL), + config: rangeSliderControlSchema, + ...pinnedControl, + }, { meta: { title: RANGE_SLIDER_CONTROL, }, } - ) - .extendsDeep({ unknowns: 'allow' }), - schema - .allOf( - [ - schema.object({ type: schema.literal(TIME_SLIDER_CONTROL) }), - schema.object({ config: timeSliderControlSchema }), - pinnedControlSchema, - ], + ), + schema.object( + { + type: schema.literal(TIME_SLIDER_CONTROL), + config: timeSliderControlSchema, + ...pinnedControl, + }, { meta: { title: TIME_SLIDER_CONTROL, }, } - ) - .extendsDeep({ unknowns: 'allow' }), // allows for legacy unknowns such as `useGlobalFilters` - schema.allOf( - [ - schema.object({ type: schema.literal(ESQL_CONTROL) }), - schema.object({ config: optionsListESQLControlSchema }), - pinnedControlSchema, - ], - { - meta: { - title: ESQL_CONTROL, - }, - } - ), // variable controls do not need `unknowns: 'allow'` because they have no legacy values - ]), - { - defaultValue: [], - maxSize: 100, - meta: { description: 'An array of control panels and their state in the control group.' }, - } -); + ), + ]), + { + defaultValue: [], + maxSize: 100, + meta: { description: 'An array of control panels and their state in the control group.' }, + } + ); +}; 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 c526fb280fbcd..cb39823b62e0d 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 @@ -13,7 +13,7 @@ import { DEFAULT_ESQL_OPTIONS_LIST_STATE, MAX_OPTIONS_LIST_REQUEST_SIZE, } from '@kbn/controls-constants'; -import { controlSchema, dataControlSchema } from './control_schema'; +import { controlTitleSchema, dataControlSchema } from './control_schema'; const SELECTIONS_MAX = 10000; @@ -40,48 +40,53 @@ export const optionsListSortSchema = schema.object( export const optionsListSelectionSchema = schema.oneOf([schema.string(), schema.number()]); -const optionsListControlBaseParameters = { +const optionsListControlBaseParameters = schema.object({ display_settings: schema.maybe(optionsListDisplaySettingsSchema), -}; +}); -export const optionsListDSLControlSchema = dataControlSchema - .extends(optionsListControlBaseParameters) - .extends({ - exclude: schema.boolean({ defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.exclude }), - exists_selected: schema.boolean({ - defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.exists_selected, - }), - run_past_timeout: schema.boolean({ - defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.run_past_timeout, - }), - search_technique: optionsListSearchTechniqueSchema, - selected_options: schema.arrayOf(optionsListSelectionSchema, { - defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.selected_options, - maxSize: SELECTIONS_MAX, - }), - single_select: schema.boolean({ defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.single_select }), - sort: optionsListSortSchema, - }); +export const optionsListDSLControlSchema = schema.object({ + ...optionsListControlBaseParameters.getPropSchemas(), + ...dataControlSchema.getPropSchemas(), + exclude: schema.boolean({ defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.exclude }), + exists_selected: schema.boolean({ + defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.exists_selected, + }), + run_past_timeout: schema.boolean({ + defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.run_past_timeout, + }), + search_technique: optionsListSearchTechniqueSchema, + selected_options: schema.arrayOf(optionsListSelectionSchema, { + defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.selected_options, + maxSize: SELECTIONS_MAX, + }), + single_select: schema.boolean({ defaultValue: DEFAULT_DSL_OPTIONS_LIST_STATE.single_select }), + sort: optionsListSortSchema, +}); + +const baseEsqlControl = { + ...controlTitleSchema.getPropSchemas(), + ...optionsListControlBaseParameters.getPropSchemas(), + selected_options: schema.arrayOf(schema.string(), { maxSize: SELECTIONS_MAX }), + single_select: schema.boolean({ defaultValue: DEFAULT_ESQL_OPTIONS_LIST_STATE.single_select }), + variable_name: schema.string(), + variable_type: schema.oneOf([ + schema.literal('fields'), + schema.literal('values'), + schema.literal('functions'), + schema.literal('time_literal'), + schema.literal('multi_values'), + ]), +}; -export const optionsListESQLControlSchema = controlSchema - .extends(optionsListControlBaseParameters) - .extends({ - selected_options: schema.arrayOf(schema.string(), { maxSize: SELECTIONS_MAX }), - single_select: schema.boolean({ defaultValue: DEFAULT_ESQL_OPTIONS_LIST_STATE.single_select }), - variable_name: schema.string(), - variable_type: schema.oneOf([ - schema.literal('fields'), - schema.literal('values'), - schema.literal('functions'), - schema.literal('time_literal'), - schema.literal('multi_values'), - ]), +export const optionsListESQLControlSchema = schema.discriminatedUnion('control_type', [ + schema.object({ + ...baseEsqlControl, + control_type: schema.literal('STATIC_VALUES'), + available_options: schema.arrayOf(schema.string(), { maxSize: MAX_OPTIONS_LIST_REQUEST_SIZE }), + }), + schema.object({ + ...baseEsqlControl, + control_type: schema.literal('VALUES_FROM_QUERY'), esql_query: schema.string(), - control_type: schema.oneOf([ - schema.literal('STATIC_VALUES'), - schema.literal('VALUES_FROM_QUERY'), - ]), - available_options: schema.maybe( - schema.arrayOf(schema.string(), { maxSize: MAX_OPTIONS_LIST_REQUEST_SIZE }) - ), - }); + }), +]); diff --git a/src/platform/packages/shared/controls/controls-schemas/src/range_slider_schema.ts b/src/platform/packages/shared/controls/controls-schemas/src/range_slider_schema.ts index fae0af34149c7..d57db1913cc24 100644 --- a/src/platform/packages/shared/controls/controls-schemas/src/range_slider_schema.ts +++ b/src/platform/packages/shared/controls/controls-schemas/src/range_slider_schema.ts @@ -13,7 +13,8 @@ import { dataControlSchema } from './control_schema'; export const rangeValueSchema = schema.arrayOf(schema.string(), { minSize: 2, maxSize: 2 }); -export const rangeSliderControlSchema = dataControlSchema.extends({ +export const rangeSliderControlSchema = schema.object({ + ...dataControlSchema.getPropSchemas(), value: schema.maybe(rangeValueSchema), step: schema.number({ defaultValue: DEFAULT_RANGE_SLIDER_STATE.step, min: 0 }), }); diff --git a/src/platform/packages/shared/controls/controls-schemas/src/types.ts b/src/platform/packages/shared/controls/controls-schemas/src/types.ts index b6a53b5e1e9e5..180b183ef8423 100644 --- a/src/platform/packages/shared/controls/controls-schemas/src/types.ts +++ b/src/platform/packages/shared/controls/controls-schemas/src/types.ts @@ -10,9 +10,9 @@ import type React from 'react'; import type { TypeOf } from '@kbn/config-schema'; -import type { controlSchema, dataControlSchema } from './control_schema'; +import type { controlTitleSchema, dataControlSchema } from './control_schema'; import type { - controlsGroupSchema, + getControlsGroupSchema, controlWidthSchema, pinnedControlSchema, } from './controls_group_schema'; @@ -27,14 +27,15 @@ import type { import type { rangeSliderControlSchema, rangeValueSchema } from './range_slider_schema'; import type { timeSliderControlSchema } from './time_slider_schema'; -export type ControlsGroupState = TypeOf; +export type ControlsGroupState = TypeOf>; export type PinnedControlState = ControlsGroupState[number]; export type PinnedControlLayoutState = TypeOf & { order: number; type: string; }; + export type ControlWidth = TypeOf; -export type ControlState = TypeOf; +export type ControlState = TypeOf; export type DataControlState = TypeOf; diff --git a/src/platform/packages/shared/kbn-esql-types/index.ts b/src/platform/packages/shared/kbn-esql-types/index.ts index 6fac4ec849d63..945f4fd747dce 100644 --- a/src/platform/packages/shared/kbn-esql-types/index.ts +++ b/src/platform/packages/shared/kbn-esql-types/index.ts @@ -14,9 +14,13 @@ export { type ESQLControlVariable, type PublishesESQLVariable, type PublishesESQLVariables, + type StaticESQLControl, + type QueryESQLControl, apiPublishesESQLVariable, apiPublishesESQLVariables, controlHasVariableName, + isStaticESQLControl, + isQueryESQLControl, } from './src/variables_types'; export { diff --git a/src/platform/packages/shared/kbn-esql-types/moon.yml b/src/platform/packages/shared/kbn-esql-types/moon.yml index 1e90761d13de8..85d97d08d25a6 100644 --- a/src/platform/packages/shared/kbn-esql-types/moon.yml +++ b/src/platform/packages/shared/kbn-esql-types/moon.yml @@ -20,6 +20,7 @@ dependsOn: - '@kbn/licensing-types' - '@kbn/core-pricing-common' - '@kbn/projects-solutions-groups' + - '@kbn/controls-schemas' tags: - shared-common - package diff --git a/src/platform/packages/shared/kbn-esql-types/src/variables_types.ts b/src/platform/packages/shared/kbn-esql-types/src/variables_types.ts index a0b0f350f0edc..11634bee0fcd6 100644 --- a/src/platform/packages/shared/kbn-esql-types/src/variables_types.ts +++ b/src/platform/packages/shared/kbn-esql-types/src/variables_types.ts @@ -8,6 +8,7 @@ */ import type { BehaviorSubject } from 'rxjs'; +import type { OptionsListESQLControlState } from '@kbn/controls-schemas'; type PublishingSubject = Omit, 'next'>; @@ -34,6 +35,28 @@ export enum EsqlControlType { VALUES_FROM_QUERY = 'VALUES_FROM_QUERY', } +export type StaticESQLControl = Extract< + OptionsListESQLControlState, + { control_type: 'STATIC_VALUES' } +>; +export const isStaticESQLControl = (control?: object): control is StaticESQLControl => { + return Boolean( + control && 'control_type' in control && control.control_type === EsqlControlType.STATIC_VALUES + ); +}; + +export type QueryESQLControl = Extract< + OptionsListESQLControlState, + { control_type: 'VALUES_FROM_QUERY' } +>; +export const isQueryESQLControl = (control?: object): control is QueryESQLControl => { + return Boolean( + control && + 'control_type' in control && + control.control_type === EsqlControlType.VALUES_FROM_QUERY + ); +}; + export interface ESQLControlVariable { key: string; value: string | number | (string | number)[]; diff --git a/src/platform/packages/shared/kbn-esql-types/tsconfig.json b/src/platform/packages/shared/kbn-esql-types/tsconfig.json index 7e80e07cabd41..c7b0e5aa36017 100644 --- a/src/platform/packages/shared/kbn-esql-types/tsconfig.json +++ b/src/platform/packages/shared/kbn-esql-types/tsconfig.json @@ -8,6 +8,7 @@ "@kbn/licensing-types", "@kbn/core-pricing-common", "@kbn/projects-solutions-groups", + "@kbn/controls-schemas", ], "exclude": ["target/**/*"] } diff --git a/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts b/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts index 5107fe18fa9da..7f0e579fc6421 100644 --- a/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts +++ b/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts @@ -28,7 +28,6 @@ const createControlState = (variableName: string): OptionsListESQLControlState = selected_options: ['option-1'], variable_name: variableName, variable_type: ESQLVariableType.VALUES, - esql_query: 'FROM index', control_type: EsqlControlType.STATIC_VALUES, single_select: true, available_options: ['option-1', 'option-2'], diff --git a/src/platform/plugins/shared/controls/common/options_list/index.ts b/src/platform/plugins/shared/controls/common/options_list/index.ts index 4976a1314a3f0..9ac55429121f1 100644 --- a/src/platform/plugins/shared/controls/common/options_list/index.ts +++ b/src/platform/plugins/shared/controls/common/options_list/index.ts @@ -15,4 +15,3 @@ export type { OptionsListSuccessResponse, OptionsListSuggestions, } from './types'; -export { isOptionsListESQLControlState } from './types'; diff --git a/src/platform/plugins/shared/controls/common/options_list/types.ts b/src/platform/plugins/shared/controls/common/options_list/types.ts index 94b4c9b162571..626320c0ee7e9 100644 --- a/src/platform/plugins/shared/controls/common/options_list/types.ts +++ b/src/platform/plugins/shared/controls/common/options_list/types.ts @@ -7,29 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { - OptionsListControlState, - OptionsListDSLControlState, - OptionsListESQLControlState, - OptionsListSelection, -} from '@kbn/controls-schemas'; +import type { OptionsListDSLControlState, OptionsListSelection } from '@kbn/controls-schemas'; import type { DataView, FieldSpec, RuntimeFieldSpec } from '@kbn/data-views-plugin/common'; import type { AggregateQuery, BoolQuery, Filter, Query, TimeRange } from '@kbn/es-query'; -/** - * ---------------------------------------------------------------- - * Options list state types - * ---------------------------------------------------------------- - */ - -export const isOptionsListESQLControlState = ( - state: OptionsListControlState | undefined -): state is OptionsListESQLControlState => - typeof state !== 'undefined' && - Object.hasOwn(state, 'esqlQuery') && - Object.hasOwn(state, 'controlType') && - !Object.hasOwn(state, 'fieldName'); - /** * ---------------------------------------------------------------- * Options list server request + response types diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx index aac114c8e9611..7db4ad980d934 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx @@ -22,7 +22,7 @@ import { } from 'rxjs'; import { OPTIONS_LIST_CONTROL, DEFAULT_DSL_OPTIONS_LIST_STATE } from '@kbn/controls-constants'; -import type { OptionsListSelection, OptionsListControlState } from '@kbn/controls-schemas'; +import type { OptionsListSelection, OptionsListDSLControlState } from '@kbn/controls-schemas'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { apiHasPinnedPanels, @@ -32,7 +32,7 @@ import { } from '@kbn/presentation-publishing'; import type { OptionsListSuccessResponse } from '../../../../common/options_list'; -import { isOptionsListESQLControlState, isValidSearch } from '../../../../common/options_list'; +import { isValidSearch } from '../../../../common/options_list'; import { defaultDataControlComparators, initializeDataControlManager, @@ -61,7 +61,7 @@ import { } from './utils/selection_utils'; export const getOptionsListControlFactory = (): EmbeddableFactory< - OptionsListControlState, + OptionsListDSLControlState, OptionsListControlApi > => { return { @@ -69,9 +69,6 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { const state = initialState; - if (isOptionsListESQLControlState(state)) { - throw new Error('ES|QL control state handling not yet implemented'); - } const editorStateManager = initializeEditorStateManager(state); const temporaryStateManager = initializeTemporayStateManager(); const selectionsManager = initializeSelectionsManager(state); @@ -235,7 +232,7 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< } ); - function serializeState(): OptionsListControlState { + function serializeState(): OptionsListDSLControlState { return { ...dataControlManager.getLatestState(), ...selectionsManager.getLatestState(), @@ -246,7 +243,7 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< }; } - const unsavedChangesApi = initializeUnsavedChanges({ + const unsavedChangesApi = initializeUnsavedChanges({ uuid, parentApi, serializeState, @@ -271,9 +268,6 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< exists_selected: false, }, onReset: (lastSaved) => { - if (isOptionsListESQLControlState(lastSaved)) { - throw new Error('ES|QL control state handling not yet implemented'); - } dataControlManager.reinitializeState(lastSaved); selectionsManager.reinitializeState(lastSaved); editorStateManager.reinitializeState(lastSaved); diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/types.ts b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/types.ts index 21848c7c3688e..5251ae69267e2 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/types.ts +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/types.ts @@ -10,10 +10,10 @@ import type { Subject } from 'rxjs'; import type { - OptionsListControlState, OptionsListSelection, OptionsListSortingType, DataControlState, + OptionsListDSLControlState, } from '@kbn/controls-schemas'; import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; import type { HasType, HasUniqueId, PublishingSubject } from '@kbn/presentation-publishing'; @@ -24,7 +24,7 @@ import type { SelectionsState } from './selections_manager'; import type { TemporaryState } from './temporay_state_manager'; import type { OptionsListPublishesOptions, OptionsListSelectionsApi } from '../../types'; -export type OptionsListControlApi = DefaultEmbeddableApi & +export type OptionsListControlApi = DefaultEmbeddableApi & DataControlApi & { setSelectedOptions: (options: OptionsListSelection[]) => void; clearSelections: () => void; diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.test.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.test.tsx index bf005b124760c..413ea8dcea013 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/esql_control_manager.test.tsx @@ -106,7 +106,6 @@ describe('initializeESQLControlManager', () => { "option2", ], "control_type": "STATIC_VALUES", - "esql_query": "", "selected_options": Array [ "option1", ], 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 f9136dc5a45ae..24104d4e68ee7 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 @@ -21,15 +21,19 @@ import { switchMap, } from 'rxjs'; -import { DEFAULT_ESQL_OPTIONS_LIST_STATE } from '@kbn/controls-constants'; import type { OptionsListESQLControlState, OptionsListSearchTechnique, OptionsListSelection, } from '@kbn/controls-schemas'; import type { TimeRange } from '@kbn/es-query'; -import type { ESQLControlVariable } from '@kbn/esql-types'; -import { ESQLVariableType, EsqlControlType } from '@kbn/esql-types'; +import type { ESQLControlVariable, QueryESQLControl } from '@kbn/esql-types'; +import { + EsqlControlType, + ESQLVariableType, + isQueryESQLControl, + isStaticESQLControl, +} from '@kbn/esql-types'; import { getESQLQueryVariables, hasStartEndParams } from '@kbn/esql-utils'; import { apiHasSections, @@ -41,6 +45,7 @@ import { import type { OptionsListSuggestions } from '../../../common/options_list'; import { dataService } from '../../services/kibana_services'; import { initializeTemporayStateManager } from '../data_controls/options_list_control/temporay_state_manager'; +import type { ESQLOptionsListRuntimeState } from './types'; import { castESQLValue } from './utils/esql_type_utils'; import { getESQLSingleColumnValues } from './utils/get_esql_single_column_values'; @@ -48,36 +53,38 @@ function selectedOptionsComparatorFunction(a?: OptionsListSelection[], b?: Optio return deepEqual(a ?? [], b ?? []); } -export const selectionComparators: StateComparators< - Pick< - OptionsListESQLControlState, - | 'selected_options' - | 'available_options' - | 'variable_name' - | 'single_select' - | 'variable_type' - | 'control_type' - | 'esql_query' - | 'title' - > -> = { - selected_options: selectedOptionsComparatorFunction, - available_options: (a, b, lastState, currentState) => { - // Only compare availableOptions for static values controls; values from query fetch these at runtime - if ( - lastState?.control_type === currentState?.control_type && - currentState?.control_type === EsqlControlType.VALUES_FROM_QUERY - ) { - return true; - } - return deepEqual(a ?? [], b ?? []); - }, - variable_name: 'referenceEquality', - variable_type: 'referenceEquality', - control_type: 'referenceEquality', - esql_query: 'referenceEquality', - title: 'referenceEquality', - single_select: 'referenceEquality', +type ComparatorsReturnType = + StateComparators< + Pick< + ESQLOptionsListRuntimeState, + | 'selected_options' + | 'variable_name' + | 'single_select' + | 'variable_type' + | 'control_type' + | 'title' + | 'available_options' + > & + (ControlType extends EsqlControlType.STATIC_VALUES + ? {} + : Pick) + >; + +export const getSelectionComparators = ( + controlType: Type +): ComparatorsReturnType => { + return { + selected_options: selectedOptionsComparatorFunction, + variable_name: 'referenceEquality', + variable_type: 'referenceEquality', + title: 'referenceEquality', + single_select: 'referenceEquality', + control_type: 'skip', // static + available_options: 'deepEquality', + ...(controlType === EsqlControlType.VALUES_FROM_QUERY + ? { esql_query: 'referenceEquality' } + : {}), + } as ComparatorsReturnType; }; export function initializeESQLControlManager( @@ -87,7 +94,13 @@ export function initializeESQLControlManager( setDataLoading: (loading: boolean) => void ) { const sectionId$ = apiHasSections(parentApi) ? parentApi.panelSection$(uuid) : of(undefined); - const availableOptions$ = new BehaviorSubject(initialState.available_options ?? []); + /** The control_type does not change during runtime, so it is safe to declare the type based on initial state */ + const isStaticControl = isStaticESQLControl(initialState); + const isEsqlQueryControl = !isStaticControl; + + const availableOptions$ = new BehaviorSubject( + isStaticControl ? initialState.available_options ?? [] : [] + ); const selectedOptions$ = new BehaviorSubject(initialState.selected_options); const hasSelections$ = new BehaviorSubject(false); // hardcoded to false to prevent clear action from appearing. const singleSelect$ = new BehaviorSubject(initialState.single_select); @@ -95,18 +108,15 @@ export function initializeESQLControlManager( const variableType$ = new BehaviorSubject( initialState.variable_type as ESQLVariableType ); - const controlType$ = new BehaviorSubject( - initialState.control_type as EsqlControlType - ); - const esqlQuery$ = new BehaviorSubject(initialState.esql_query); + const esqlQuery$ = new BehaviorSubject(isEsqlQueryControl ? initialState.esql_query : ''); let valuesColumnType: string | undefined; const totalCardinality$ = new BehaviorSubject( - initialState.available_options?.length ?? 0 + isStaticControl ? initialState.available_options.length : 0 ); const searchString$ = new BehaviorSubject(''); const displayedAvailableOptions$ = new BehaviorSubject>( - initialState.available_options?.map((value) => ({ value })) ?? [] + isStaticControl ? initialState.available_options.map((value) => ({ value })) : [] ); // Use it for incompatible suggestions @@ -148,79 +158,83 @@ export function initializeESQLControlManager( let previousTimeRange: TimeRange | undefined; let hasInitialFetch = false; let fetchAbortController = new AbortController(); - const fetchSubscription = fetch$({ uuid, parentApi }) - .pipe( - filter(() => controlType$.getValue() === EsqlControlType.VALUES_FROM_QUERY), - filter(({ esqlVariables, timeRange }) => { - if (hasStartEndParams(esqlQuery$.getValue()) && !timeRange) { - return false; - } - const variablesInQuery = getESQLQueryVariables(esqlQuery$.getValue()); - const variablesInParent = esqlVariables || []; - - // Filter out this control's own variable - const currentVariableName = variableName$.getValue(); - const externalVariables = variablesInParent.filter( - (variable) => variable.key !== currentVariableName - ); - - // Check if timeRange has changed - const timeRangeChanged = !deepEqual(previousTimeRange, timeRange); - - const shouldFetch = - !hasInitialFetch || - timeRangeChanged || - haveVariablesValuesChanged(externalVariables, previousESQLVariables, variablesInQuery); - - if (shouldFetch) { - previousESQLVariables = [...externalVariables]; - previousTimeRange = timeRange ? { ...timeRange } : undefined; - hasInitialFetch = true; - } - - return shouldFetch; - }), - switchMap(({ timeRange, esqlVariables }) => { - fetchAbortController.abort(); - fetchAbortController = new AbortController(); - const { signal } = fetchAbortController; - - setDataLoading(true); - const variablesInParent = esqlVariables || []; - - return from( - getESQLSingleColumnValues({ - query: esqlQuery$.getValue(), - search: dataService.search.search, - signal, - timeRange, - esqlVariables: variablesInParent, + const fetchSubscription = isStaticControl + ? undefined + : fetch$({ uuid, parentApi }) + .pipe( + filter(({ esqlVariables, timeRange }) => { + if (hasStartEndParams(esqlQuery$.getValue()) && !timeRange) { + return false; + } + const variablesInQuery = getESQLQueryVariables(esqlQuery$.getValue()); + const variablesInParent = esqlVariables || []; + + // Filter out this control's own variable + const currentVariableName = variableName$.getValue(); + const externalVariables = variablesInParent.filter( + (variable) => variable.key !== currentVariableName + ); + + // Check if timeRange has changed + const timeRangeChanged = !deepEqual(previousTimeRange, timeRange); + + const shouldFetch = + !hasInitialFetch || + timeRangeChanged || + haveVariablesValuesChanged( + externalVariables, + previousESQLVariables, + variablesInQuery + ); + + if (shouldFetch) { + previousESQLVariables = [...externalVariables]; + previousTimeRange = timeRange ? { ...timeRange } : undefined; + hasInitialFetch = true; + } + + return shouldFetch; + }), + switchMap(({ timeRange, esqlVariables }) => { + fetchAbortController.abort(); + fetchAbortController = new AbortController(); + const { signal } = fetchAbortController; + + setDataLoading(true); + const variablesInParent = esqlVariables || []; + return from( + getESQLSingleColumnValues({ + query: esqlQuery$.getValue(), + search: dataService.search.search, + signal, + timeRange, + esqlVariables: variablesInParent, + }) + ); }) - ); - }) - ) - .subscribe((result) => { - setDataLoading(false); - if (getESQLSingleColumnValues.isSuccess(result)) { - valuesColumnType = result.columnType; - const newAvailableOptions = result.values.map((value) => value); - availableOptions$.next(newAvailableOptions); - - // Check if current selections are still compatible - const currentSelections = selectedOptions$.getValue() ?? []; - const incompatibleSelections = new Set(); - - currentSelections.forEach((selection) => { - if (!newAvailableOptions.includes(selection)) { - incompatibleSelections.add(selection); + ) + .subscribe((result) => { + setDataLoading(false); + if (getESQLSingleColumnValues.isSuccess(result)) { + valuesColumnType = result.columnType; + const newAvailableOptions = result.values.map((value) => value); + availableOptions$.next(newAvailableOptions); + + // Check if current selections are still compatible + const currentSelections = selectedOptions$.getValue() ?? []; + const incompatibleSelections = new Set(); + + currentSelections.forEach((selection) => { + if (!newAvailableOptions.includes(selection)) { + incompatibleSelections.add(selection); + } + }); + + // Update incompatible selections + temporaryStateManager.api.setInvalidSelections(incompatibleSelections); } }); - // Update incompatible selections - temporaryStateManager.api.setInvalidSelections(incompatibleSelections); - } - }); - // Filter the displayed available options by the current search string // TODO: Run this filtering server-side instead of client side; this just replicates the basic behavior // of a combo box dropdown for keyboard accessibility @@ -237,8 +251,7 @@ export function initializeESQLControlManager( const getEsqlVariable = (sectionId?: string) => { const isSingleSelect = singleSelect$.value; const selectedValues = selectedOptions$.value; - const columnType = - controlType$.value === EsqlControlType.VALUES_FROM_QUERY ? valuesColumnType : undefined; + const columnType = isEsqlQueryControl ? valuesColumnType : undefined; // For single select, return the first value; for multi-select, return the array let value: ESQLControlVariable['value']; @@ -288,7 +301,7 @@ export function initializeESQLControlManager( cleanup: () => { fetchAbortController.abort(); variableSubscriptions.unsubscribe(); - fetchSubscription.unsubscribe(); + fetchSubscription?.unsubscribe(); availableOptionsSearchSubscription.unsubscribe(); }, api: { @@ -302,34 +315,34 @@ export function initializeESQLControlManager( variableName$, singleSelect$, variableType$, - controlType$, esqlQuery$ ).pipe(map(() => undefined)), - reinitializeState: (lastSaved?: OptionsListESQLControlState) => { + reinitializeState: (lastSaved?: ESQLOptionsListRuntimeState) => { setSelectedOptions(lastSaved?.selected_options ?? []); - availableOptions$.next(lastSaved?.available_options ?? []); variableName$.next(lastSaved?.variable_name ?? ''); singleSelect$.next(lastSaved?.single_select ?? true); variableType$.next((lastSaved?.variable_type as ESQLVariableType) ?? ESQLVariableType.VALUES); - if (lastSaved?.control_type) controlType$.next(lastSaved?.control_type as EsqlControlType); - esqlQuery$.next(lastSaved?.esql_query ?? ''); valuesColumnType = undefined; temporaryStateManager.api.setInvalidSelections(new Set()); previousESQLVariables = []; previousTimeRange = undefined; hasInitialFetch = false; + availableOptions$.next(lastSaved?.available_options ?? []); + esqlQuery$.next(isQueryESQLControl(lastSaved) ? lastSaved.esql_query : ''); }, getLatestState: (): OptionsListESQLControlState => { return { - selected_options: selectedOptions$.getValue() ?? [], - ...(controlType$.getValue() === EsqlControlType.STATIC_VALUES - ? { available_options: availableOptions$.getValue() ?? [] } - : {}), - variable_name: variableName$.getValue() ?? '', - single_select: singleSelect$.getValue() ?? DEFAULT_ESQL_OPTIONS_LIST_STATE.single_select, - variable_type: variableType$.getValue() ?? ESQLVariableType.VALUES, - control_type: controlType$.getValue(), - esql_query: esqlQuery$.getValue() ?? '', + selected_options: selectedOptions$.getValue(), + variable_name: variableName$.getValue(), + single_select: singleSelect$.getValue(), + variable_type: variableType$.getValue(), + /** The control_type is static from creation */ + ...(isStaticControl + ? { + control_type: EsqlControlType.STATIC_VALUES, + available_options: availableOptions$.getValue(), + } + : { control_type: EsqlControlType.VALUES_FROM_QUERY, esql_query: esqlQuery$.getValue() }), }; }, internalApi: { 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 3e14d64ab7335..3bfdcbf2c3c63 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 @@ -51,7 +51,6 @@ describe('ESQLControlApi', () => { available_options: ['option1', 'option2'], variable_name: 'variable1', variable_type: 'values', - esql_query: 'FROM foo | WHERE column = ?variable1', control_type: 'STATIC_VALUES', }; const { api } = await factory.buildEmbeddable({ @@ -78,8 +77,7 @@ describe('ESQLControlApi', () => { available_options: ['option1', 'option2'], variable_name: 'variable1', variable_type: 'values', - esql_query: 'FROM foo | WHERE column = ?variable1', - control_type: 'STATIC_VALUES', + control_type: EsqlControlType.STATIC_VALUES, }; const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), @@ -91,7 +89,6 @@ describe('ESQLControlApi', () => { expect(api.serializeState()).toStrictEqual({ available_options: ['option1', 'option2'], control_type: 'STATIC_VALUES', - esql_query: 'FROM foo | WHERE column = ?variable1', selected_options: ['option1'], title: undefined, variable_name: 'variable1', @@ -105,7 +102,6 @@ describe('ESQLControlApi', () => { const initialState: OptionsListESQLControlState = { ...DEFAULT_ESQL_OPTIONS_LIST_STATE, selected_options: ['option1'], - available_options: ['option1', 'option2'], variable_name: 'variable1', variable_type: 'values', esql_query: 'FROM foo | STATS BY column', @@ -171,7 +167,6 @@ describe('ESQLControlApi', () => { available_options: ['option1', 'option2'], variable_name: 'variable1', variable_type: 'values', - esql_query: 'FROM foo | WHERE column = ?variable1', control_type: 'STATIC_VALUES', }; const { Component, api } = await factory.buildEmbeddable({ 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 e6efadc6aa0cb..76c5a86a2d6d7 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 @@ -7,27 +7,42 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { pick } from 'lodash'; import React, { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { pick } from 'lodash'; import { ESQL_CONTROL } from '@kbn/controls-constants'; -import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; -import { apiPublishesESQLVariables } from '@kbn/esql-types'; -import { apiHasPinnedPanels, initializeUnsavedChanges } from '@kbn/presentation-publishing'; import type { OptionsListESQLControlState } from '@kbn/controls-schemas'; +import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { + apiPublishesESQLVariables, + isStaticESQLControl, + type QueryESQLControl, + type StaticESQLControl, +} from '@kbn/esql-types'; +import { + apiHasPinnedPanels, + initializeUnsavedChanges, + type StateComparators, +} from '@kbn/presentation-publishing'; import { uiActionsService } from '../../services/kibana_services'; +import { defaultControlLabelComparators, initializeLabelManager } from '../control_labels'; import { OptionsListControl } from '../data_controls/options_list_control/components/options_list_control'; import { OptionsListControlContext } from '../data_controls/options_list_control/options_list_context_provider'; -import { initializeESQLControlManager, selectionComparators } from './esql_control_manager'; -import type { ESQLControlApi, ESQLOptionsListComponentApi } from './types'; import { VariableControlsStrings } from './constants'; -import { initializeLabelManager, defaultControlLabelComparators } from '../control_labels'; +import { getSelectionComparators, initializeESQLControlManager } from './esql_control_manager'; +import type { + ESQLControlApi, + ESQLOptionsListComponentApi, + ESQLOptionsListRuntimeState, +} from './types'; -export const getESQLControlFactory = (): EmbeddableFactory< - OptionsListESQLControlState, - ESQLControlApi +export const getESQLControlFactory = < + State extends OptionsListESQLControlState = OptionsListESQLControlState +>(): EmbeddableFactory< + State extends { control_type: 'STATIC_VALUES' } ? StaticESQLControl : QueryESQLControl, + ESQLControlApi > => { return { type: ESQL_CONTROL, @@ -48,28 +63,31 @@ export const getESQLControlFactory = (): EmbeddableFactory< return { ...selections.getLatestState(), ...labelManager.getLatestState(), - }; + } as typeof initialState; } - const unsavedChangesApi = initializeUnsavedChanges({ + const unsavedChangesApi = initializeUnsavedChanges({ uuid, parentApi, serializeState, anyStateChange$: selections.anyStateChange$, getComparators: () => { return { - ...selectionComparators, + ...getSelectionComparators(state.control_type), ...defaultControlLabelComparators, display_settings: 'skip', - }; + } as StateComparators; }, onReset: (lastSaved) => { - selections.reinitializeState(lastSaved); + selections.reinitializeState({ + available_options: [], + ...lastSaved, + } as ESQLOptionsListRuntimeState); labelManager.reinitializeState(lastSaved); }, }); - const api: ESQLControlApi = finalizeApi({ + const api = finalizeApi({ ...unsavedChangesApi, ...selections.api, ...labelManager.api, @@ -95,13 +113,13 @@ export const getESQLControlFactory = (): EmbeddableFactory< const variablesInParent = apiPublishesESQLVariables(api.parentApi) ? api.parentApi.esqlVariables$.value : []; - const onSaveControl = async (updatedState: OptionsListESQLControlState) => { + const onSaveControl = async (updatedState: ESQLOptionsListRuntimeState) => { selections.reinitializeState(updatedState); labelManager.reinitializeState(updatedState); }; try { await uiActionsService.executeTriggerActions('ESQL_CONTROL_TRIGGER', { - queryString: nextState.esql_query, + queryString: isStaticESQLControl(nextState) ? '' : nextState.esql_query, variableType: nextState.variable_type, controlType: nextState.control_type, esqlVariables: variablesInParent, @@ -116,7 +134,7 @@ export const getESQLControlFactory = (): EmbeddableFactory< } }, serializeState, - }); + }) as ESQLControlApi; const componentApi: ESQLOptionsListComponentApi = { ...pick(api, ['dataLoading$', 'label$', 'type']), @@ -125,10 +143,11 @@ export const getESQLControlFactory = (): EmbeddableFactory< setDataLoading, makeSelection(key?: string) { - const singleSelect = selections.api.singleSelect$.value ?? true; - if (singleSelect && key) { + if (!key) return; + const singleSelect = selections.api.singleSelect$.value; + if (singleSelect) { selections.internalApi.setSelectedOptions([key]); - } else if (key) { + } else { // Get current selection state, not initial state const current = componentApi.selectedOptions$.value || []; const isSelected = current.includes(key); diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/types.ts b/src/platform/plugins/shared/controls/public/controls/esql_control/types.ts index 2cd15e4d642ae..783c3103b31ca 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/types.ts +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/types.ts @@ -11,7 +11,7 @@ import type { OptionsListSearchTechnique, } from '@kbn/controls-schemas'; import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; -import type { PublishesESQLVariable } from '@kbn/esql-types'; +import type { PublishesESQLVariable, QueryESQLControl, StaticESQLControl } from '@kbn/esql-types'; import type { HasEditCapabilities, HasType, @@ -24,12 +24,17 @@ import type { TemporaryState } from '../data_controls/options_list_control/tempo import type { OptionsListPublishesOptions, OptionsListSelectionsApi } from '../types'; import type { initializeLabelManager } from '../control_labels'; -export type ESQLControlApi = DefaultEmbeddableApi & +export type ESQLControlApi = DefaultEmbeddableApi< + State extends { control_type: 'STATIC_VALUES' } ? StaticESQLControl : QueryESQLControl +> & PublishesESQLVariable & HasEditCapabilities & PublishesDataLoading & ReturnType['api']; +export type ESQLOptionsListRuntimeState = Omit & + Pick; // both types have `available_options` during runtime + export type ESQLOptionsListComponentState = Pick< OptionsListESQLControlState, 'single_select' | 'selected_options' diff --git a/src/platform/plugins/shared/controls/server/transforms/esql_control_transforms.ts b/src/platform/plugins/shared/controls/server/transforms/esql_control_transforms.ts index 698fb12c98e03..c85226b033d7d 100644 --- a/src/platform/plugins/shared/controls/server/transforms/esql_control_transforms.ts +++ b/src/platform/plugins/shared/controls/server/transforms/esql_control_transforms.ts @@ -15,6 +15,7 @@ import { } from '@kbn/controls-schemas'; import type { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; import { convertCamelCasedKeysToSnakeCase } from '@kbn/presentation-publishing'; +import { EsqlControlType } from '@kbn/esql-types'; export const registerESQLControlTransforms = (embeddable: EmbeddableSetup) => { embeddable.registerTransforms(ESQL_CONTROL, { @@ -42,16 +43,26 @@ export const registerESQLControlTransforms = (embeddable: EmbeddableSetup) => { } = convertCamelCasedKeysToSnakeCase( state as LegacyStoredESQLControlExplicitInput ); - return { - available_options, + + const shared = { control_type: control_type as OptionsListESQLControlState['control_type'], display_settings, - esql_query: esql_query ?? '', selected_options: selected_options ?? DEFAULT_ESQL_OPTIONS_LIST_STATE.selected_options, single_select: single_select ?? DEFAULT_ESQL_OPTIONS_LIST_STATE.single_select, variable_name: variable_name ?? '', variable_type: variable_type as OptionsListESQLControlState['variable_type'], }; + return control_type === EsqlControlType.STATIC_VALUES + ? { + ...shared, + control_type: EsqlControlType.STATIC_VALUES, + available_options: available_options ?? [], + } + : { + ...shared, + control_type: EsqlControlType.VALUES_FROM_QUERY, + esql_query: esql_query ?? '', + }; }, }), }); diff --git a/src/platform/plugins/shared/dashboard/server/api/dashboard_state_schemas.ts b/src/platform/plugins/shared/dashboard/server/api/dashboard_state_schemas.ts index 26d3e8cb76389..73c328f97ced1 100644 --- a/src/platform/plugins/shared/dashboard/server/api/dashboard_state_schemas.ts +++ b/src/platform/plugins/shared/dashboard/server/api/dashboard_state_schemas.ts @@ -16,7 +16,7 @@ import { asCodeQuerySchema } from '@kbn/as-code-shared-schemas'; * Currently, controls are the only pinnable panels. However, if we intend to make this extendable, we should instead * get the pinned panel schema from a pinned panel registry **independent** from controls */ -import { controlsGroupSchema as pinnedPanelsSchema } from '@kbn/controls-schemas'; +import { getControlsGroupSchema as getPinnedPanelsSchema } from '@kbn/controls-schemas'; import { timeRangeSchema } from '@kbn/es-query-server'; import { embeddableService } from '../kibana_services'; import { DASHBOARD_GRID_COLUMN_COUNT } from '../../common/page_bundle_constants'; @@ -203,7 +203,7 @@ export const accessControlSchema = schema.maybe( export function getDashboardStateSchema(isDashboardAppRequest: boolean) { return schema.object( { - pinned_panels: pinnedPanelsSchema, + pinned_panels: getPinnedPanelsSchema(), description: schema.maybe(schema.string({ meta: { description: 'A short description.' } })), filters: schema.maybe(schema.arrayOf(asCodeFilterSchema, { maxSize: 500 })), options: optionsSchema, diff --git a/src/platform/plugins/shared/discover/public/__mocks__/esql_controls.ts b/src/platform/plugins/shared/discover/public/__mocks__/esql_controls.ts index c557a2b7b7073..200e4c86eee9e 100644 --- a/src/platform/plugins/shared/discover/public/__mocks__/esql_controls.ts +++ b/src/platform/plugins/shared/discover/public/__mocks__/esql_controls.ts @@ -13,7 +13,7 @@ import { DEFAULT_ESQL_OPTIONS_LIST_STATE, DEFAULT_PINNED_CONTROL_STATE, } from '@kbn/controls-constants'; -import type { EsqlControlType } from '@kbn/esql-types'; +import { EsqlControlType } from '@kbn/esql-types'; export const mockControlState: ControlPanelsState = { panel1: { @@ -25,8 +25,7 @@ export const mockControlState: ControlPanelsState = variable_name: 'foo', title: 'Panel 1', selected_options: ['bar'], - esql_query: '', - control_type: 'STATIC_VALUES' as EsqlControlType, + control_type: EsqlControlType.STATIC_VALUES, order: 0, }, }; diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/utils.test.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/utils.test.ts index d2cef9aa7571e..37a82cf78c854 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/utils.test.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/utils.test.ts @@ -124,7 +124,6 @@ describe('extractEsqlVariables', () => { width: 'medium', grow: false, control_type: EsqlControlType.STATIC_VALUES, - esql_query: '', }); it('should extract variables from control panels', () => { 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 3b29bf4110759..21a5ff1d99e79 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 @@ -15,7 +15,7 @@ import { isEqual } from 'lodash'; import { EuiComboBox, EuiFormRow, type EuiComboBoxOptionOption } from '@elastic/eui'; import type { monaco } from '@kbn/monaco'; import type { ISearchGeneric } from '@kbn/search-types'; -import type { ESQLControlVariable } from '@kbn/esql-types'; +import type { ESQLControlVariable, StaticESQLControl } from '@kbn/esql-types'; import { ESQLVariableType, EsqlControlType } from '@kbn/esql-types'; import type { OptionsListESQLControlState } from '@kbn/controls-schemas'; import { DEFAULT_ESQL_OPTIONS_LIST_STATE } from '@kbn/controls-constants'; @@ -48,21 +48,20 @@ export function IdentifierControlForm({ search, }: IdentifierControlFormProps) { const isMounted = useMountedState(); + const { available_options: initialAvailableOptions } = { available_options: [], ...initialState }; const [availableIdentifiersOptions, setAvailableIdentifiersOptions] = useState< EuiComboBoxOptionOption[] >([]); const [selectedIdentifiers, setSelectedIdentifiers] = useState( - initialState?.available_options - ? initialState.available_options.map((option) => { - return { - label: option, - key: option, - 'data-test-subj': option, - }; - }) - : [] + initialAvailableOptions.map((option) => { + return { + label: option, + key: option, + 'data-test-subj': option, + }; + }) ); const [label, setLabel] = useState(initialState?.title ?? ''); @@ -157,8 +156,7 @@ export function IdentifierControlForm({ title: label || variableNameWithoutQuestionmark, variable_name: variableNameWithoutQuestionmark, variable_type: variableType, - esql_query: queryString, - control_type: EsqlControlType.STATIC_VALUES, + control_type: EsqlControlType.STATIC_VALUES as StaticESQLControl['control_type'], }; if (!isEqual(state, initialState)) { setControlState(state); diff --git a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/index.tsx b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/index.tsx index 69c8cde81c86c..82e6cd44f9966 100644 --- a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/index.tsx +++ b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/index.tsx @@ -18,6 +18,7 @@ import { TelemetryControlCancelledReason, type ESQLControlVariable, type ControlTriggerSource, + isQueryESQLControl, } from '@kbn/esql-types'; import type { OptionsListESQLControlState } from '@kbn/controls-schemas'; import { getValuesFromQueryField } from '@kbn/esql-utils'; @@ -126,11 +127,12 @@ export function ESQLControlsFlyout({ ); const areValuesValid = useMemo(() => { - const available = controlState?.available_options ?? []; + if (!controlState || isQueryESQLControl(controlState)) return true; + const available = controlState.available_options; return variableType === ESQLVariableType.TIME_LITERAL ? areValuesIntervalsValid(available.map((option) => option)) : true; - }, [variableType, controlState?.available_options]); + }, [variableType, controlState]); const onVariableNameChange = useCallback( (e: { target: { value: React.SetStateAction } }) => { @@ -153,16 +155,17 @@ export function ESQLControlsFlyout({ const variableNameWithoutQuestionmark = variableName.replace(/^\?+/, ''); const variableExists = checkVariableExistence(esqlVariables, variableName) && !isControlInEditMode; + const { available_options } = { available_options: [], ...controlState }; setFormIsInvalid( !variableNameWithoutQuestionmark || variableExists || !areValuesValid || - !controlState?.available_options?.length + !available_options.length ); }, [ isControlInEditMode, areValuesValid, - controlState?.available_options?.length, + controlState, esqlVariables, variableName, variableType, @@ -173,7 +176,11 @@ export function ESQLControlsFlyout({ }, []); const onCreateControl = useCallback(async () => { - if (controlState && controlState.available_options?.length) { + if ( + controlState && + 'available_options' in controlState && + controlState.available_options.length + ) { if (!isControlInEditMode) { if (cursorPosition) { const query = updateQueryStringWithVariable(queryString, variableName, cursorPosition); diff --git a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/value_control_form.tsx b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/value_control_form.tsx index 93232770ecd48..b52f15658dd01 100644 --- a/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/value_control_form.tsx +++ b/src/platform/plugins/shared/esql/public/triggers/esql_controls/control_flyout/value_control_form.tsx @@ -23,6 +23,8 @@ import { ESQLVariableType, EsqlControlType, TIMEFIELD_ROUTE, + isQueryESQLControl, + isStaticESQLControl, type ESQLControlVariable, } from '@kbn/esql-types'; import type { OptionsListESQLControlState } from '@kbn/controls-schemas'; @@ -102,7 +104,7 @@ export function ValueControlForm({ ); const [selectedValues, setSelectedValues] = useState( - initialState?.available_options + isStaticESQLControl(initialState) ? initialState.available_options.map((option) => { return { label: option, @@ -115,7 +117,9 @@ export function ValueControlForm({ const [valuesQuery, setValuesQuery] = useState( variableType === ESQLVariableType.VALUES - ? initialState?.esql_query ?? INITIAL_EMPTY_STATE_QUERY + ? isQueryESQLControl(initialState) + ? initialState.esql_query + : INITIAL_EMPTY_STATE_QUERY : '' ); const [esqlQueryErrors, setEsqlQueryErrors] = useState(); @@ -240,7 +244,7 @@ export function ValueControlForm({ useEffect(() => { if (!selectedValues?.length && controlFlyoutType === EsqlControlType.VALUES_FROM_QUERY) { - if (initialState?.esql_query) { + if (isQueryESQLControl(initialState)) { onValuesQuerySubmit(initialState.esql_query); } else if (valuesRetrieval) { setSuggestedQuery(); @@ -249,7 +253,7 @@ export function ValueControlForm({ }, [ selectedValues?.length, controlFlyoutType, - initialState?.esql_query, + initialState, variableName, valuesRetrieval, onValuesQuerySubmit,