diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index 5a0080039e21a..f27298f371f07 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -7,7 +7,7 @@ */ import type { Filter, Query, BoolQuery, TimeRange } from '@kbn/es-query'; -import { FieldSpec, DataView } from '@kbn/data-views-plugin/common'; +import { FieldSpec, DataView, RuntimeFieldSpec } from '@kbn/data-views-plugin/common'; import { DataControlInput } from '../types'; @@ -57,9 +57,11 @@ export type OptionsListRequest = Omit< * The Options list request body is sent to the serverside Options List route and is used to create the ES query. */ export interface OptionsListRequestBody { + runtimeFieldMap?: Record; filters?: Array<{ bool: BoolQuery }>; selectedOptions?: string[]; runPastTimeout?: boolean; + parentFieldName?: string; textFieldName?: string; searchString?: string; fieldSpec?: FieldSpec; diff --git a/src/plugins/controls/public/control_group/editor/control_editor.tsx b/src/plugins/controls/public/control_group/editor/control_editor.tsx index 1e9c42ff420de..bce776a3922a3 100644 --- a/src/plugins/controls/public/control_group/editor/control_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_editor.tsx @@ -52,7 +52,7 @@ import { } from '../../types'; import { CONTROL_WIDTH_OPTIONS } from './editor_constants'; import { pluginServices } from '../../services'; -import { loadFieldRegistryFromDataViewId } from './data_control_editor_tools'; +import { getDataControlFieldRegistry } from './data_control_editor_tools'; interface EditControlProps { embeddable?: ControlEmbeddable; isCreate: boolean; @@ -116,10 +116,10 @@ export const ControlEditor = ({ useEffect(() => { (async () => { if (state.selectedDataView?.id) { - setFieldRegistry(await loadFieldRegistryFromDataViewId(state.selectedDataView.id)); + setFieldRegistry(await getDataControlFieldRegistry(await get(state.selectedDataView.id))); } })(); - }, [state.selectedDataView]); + }, [state.selectedDataView?.id, get]); useMount(() => { let mounted = true; diff --git a/src/plugins/controls/public/control_group/editor/data_control_editor_tools.ts b/src/plugins/controls/public/control_group/editor/data_control_editor_tools.ts index cb0d1db5f4a89..4344891280ce6 100644 --- a/src/plugins/controls/public/control_group/editor/data_control_editor_tools.ts +++ b/src/plugins/controls/public/control_group/editor/data_control_editor_tools.ts @@ -6,13 +6,20 @@ * Side Public License, v 1. */ +import { memoize } from 'lodash'; + import { IFieldSubTypeMulti } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/common'; import { pluginServices } from '../../services'; import { DataControlFieldRegistry, IEditableControlFactory } from '../../types'; -const dataControlFieldRegistryCache: { [key: string]: DataControlFieldRegistry } = {}; +export const getDataControlFieldRegistry = memoize( + async (dataView: DataView) => { + return await loadFieldRegistryFromDataView(dataView); + }, + (dataView: DataView) => [dataView.id, JSON.stringify(dataView.fields.getAll())].join('|') +); const doubleLinkFields = (dataView: DataView) => { // double link the parent-child relationship specifically for case-sensitivity support for options lists @@ -22,6 +29,7 @@ const doubleLinkFields = (dataView: DataView) => { if (!fieldRegistry[field.name]) { fieldRegistry[field.name] = { field, compatibleControlTypes: [] }; } + const parentFieldName = (field.subType as IFieldSubTypeMulti)?.multi?.parent; if (parentFieldName) { fieldRegistry[field.name].parentFieldName = parentFieldName; @@ -36,20 +44,13 @@ const doubleLinkFields = (dataView: DataView) => { return fieldRegistry; }; -export const loadFieldRegistryFromDataViewId = async ( - dataViewId: string +const loadFieldRegistryFromDataView = async ( + dataView: DataView ): Promise => { - if (dataControlFieldRegistryCache[dataViewId]) { - return dataControlFieldRegistryCache[dataViewId]; - } const { - dataViews, controls: { getControlTypes, getControlFactory }, } = pluginServices.getServices(); - const dataView = await dataViews.get(dataViewId); - const newFieldRegistry: DataControlFieldRegistry = doubleLinkFields(dataView); - const controlFactories = getControlTypes().map( (controlType) => getControlFactory(controlType) as IEditableControlFactory ); @@ -64,7 +65,6 @@ export const loadFieldRegistryFromDataViewId = async ( delete newFieldRegistry[dataViewField.name]; } }); - dataControlFieldRegistryCache[dataViewId] = newFieldRegistry; return newFieldRegistry; }; diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx index 35c251a179d09..06dd2b069bc44 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx @@ -43,7 +43,7 @@ import { ControlEmbeddable, ControlInput, ControlOutput, DataControlInput } from import { CreateControlButton, CreateControlButtonTypes } from '../editor/create_control'; import { CreateTimeSliderControlButton } from '../editor/create_time_slider_control'; import { TIME_SLIDER_CONTROL } from '../../time_slider'; -import { loadFieldRegistryFromDataViewId } from '../editor/data_control_editor_tools'; +import { getDataControlFieldRegistry } from '../editor/data_control_editor_tools'; let flyoutRef: OverlayRef | undefined; export const setFlyoutRef = (newRef: OverlayRef | undefined) => { @@ -102,7 +102,8 @@ export class ControlGroupContainer extends Container< fieldName: string; title?: string; }) { - const fieldRegistry = await loadFieldRegistryFromDataViewId(dataViewId); + const dataView = await pluginServices.getServices().dataViews.get(dataViewId); + const fieldRegistry = await getDataControlFieldRegistry(dataView); const field = fieldRegistry[fieldName]; return this.addNewEmbeddable(field.compatibleControlTypes[0], { id: uuid, diff --git a/src/plugins/controls/public/services/options_list/options_list_service.ts b/src/plugins/controls/public/services/options_list/options_list_service.ts index 27867b5724cec..857a363154b7b 100644 --- a/src/plugins/controls/public/services/options_list/options_list_service.ts +++ b/src/plugins/controls/public/services/options_list/options_list_service.ts @@ -89,6 +89,7 @@ class OptionsListService implements ControlsOptionsListService { fieldName: field.name, fieldSpec: field, textFieldName: (field as OptionsListField).textFieldName, + runtimeFieldMap: dataView.toSpec().runtimeFieldMap, }; }; diff --git a/src/plugins/controls/server/options_list/options_list_suggestions_route.ts b/src/plugins/controls/server/options_list/options_list_suggestions_route.ts index fe2218c3f7135..95900d82cbd83 100644 --- a/src/plugins/controls/server/options_list/options_list_suggestions_route.ts +++ b/src/plugins/controls/server/options_list/options_list_suggestions_route.ts @@ -85,8 +85,7 @@ export const setupOptionsListSuggestionsRoute = ( /** * Build ES Query */ - const { runPastTimeout, filters, fieldName } = request; - + const { runPastTimeout, filters, fieldName, runtimeFieldMap } = request; const { terminateAfter, timeout } = getAutocompleteSettings(); const timeoutSettings = runPastTimeout ? {} @@ -124,6 +123,9 @@ export const setupOptionsListSuggestionsRoute = ( }, }, }, + runtime_mappings: { + ...runtimeFieldMap, + }, }; /** diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 2a16d33775397..e7ec21d616482 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -22,47 +22,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); - const { dashboardControls, timePicker, console, common, dashboard, header } = getPageObjects([ - 'dashboardControls', - 'timePicker', - 'dashboard', - 'console', - 'common', - 'header', - ]); + const { dashboardControls, timePicker, console, common, dashboard, header, settings } = + getPageObjects([ + 'dashboardControls', + 'timePicker', + 'dashboard', + 'settings', + 'console', + 'common', + 'header', + ]); const DASHBOARD_NAME = 'Test Options List Control'; describe('Dashboard options list integration', () => { - const newDocuments: Array<{ index: string; id: string }> = []; - - const addDocument = async (index: string, document: string) => { - await console.enterRequest('\nPOST ' + index + '/_doc/ \n{\n ' + document); - await console.clickPlay(); + const returnToDashboard = async () => { + await common.navigateToApp('dashboard'); await header.waitUntilLoadingHasFinished(); - const response = JSON.parse(await console.getResponse()); - newDocuments.push({ index, id: response._id }); + await elasticChart.setNewChartUiDebugFlag(); + await dashboard.loadSavedDashboard(DASHBOARD_NAME); + if (await dashboard.getIsInViewMode()) { + await dashboard.switchToEditMode(); + } + await dashboard.waitForRenderComplete(); }; before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); - /* start by adding some incomplete data so that we can test `exists` query */ - await common.navigateToApp('console'); - await console.collapseHelp(); - await console.clearTextArea(); - await addDocument( - 'animals-cats-2018-01-01', - '"@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss"' - ); - - /* then, create our testing dashboard */ await common.navigateToApp('dashboard'); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); await timePicker.setDefaultDataRange(); await elasticChart.setNewChartUiDebugFlag(); - await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); + await dashboard.saveDashboard(DASHBOARD_NAME, { + exitFromEditMode: false, + storeTimeWithDashboard: true, + }); }); describe('Options List Control Editor selects relevant data views', async () => { @@ -392,56 +388,139 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await pieChart.getPieSliceCount()).to.be(2); await dashboard.clearUnsavedChanges(); }); + }); - describe('test exists query', async () => { - before(async () => { - await dashboardControls.deleteAllControls(); - await dashboardControls.createControl({ - controlType: OPTIONS_LIST_CONTROL, - dataViewTitle: 'animals-*', - fieldName: 'animal.keyword', - title: 'Animal', - }); - controlId = (await dashboardControls.getAllControlIds())[0]; - }); + describe('test data view runtime field', async () => { + const FIELD_NAME = 'testRuntimeField'; + const FIELD_VALUES = ['G', 'H', 'B', 'R', 'M']; - it('creating exists query has expected results', async () => { - expect((await pieChart.getPieChartValues())[0]).to.be(6); - await dashboardControls.optionsListOpenPopover(controlId); - await dashboardControls.optionsListPopoverSelectOption('exists'); - await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); - await dashboard.waitForRenderComplete(); + before(async () => { + await common.navigateToApp('settings'); + await settings.clickKibanaIndexPatterns(); + await settings.clickIndexPatternByName('animals-*'); + await settings.addRuntimeField( + FIELD_NAME, + 'keyword', + `emit(doc['sound.keyword'].value.substring(0, 1).toUpperCase())` + ); + await header.waitUntilLoadingHasFinished(); - expect(await pieChart.getPieSliceCount()).to.be(5); - expect((await pieChart.getPieChartValues())[0]).to.be(5); + await returnToDashboard(); + await dashboardControls.deleteAllControls(); + }); + + it('can create options list control on runtime field', async () => { + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, + fieldName: FIELD_NAME, + dataViewTitle: 'animals-*', }); + expect(await dashboardControls.getControlsCount()).to.be(1); + }); - it('negating exists query has expected results', async () => { - await dashboardControls.optionsListOpenPopover(controlId); - await dashboardControls.optionsListPopoverSetIncludeSelections(false); - await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); - await dashboard.waitForRenderComplete(); + it('new control has expected suggestions', async () => { + controlId = (await dashboardControls.getAllControlIds())[0]; + await ensureAvailableOptionsEql(FIELD_VALUES); + }); - expect(await pieChart.getPieSliceCount()).to.be(1); - expect((await pieChart.getPieChartValues())[0]).to.be(1); - }); + it('making selection has expected results', async () => { + await dashboardControls.optionsListOpenPopover(controlId); + await dashboardControls.optionsListPopoverSelectOption('B'); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); + expect(await pieChart.getPieChartLabels()).to.eql(['bark', 'bow ow ow']); }); after(async () => { await dashboardControls.deleteAllControls(); + await dashboard.clickQuickSave(); + await header.waitUntilLoadingHasFinished(); + + await common.navigateToApp('settings'); + await settings.clickKibanaIndexPatterns(); + await settings.clickIndexPatternByName('animals-*'); + await settings.filterField('testRuntimeField'); + await testSubjects.click('deleteField'); + await settings.confirmDelete(); + }); + }); + + describe('test exists query', async () => { + const newDocuments: Array<{ index: string; id: string }> = []; + + const addDocument = async (index: string, document: string) => { + await console.enterRequest('\nPOST ' + index + '/_doc/ \n{\n ' + document); + await console.clickPlay(); + await header.waitUntilLoadingHasFinished(); + const response = JSON.parse(await console.getResponse()); + newDocuments.push({ index, id: response._id }); + }; + + before(async () => { + await common.navigateToApp('console'); + await console.collapseHelp(); + await console.clearTextArea(); + await addDocument( + 'animals-cats-2018-01-01', + '"@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss"' + ); + await returnToDashboard(); await dashboardControls.createControl({ controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', - fieldName: 'sound.keyword', - title: 'Animal Sounds', + fieldName: 'animal.keyword', + title: 'Animal', }); controlId = (await dashboardControls.getAllControlIds())[0]; + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + }); + + it('creating exists query has expected results', async () => { + expect((await pieChart.getPieChartValues())[0]).to.be(6); + await dashboardControls.optionsListOpenPopover(controlId); + await dashboardControls.optionsListPopoverSelectOption('exists'); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); + await dashboard.waitForRenderComplete(); + + expect(await pieChart.getPieSliceCount()).to.be(5); + expect((await pieChart.getPieChartValues())[0]).to.be(5); + }); + + it('negating exists query has expected results', async () => { + await dashboardControls.optionsListOpenPopover(controlId); + await dashboardControls.optionsListPopoverSetIncludeSelections(false); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); + await dashboard.waitForRenderComplete(); + + expect(await pieChart.getPieSliceCount()).to.be(1); + expect((await pieChart.getPieChartValues())[0]).to.be(1); + }); + + after(async () => { + await common.navigateToApp('console'); + await console.clearTextArea(); + for (const { index, id } of newDocuments) { + await console.enterRequest(`\nDELETE /${index}/_doc/${id}`); + await console.clickPlay(); + await header.waitUntilLoadingHasFinished(); + } + + await returnToDashboard(); + await dashboardControls.deleteAllControls(); }); }); describe('Options List dashboard validation', async () => { before(async () => { + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, + dataViewTitle: 'animals-*', + fieldName: 'sound.keyword', + title: 'Animal Sounds', + }); + controlId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.optionsListOpenPopover(controlId); await dashboardControls.optionsListPopoverSelectOption('meow'); await dashboardControls.optionsListPopoverSelectOption('bark'); @@ -528,14 +607,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await common.navigateToApp('console'); - await console.collapseHelp(); - await console.clearTextArea(); - for (const { index, id } of newDocuments) { - await console.enterRequest(`\nDELETE /${index}/_doc/${id}`); - await console.clickPlay(); - await header.waitUntilLoadingHasFinished(); - } await security.testUser.restoreDefaults(); }); });