mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Dashboard] [Controls] Add unmapped runtime field support to options list (#144947)
Closes https://github.com/elastic/kibana/issues/144931 ## Summary This PR adds options list support for runtime fields that are added to **data views** - note that runtime fields that are added as part of an **index** (i.e. they are part of the mapping) already worked as expected. https://user-images.githubusercontent.com/8698078/201154529-588c7ecd-3a6a-4c51-a05e-97d9bacd7c18.mov ### How to Test - Create some data view runtime fields of various types as described in the [documentation](https://www.elastic.co/guide/en/kibana/master/managing-data-views.html#runtime-fields). For example, in the sample Logs data view, you could create the following: - **Keyword:** ```javascript emit(doc['referer'].value.splitOnToken('/')[2].replace(' ', '')) ``` - **Keyword with partial data** (to test the `exists` options list functionality): ```javascript if(doc['geo.dest'].value == 'US') { emit('Party in the USA') } ``` - **Boolean:** ```javascript emit(doc['geo.src'].value == doc['geo.dest'].value) ``` - **IP:** ```javascript emit(doc['clientip'].value) ``` - **Composite:** ``` javascript if(doc['geo.dest'].value == doc['geo.src'].value) { emit('english', 'equals'); emit('spanish', 'es igual'); emit('french', 'équivaut à'); return; } emit('english', 'does not equal'); emit('spanish', 'no es igual'); emit('french', 'n\'est pas égal'); ``` - Create options list controls on these new fields - Ensure that they work as expected, including chaining and validation. **Flaky Test Runner** <a href="https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1548"><img src="https://user-images.githubusercontent.com/8698078/201746854-1ab14130-86c5-46db-82c3-2cb6433c56a9.png"/></a> ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
24df1db3a5
commit
3190eddd5c
7 changed files with 163 additions and 86 deletions
|
@ -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<string, RuntimeFieldSpec>;
|
||||
filters?: Array<{ bool: BoolQuery }>;
|
||||
selectedOptions?: string[];
|
||||
runPastTimeout?: boolean;
|
||||
parentFieldName?: string;
|
||||
textFieldName?: string;
|
||||
searchString?: string;
|
||||
fieldSpec?: FieldSpec;
|
||||
|
|
|
@ -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<DataControlInput>;
|
||||
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;
|
||||
|
|
|
@ -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<DataControlFieldRegistry> => {
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -89,6 +89,7 @@ class OptionsListService implements ControlsOptionsListService {
|
|||
fieldName: field.name,
|
||||
fieldSpec: field,
|
||||
textFieldName: (field as OptionsListField).textFieldName,
|
||||
runtimeFieldMap: dataView.toSpec().runtimeFieldMap,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,44 +388,131 @@ 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'];
|
||||
|
||||
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();
|
||||
|
||||
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('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();
|
||||
it('new control has expected suggestions', async () => {
|
||||
controlId = (await dashboardControls.getAllControlIds())[0];
|
||||
await ensureAvailableOptionsEql(FIELD_VALUES);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
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: '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-*',
|
||||
|
@ -437,11 +520,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
title: 'Animal Sounds',
|
||||
});
|
||||
controlId = (await dashboardControls.getAllControlIds())[0];
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options List dashboard validation', async () => {
|
||||
before(async () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue