Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8cc598a
Extract esqlQueryToOptions, run on each control init
Zacqary Jun 23, 2025
7770834
Update ESQL options when use clicks refresh
Zacqary Jun 23, 2025
8248892
Switch reload$ subscription to controlFetch$
Zacqary Jun 24, 2025
6bef22f
Fix README, add typed results to esqlQueryToOptions
Zacqary Jun 24, 2025
f482804
Remove console.trace
Zacqary Jun 24, 2025
e9b9ad4
Update choose column button text to make more prominent
Zacqary Jun 24, 2025
9b9cf7c
Fix values control form test
Zacqary Jun 24, 2025
4dccf60
Add test for values from query update
Zacqary Jun 24, 2025
4c8cced
Merge remote-tracking branch 'upstream/main' into 222198-esql-control-rt
Zacqary Jun 24, 2025
02a14b4
[CI] Auto-commit changed files from 'node scripts/build_plugin_list_d…
kibanamachine Jun 24, 2025
94d744f
Fix presentation-controls jest config
Zacqary Jun 24, 2025
f382b4e
Add test for esqlQueryToOptions
Zacqary Jun 24, 2025
bea79e7
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Jun 24, 2025
29bcd37
Fix tsconfig
Zacqary Jun 24, 2025
3dcb2ec
Merge remote-tracking branch 'upstream/main' into 222198-esql-control-rt
Zacqary Jun 25, 2025
62e6dff
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Jun 25, 2025
eb4ebe2
Fix typecheck
Zacqary Jun 25, 2025
94c31fb
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 25, 2025
a539e68
Fix lint
Zacqary Jun 25, 2025
0e4846a
Merge branch '222198-esql-control-rt' of https://github.com/Zacqary/k…
Zacqary Jun 25, 2025
72b53b7
Remove kbn-presentation-controls and move new function to esql-utils
Zacqary Jun 25, 2025
d52e0b1
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Jun 25, 2025
c3f2737
Fix value control form jest
Zacqary Jun 25, 2025
3978d1b
Merge branch '222198-esql-control-rt' of https://github.com/Zacqary/k…
Zacqary Jun 25, 2025
702b116
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Jun 25, 2025
e298842
Fix esql-utils circular ref
Zacqary Jun 25, 2025
63d9977
Merge branch '222198-esql-control-rt' of https://github.com/Zacqary/k…
Zacqary Jun 25, 2025
1c1a0d1
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 25, 2025
b6fd740
Fix query to options test
Zacqary Jun 25, 2025
9a798e6
Add timeRange to esqlQueryToOptions test
Zacqary Jun 25, 2025
20ac465
Merge branch '222198-esql-control-rt' of https://github.com/Zacqary/k…
Zacqary Jun 25, 2025
5061ef5
Elastic company policy prohibits unit tests from being any fun
Zacqary Jun 26, 2025
d5eff07
Remove esqlQueryToOptions from esql-utils
Zacqary Jun 26, 2025
80d8f0b
Rename esqlQueryToOptions => getESQLSingleColumnValues
Zacqary Jun 26, 2025
2da3896
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Jun 26, 2025
279735d
Remove controls optional plugin
Zacqary Jun 26, 2025
ced7251
Fix controls kibana.jsonc
Zacqary Jun 26, 2025
b7044f3
Revert "Fix controls kibana.jsonc"
Zacqary Jun 26, 2025
950b0ad
Rename esqlQueryToOptions => getESQLSingleColumnValues
Zacqary Jun 26, 2025
a9a0aaf
Sync update schema of getESQLSingleColumnValues
Zacqary Jun 27, 2025
efb01f3
Fix jest
Zacqary Jun 27, 2025
583c491
Fix typecheck
Zacqary Jun 27, 2025
7637726
Fix jest
Zacqary Jun 27, 2025
396b8c1
Merge remote-tracking branch 'upstream/main' into 222198-esql-control-rt
Zacqary Jun 27, 2025
b6a547e
Revert "Fix jest"
Zacqary Jun 27, 2025
1470e33
Revert cahgnes to esql plugin
Zacqary Jun 27, 2025
03e2ce6
Move getESQLSingleColumnValues into controls plugin
Zacqary Jun 27, 2025
9e49d01
Revert tsconfig changes
Zacqary Jun 27, 2025
1d6a991
Remove code from kbn/esql-utils
Zacqary Jun 27, 2025
68c786c
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Jun 27, 2025
8378974
Fix race condition in inializeESQLControlSelections
Zacqary Jun 27, 2025
3f9075a
Fix jest
Zacqary Jun 27, 2025
89b05d8
Remove columns from getESQLSingleColumnValues return type
Zacqary Jun 30, 2025
0b51062
Merge remote-tracking branch 'upstream/main' into 222198-esql-control-rt
Zacqary Jun 30, 2025
d97cb71
Fix jest
Zacqary Jun 30, 2025
File filter

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 @@ -7,10 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import deepEqual from 'react-fast-compare';
import { BehaviorSubject, combineLatest, map, merge } from 'rxjs';
import type { ESQLControlVariable, ESQLControlState, EsqlControlType } from '@kbn/esql-types';
import { ESQLVariableType } from '@kbn/esql-types';
import { BehaviorSubject, combineLatest, filter, map, merge, switchMap } from 'rxjs';
import {
ESQLControlVariable,
ESQLControlState,
EsqlControlType,
ESQLVariableType,
} from '@kbn/esql-types';
import { PublishingSubject, StateComparators } from '@kbn/presentation-publishing';
import { dataService } from '../../services/kibana_services';
import { ControlGroupApi } from '../../control_group/types';
import { getESQLSingleColumnValues } from './utils/get_esql_single_column_values';

function selectedOptionsComparatorFunction(a?: string[], b?: string[]) {
return deepEqual(a ?? [], b ?? []);
Expand All @@ -37,7 +44,10 @@ export const selectionComparators: StateComparators<
title: 'referenceEquality',
};

export function initializeESQLControlSelections(initialState: ESQLControlState) {
export function initializeESQLControlSelections(
initialState: ESQLControlState,
controlFetch$: ReturnType<ControlGroupApi['controlFetch$']>
) {
const availableOptions$ = new BehaviorSubject<string[]>(initialState.availableOptions ?? []);
const selectedOptions$ = new BehaviorSubject<string[]>(initialState.selectedOptions ?? []);
const hasSelections$ = new BehaviorSubject<boolean>(false); // hardcoded to false to prevent clear action from appearing.
Expand All @@ -55,6 +65,25 @@ export function initializeESQLControlSelections(initialState: ESQLControlState)
}
}

// For Values From Query controls, update values on dashboard load/reload
const fetchSubscription = controlFetch$
.pipe(
filter(() => controlType$.getValue() === EsqlControlType.VALUES_FROM_QUERY),
switchMap(
async ({ timeRange }) =>
await getESQLSingleColumnValues({
query: esqlQuery$.getValue(),
search: dataService.search.search,
timeRange,
})
)
)
.subscribe((result) => {
if (getESQLSingleColumnValues.isSuccess(result)) {
availableOptions$.next(result.values);
}
});

// derive ESQL control variable from state.
const getEsqlVariable = () => ({
key: variableName$.value,
Expand All @@ -64,12 +93,18 @@ export function initializeESQLControlSelections(initialState: ESQLControlState)
type: variableType$.value,
});
const esqlVariable$ = new BehaviorSubject<ESQLControlVariable>(getEsqlVariable());
const subscriptions = combineLatest([variableName$, variableType$, selectedOptions$]).subscribe(
() => esqlVariable$.next(getEsqlVariable())
);
const variableSubscriptions = combineLatest([
variableName$,
variableType$,
selectedOptions$,
availableOptions$,
]).subscribe(() => esqlVariable$.next(getEsqlVariable()));

return {
cleanup: () => subscriptions.unsubscribe(),
cleanup: () => {
variableSubscriptions.unsubscribe();
fetchSubscription.unsubscribe();
},
api: {
hasSelections$: hasSelections$ as PublishingSubject<boolean | undefined>,
esqlVariable$: esqlVariable$ as PublishingSubject<ESQLControlVariable>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@

import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import type { ESQLControlState } from '@kbn/esql-types';
import { EsqlControlType, type ESQLControlState } from '@kbn/esql-types';
import { getMockedControlGroupApi, getMockedFinalizeApi } from '../mocks/control_mocks';
import { getESQLControlFactory } from './get_esql_control_factory';
import { BehaviorSubject } from 'rxjs';
import { ControlFetchContext } from '../../control_group/control_fetch';

const mockGetESQLSingleColumnValues = jest.fn(() => ({ options: ['option1', 'option2'] }));
const mockIsSuccess = jest.fn(() => true);

jest.mock('./utils/get_esql_single_column_values', () => {
const getESQLSingleColumnValues = () => mockGetESQLSingleColumnValues();
getESQLSingleColumnValues.isSuccess = () => mockIsSuccess();
return {
getESQLSingleColumnValues,
};
});

describe('ESQLControlApi', () => {
beforeEach(() => {
jest.resetAllMocks();
});

const uuid = 'myESQLControl';

const dashboardApi = {};
const controlGroupApi = getMockedControlGroupApi(dashboardApi);
const factory = getESQLControlFactory();
const finalizeApi = getMockedFinalizeApi(uuid, factory, controlGroupApi);

test('Should publish ES|QL variable', async () => {
test('should publish ES|QL variable', async () => {
const initialState = {
selectedOptions: ['option1'],
availableOptions: ['option1', 'option2'],
Expand All @@ -43,7 +60,7 @@ describe('ESQLControlApi', () => {
});
});

test('Should serialize state', async () => {
test('should serialize state', async () => {
const initialState = {
selectedOptions: ['option1'],
availableOptions: ['option1', 'option2'],
Expand Down Expand Up @@ -74,37 +91,70 @@ describe('ESQLControlApi', () => {
});
});

test('changing the dropdown should publish new ES|QL variable', async () => {
const initialState = {
selectedOptions: ['option1'],
availableOptions: ['option1', 'option2'],
variableName: 'variable1',
variableType: 'values',
esqlQuery: 'FROM foo | WHERE column = ?variable1',
controlType: 'STATIC_VALUES',
} as ESQLControlState;
const { Component, api } = await factory.buildControl({
initialState,
finalizeApi,
uuid,
controlGroupApi,
});

expect(api.esqlVariable$.value).toStrictEqual({
key: 'variable1',
type: 'values',
value: 'option1',
describe('values from query', () => {
test('should update on load and fetch', async () => {
const initialState = {
selectedOptions: ['option1'],
availableOptions: ['option1', 'option2'],
variableName: 'variable1',
variableType: 'values',
esqlQuery: 'FROM foo | STATS BY column',
controlType: EsqlControlType.VALUES_FROM_QUERY,
} as ESQLControlState;
await factory.buildControl({
initialState,
finalizeApi,
uuid,
controlGroupApi,
});
await waitFor(() => {
expect(mockGetESQLSingleColumnValues).toHaveBeenCalledTimes(1);
expect(mockIsSuccess).toHaveBeenCalledTimes(1);
});
const controlFetch$ = controlGroupApi.controlFetch$(
uuid
) as BehaviorSubject<ControlFetchContext>;
controlFetch$.next({});
await waitFor(() => {
expect(mockGetESQLSingleColumnValues).toHaveBeenCalledTimes(2);
expect(mockIsSuccess).toHaveBeenCalledTimes(2);
});
});
});

const { findByTestId, findByTitle } = render(<Component className="" />);
fireEvent.click(await findByTestId('comboBoxSearchInput'));
fireEvent.click(await findByTitle('option2'));
describe('changing the dropdown', () => {
test('should publish new ES|QL variable', async () => {
const initialState = {
selectedOptions: ['option1'],
availableOptions: ['option1', 'option2'],
variableName: 'variable1',
variableType: 'values',
esqlQuery: 'FROM foo | WHERE column = ?variable1',
controlType: 'STATIC_VALUES',
} as ESQLControlState;
const { Component, api } = await factory.buildControl({
initialState,
finalizeApi,
uuid,
controlGroupApi,
});

await waitFor(() => {
expect(api.esqlVariable$.value).toStrictEqual({
key: 'variable1',
type: 'values',
value: 'option2',
value: 'option1',
});

const { findByTestId, findByTitle } = render(<Component className="" />);
fireEvent.click(await findByTestId('comboBoxSearchInput'));
fireEvent.click(await findByTitle('option2'));

await waitFor(() => {
expect(api.esqlVariable$.value).toStrictEqual({
key: 'variable1',
type: 'values',
value: 'option2',
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ export const getESQLControlFactory = (): ControlFactory<ESQLControlState, ESQLCo
getDisplayName: () => displayName,
buildControl: async ({ initialState, finalizeApi, uuid, controlGroupApi }) => {
const defaultControlManager = initializeDefaultControlManager(initialState);
const selections = initializeESQLControlSelections(initialState);
const selections = initializeESQLControlSelections(
initialState,
controlGroupApi.controlFetch$(uuid)
);

const closeOverlay = () => {
if (apiHasParentApi(controlGroupApi) && tracksOverlays(controlGroupApi.parentApi)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 { ISearchGeneric } from '@kbn/search-types';
import {
getESQLSingleColumnValues,
GetESQLSingleColumnValuesSuccess,
GetESQLSingleColumnValuesFailure,
} from './get_esql_single_column_values';

const mockGetESQLResults = jest.fn();
jest.mock('@kbn/esql-utils', () => ({
getESQLResults: (...args: any[]) => mockGetESQLResults(...args),
}));

const searchMock = {} as ISearchGeneric;

describe('getESQLSingleColumnValues', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('returns only options on success', async () => {
mockGetESQLResults.mockResolvedValueOnce({
response: {
columns: [{ name: 'column1' }],
values: [['option1'], ['option2']],
},
});
const result = (await getESQLSingleColumnValues({
query: 'FROM index | STATS BY column',
search: searchMock,
})) as GetESQLSingleColumnValuesSuccess;
expect(getESQLSingleColumnValues.isSuccess(result)).toBe(true);
expect(result).toMatchInlineSnapshot(`
Object {
"values": Array [
"option1",
"option2",
],
}
`);
});
it('returns an error when query returns multiple columns', async () => {
mockGetESQLResults.mockResolvedValueOnce({
response: {
columns: [{ name: 'column1' }, { name: 'column2' }],
values: [['option1'], ['option2']],
},
});
const result = (await getESQLSingleColumnValues({
query: 'FROM index',
search: searchMock,
})) as GetESQLSingleColumnValuesFailure;
expect(getESQLSingleColumnValues.isSuccess(result)).toBe(false);
expect('values' in result).toBe(false);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
[Error: Query must return a single column],
],
}
`);
});
it('returns an error on a failed query', async () => {
mockGetESQLResults.mockRejectedValueOnce('Invalid ES|QL query');
const result = (await getESQLSingleColumnValues({
query: 'FROM index | EVAL',
search: searchMock,
})) as GetESQLSingleColumnValuesFailure;
expect(getESQLSingleColumnValues.isSuccess(result)).toBe(false);
expect('values' in result).toBe(false);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
"Invalid ES|QL query",
],
}
`);
});

it('passes timeRange successfully', async () => {
const timeRange = { from: 'now-10m', to: 'now' };
await getESQLSingleColumnValues({
query: 'FROM index | STATS BY column',
search: searchMock,
timeRange,
});
expect(mockGetESQLResults).toHaveBeenCalledWith(
expect.objectContaining({
timeRange,
})
);
});
});
Loading
Loading