Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { CodeEditorProps } from '@kbn/code-editor';
import { CodeEditor } from '@kbn/code-editor';
import type { CoreStart } from '@kbn/core/public';
import type { AggregateQuery, TimeRange } from '@kbn/es-query';
import type { FieldType } from '@kbn/esql-ast';
import { type FieldType } from '@kbn/esql-ast';
import type { ESQLFieldWithMetadata } from '@kbn/esql-ast/src/commands_registry/types';
import type { ESQLTelemetryCallbacks } from '@kbn/esql-types';
import {
Expand Down Expand Up @@ -286,16 +286,15 @@ const ESQLEditorInternal = function ESQLEditor({

// Enable the variables service if the feature is supported in the consumer app
useEffect(() => {
const variables = variablesService?.esqlVariables;
if (!isEqual(variables, esqlVariables)) {
variablesService?.clearVariables();
esqlVariables?.forEach((variable) => {
variablesService?.addVariable(variable);
});
}
if (controlsContext?.supportsControls) {
variablesService?.enableSuggestions();

const variables = variablesService?.esqlVariables;
if (!isEqual(variables, esqlVariables)) {
variablesService?.clearVariables();
esqlVariables?.forEach((variable) => {
variablesService?.addVariable(variable);
});
}
} else {
variablesService?.disableSuggestions();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,26 +277,25 @@ async function handleDefaultContext(ctx: ExpressionContext): Promise<ISuggestion
suggestions.push(...builder.build());
}

// Suggest control variables (e.g., "?fieldName", "$valueName") if supported and not already present
if (context?.supportsControls) {
const hasControl = suggestions.some((suggestion) =>
suggestion.command?.id?.includes('esql.control')
);
// Suggest control variables (e.g., "??fieldName", "?valueName") if supported and not already present
const hasControl = suggestions.some((suggestion) =>
suggestion.command?.id?.includes('esql.control')
);

if (!hasControl) {
const prefix = getVariablePrefix(controlType);
const variableNames =
context.variables
?.filter(({ type }) => type === controlType)
.map(({ key }) => `${prefix}${key}`) ?? [];

const controlSuggestions = getControlSuggestion(
controlType,
ControlTriggerSource.SMART_SUGGESTION,
variableNames
);
suggestions.push(...controlSuggestions);
}
if (!hasControl) {
const prefix = getVariablePrefix(controlType);
const variableNames =
context?.variables
?.filter(({ type }) => type === controlType)
.map(({ key }) => `${prefix}${key}`) ?? [];

const controlSuggestions = getControlSuggestion(
controlType,
ControlTriggerSource.SMART_SUGGESTION,
variableNames,
Boolean(context?.supportsControls)
);
suggestions.push(...controlSuggestions);
}

return suggestions;
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change here is:

  • if the supportsControl flag is enabled we display the "Create control" suggestion
  • if variables exist we suggest them regardless of the supportsControl flag

Original file line number Diff line number Diff line change
Expand Up @@ -314,27 +314,32 @@ export const columnExists = (col: string, context?: ICommandContext) =>
export function getControlSuggestion(
type: ESQLVariableType,
triggerSource: ControlTriggerSource,
variables?: string[]
variables?: string[],
suggestCreation = true
): ISuggestionItem[] {
return [
{
label: i18n.translate('kbn-esql-ast.esql.autocomplete.createControlLabel', {
defaultMessage: 'Create control',
}),
text: '',
kind: 'Issue',
detail: i18n.translate('kbn-esql-ast.esql.autocomplete.createControlDetailLabel', {
defaultMessage: 'Click to create',
}),
sortText: '1',
command: {
id: `esql.control.${type}.create`,
title: i18n.translate('kbn-esql-ast.esql.autocomplete.createControlDetailLabel', {
defaultMessage: 'Click to create',
}),
arguments: [{ triggerSource }],
},
} as ISuggestionItem,
...(suggestCreation
? [
{
label: i18n.translate('kbn-esql-ast.esql.autocomplete.createControlLabel', {
defaultMessage: 'Create control',
}),
text: '',
kind: 'Issue',
detail: i18n.translate('kbn-esql-ast.esql.autocomplete.createControlDetailLabel', {
defaultMessage: 'Click to create',
}),
sortText: '1',
command: {
id: `esql.control.${type}.create`,
title: i18n.translate('kbn-esql-ast.esql.autocomplete.createControlDetailLabel', {
defaultMessage: 'Click to create',
}),
arguments: [{ triggerSource }],
},
} as ISuggestionItem,
]
: []),
...(variables?.length
? buildConstantsDefinitions(
variables,
Expand All @@ -359,17 +364,14 @@ export function getControlSuggestionIfSupported(
variables?: ESQLControlVariable[],
shouldBePrefixed = true
) {
if (!supportsControls) {
return [];
}

const prefix = shouldBePrefixed ? getVariablePrefix(type) : '';
const filteredVariables = variables?.filter((variable) => variable.type === type) ?? [];

const controlSuggestion = getControlSuggestion(
type,
triggerSource,
filteredVariables?.map((v) => `${prefix}${v.key}`)
filteredVariables?.map((v) => `${prefix}${v.key}`),
supportsControls
);

return controlSuggestion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,20 +425,18 @@ export const buildColumnSuggestions = (
});

const suggestions = [...fieldsSuggestions];
if (options?.supportsControls) {
const variableType = options?.variableType ?? ESQLVariableType.FIELDS;
const userDefinedColumns =
variables?.filter((variable) => variable.type === variableType) ?? [];

const controlSuggestions = columns.length
? getControlSuggestion(
variableType,
ControlTriggerSource.SMART_SUGGESTION,
userDefinedColumns?.map((v) => `${getVariablePrefix(variableType)}${v.key}`)
)
: [];
suggestions.push(...controlSuggestions);
}
const variableType = options?.variableType ?? ESQLVariableType.FIELDS;
const userDefinedColumns = variables?.filter((variable) => variable.type === variableType) ?? [];

const controlSuggestions = columns.length
? getControlSuggestion(
variableType,
ControlTriggerSource.SMART_SUGGESTION,
userDefinedColumns?.map((v) => `${getVariablePrefix(variableType)}${v.key}`),
Boolean(options?.supportsControls)
)
: [];
suggestions.push(...controlSuggestions);

return [...suggestions];
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import type { ESQLControlVariable } from '@kbn/esql-types';
import type { Observable } from 'rxjs';
import { combineLatest, debounceTime, map } from 'rxjs';
import type { ChainingContext } from './chaining';
Expand All @@ -17,15 +18,17 @@ export interface ControlFetchContext {
filters?: Filter[] | undefined;
query?: Query | AggregateQuery | undefined;
timeRange?: TimeRange | undefined;
esqlVariables?: ESQLControlVariable[] | undefined;
}

export function controlFetch$(
chaining$: Observable<ChainingContext>,
controlGroupFetch$: Observable<ControlGroupFetchContext>
controlGroupFetch$: Observable<ControlGroupFetchContext>,
esqlVariables$: Observable<ESQLControlVariable[]>
): Observable<ControlFetchContext> {
return combineLatest([chaining$, controlGroupFetch$]).pipe(
return combineLatest([chaining$, controlGroupFetch$, esqlVariables$]).pipe(
debounceTime(0),
map(([chainingContext, controlGroupFetchContext]) => {
map(([chainingContext, controlGroupFetchContext, esqlVariables]) => {
const filters = [];
if (controlGroupFetchContext.unifiedSearchFilters) {
filters.push(...controlGroupFetchContext.unifiedSearchFilters);
Expand All @@ -38,6 +41,7 @@ export function controlFetch$(
filters,
query: controlGroupFetchContext.query,
timeRange: chainingContext.timeRange ?? controlGroupFetchContext.timeRange,
esqlVariables,
};
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export const getControlGroupEmbeddableFactory = () => {
editorStateManager.api.ignoreParentSettings$,
parentApi ? parentApi : {},
onReload
)
),
esqlVariables$
),
ignoreParentSettings$: editorStateManager.api.ignoreParentSettings$,
autoApplySelections$: editorStateManager.api.autoApplySelections$,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const OptionsListControl = ({
disableMultiValueEmptySelection?: boolean;
}) => {
const popoverId = useMemo(() => htmlIdGenerator()(), []);
const { componentApi, displaySettings } = useOptionsListContext();
const { componentApi, displaySettings, customStrings } = useOptionsListContext();
Copy link
Contributor Author

@stratoula stratoula Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invalid selection label is not making sense here as the value is valid (the chart works) but is not part of the updated list.

In order to introduce a new custom label I add a customStrings option in the context. It can be a property too. Let me know what you prefer.


const [isPopoverOpen, setPopoverOpen] = useState<boolean>(false);
const [
Expand Down Expand Up @@ -152,9 +152,10 @@ export const OptionsListControl = ({
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={OptionsListStrings.control.getInvalidSelectionWarningLabel(
invalidSelections.size
)}
content={
customStrings?.invalidSelectionsLabel ??
OptionsListStrings.control.getInvalidSelectionWarningLabel(invalidSelections.size)
}
delay="long"
>
<EuiToken
Expand All @@ -164,9 +165,12 @@ export const OptionsListControl = ({
color="euiColorVis9"
shape="square"
fill="dark"
title={OptionsListStrings.control.getInvalidSelectionWarningLabel(
invalidSelections.size
)}
title={
customStrings?.invalidSelectionsLabel ??
OptionsListStrings.control.getInvalidSelectionWarningLabel(
invalidSelections.size
)
}
data-test-subj={`optionsList__invalidSelectionsToken-${componentApi.uuid}`}
css={styles.invalidSelectionsToken} // Align with the notification badge
/>
Expand All @@ -185,6 +189,7 @@ export const OptionsListControl = ({
invalidSelections,
componentApi.uuid,
styles,
customStrings,
]);

const button = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const optionsListPopoverInvalidSelectionsStyles = {
};

export const OptionsListPopoverInvalidSelections = () => {
const { componentApi } = useOptionsListContext();
const { componentApi, customStrings } = useOptionsListContext();
const styles = useMemoCss(optionsListPopoverInvalidSelectionsStyles);

const [invalidSelections, fieldFormatter] = useBatchedPublishingSubjects(
Expand Down Expand Up @@ -66,15 +66,16 @@ export const OptionsListPopoverInvalidSelections = () => {
prepend: (
<EuiScreenReaderOnly>
<div>
{OptionsListStrings.popover.getInvalidSelectionScreenReaderText()}
{customStrings?.invalidSelectionsLabel ||
OptionsListStrings.popover.getInvalidSelectionScreenReaderText()}
{'" "'} {/* Adds a pause for the screen reader */}
</div>
</EuiScreenReaderOnly>
),
};
});
setSelectableOptions(options);
}, [fieldFormatter, invalidSelections]);
}, [fieldFormatter, invalidSelections, customStrings]);

return (
<>
Expand All @@ -84,22 +85,29 @@ export const OptionsListPopoverInvalidSelections = () => {
<EuiFlexItem grow={false}>
<EuiIcon
type="warning"
title={OptionsListStrings.popover.getInvalidSelectionScreenReaderText()}
title={
customStrings?.invalidSelectionsLabel ||
OptionsListStrings.popover.getInvalidSelectionScreenReaderText()
}
size="s"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<label>
{OptionsListStrings.popover.getInvalidSelectionsSectionTitle(invalidSelections.size)}
{customStrings?.invalidSelectionsLabel ||
OptionsListStrings.popover.getInvalidSelectionsSectionTitle(invalidSelections.size)}
</label>
</EuiFlexItem>
</EuiFlexGroup>
</EuiTitle>
<EuiSelectable
aria-label={OptionsListStrings.popover.getInvalidSelectionsSectionAriaLabel(
defaultPanelTitle ?? '',
invalidSelections.size
)}
aria-label={
customStrings?.invalidSelectionsLabel ||
OptionsListStrings.popover.getInvalidSelectionsSectionAriaLabel(
defaultPanelTitle ?? '',
invalidSelections.size
)
}
options={selectableOptions}
listProps={{ onFocusBadge: false }}
onChange={(newSuggestions, _, changedOption) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import React, { useContext } from 'react';
import type { OptionsListDisplaySettings } from '../../../../common/options_list';
import type { OptionsListComponentApi } from './types';

export interface OptionsListCustomStrings {
invalidSelectionsLabel?: string;
}

export const OptionsListControlContext = React.createContext<
| {
componentApi: OptionsListComponentApi;
displaySettings: OptionsListDisplaySettings;
customStrings?: OptionsListCustomStrings;
}
| undefined
>(undefined);
Expand Down
Loading