[Console] Refactor retrieval of mappings, aliases, templates, data-streams for autocomplete (#130633)

* Create a specific route for fetching mappings, aliases, templates, etc...
* Encapsulate data streams

* Encapsulate the mappings data into a class
* Setup up autocompleteInfo service and provide its instance through context
* Migrate the logic from mappings.js to Kibana server
* Translate the logic to consume the appropriate ES client method
* Update related test cases

* Lint

* Address comments

* Fix server proxy/mock

* Add API integration tests for /api/console/autocomplete_entities

* Lint

* Add tests
* Add API integration tests for autocomplete_entities API
* Add deleted tests

Co-authored-by: Muhammad Ibragimov <muhammad.ibragimov@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Muhammad Ibragimov 2022-05-19 14:30:59 +05:00 committed by GitHub
parent 59120c9340
commit b7866ac7f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1306 additions and 777 deletions

View file

@ -130,12 +130,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex
| Deprecated API | Reference location(s) | Remove By |
| ---------------|-----------|-----------|
| <DocLink id="kibDataViewsPluginApi" section="def-public.IndexPatternField" text="IndexPatternField"/> | [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField)+ 16 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-public.IndexPatternField" text="IndexPatternField"/> | [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField)+ 16 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-public.IndexPatternsContract" text="IndexPatternsContract"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [create_search_source.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.test.ts#:~:text=IndexPatternsContract)+ 19 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-public.IndexPatternsService" text="IndexPatternsService"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternsService) | - |
| <DocLink id="kibDataViewsPluginApi" section="def-public.IndexPattern" text="IndexPattern"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/types.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPattern), [tabify_docs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/tabify/tabify_docs.ts#:~:text=IndexPattern), [tabify_docs.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/tabify/tabify_docs.ts#:~:text=IndexPattern)+ 89 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-common.IFieldType" text="IFieldType"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IFieldType), [date_histogram.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/buckets/date_histogram.ts#:~:text=IFieldType), [date_histogram.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/buckets/date_histogram.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [generate_filters.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#:~:text=IFieldType), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IFieldType), [create_filters_from_range_select.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts#:~:text=IFieldType), [create_filters_from_range_select.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts#:~:text=IFieldType)+ 6 more | 8.2 |
| <DocLink id="kibDataViewsPluginApi" section="def-common.IndexPatternField" text="IndexPatternField"/> | [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField)+ 16 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-common.IndexPatternField" text="IndexPatternField"/> | [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [mapping.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/aggs/param_types/mapping.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [kibana_context_type.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/kibana_context_type.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IndexPatternField)+ 16 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-common.IIndexPattern" text="IIndexPattern"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [get_time.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/query/timefilter/get_time.ts#:~:text=IIndexPattern), [normalize_sort_request.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/normalize_sort_request.ts#:~:text=IIndexPattern), [normalize_sort_request.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/normalize_sort_request.ts#:~:text=IIndexPattern), [normalize_sort_request.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/normalize_sort_request.ts#:~:text=IIndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IIndexPattern), [search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source.ts#:~:text=IIndexPattern)+ 23 more | - |
| <DocLink id="kibDataViewsPluginApi" section="def-common.IndexPatternAttributes" text="IndexPatternAttributes"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternAttributes), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/index.ts#:~:text=IndexPatternAttributes), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/server/index.ts#:~:text=IndexPatternAttributes) | - |
| <DocLink id="kibDataViewsPluginApi" section="def-common.IndexPatternsContract" text="IndexPatternsContract"/> | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/index.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [create_search_source.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [search_source_service.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/search_source_service.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [esaggs_fn.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/public/search/types.ts#:~:text=IndexPatternsContract), [create_search_source.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data/common/search/search_source/create_search_source.test.ts#:~:text=IndexPatternsContract)+ 19 more | - |

View file

@ -16,10 +16,6 @@ jest.mock('../../../../contexts/editor_context/editor_registry', () => ({
},
}));
jest.mock('../../../../components/editor_example', () => {});
jest.mock('../../../../../lib/mappings/mappings', () => ({
retrieveAutoCompleteInfo: () => {},
clearSubscriptions: () => {},
}));
jest.mock('../../../../models/sense_editor', () => {
return {
create: () => ({

View file

@ -20,8 +20,6 @@ import { decompressFromEncodedURIComponent } from 'lz-string';
import { parse } from 'query-string';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import { ace } from '@kbn/es-ui-shared-plugin/public';
// @ts-ignore
import { retrieveAutoCompleteInfo, clearSubscriptions } from '../../../../../lib/mappings/mappings';
import { ConsoleMenu } from '../../../../components';
import { useEditorReadContext, useServicesContext } from '../../../../contexts';
import {
@ -66,7 +64,14 @@ const inputId = 'ConAppInputTextarea';
function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) {
const {
services: { history, notifications, settings: settingsService, esHostService, http },
services: {
history,
notifications,
settings: settingsService,
esHostService,
http,
autocompleteInfo,
},
docLinkVersion,
} = useServicesContext();
@ -196,14 +201,14 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) {
setInputEditor(editor);
setTextArea(editorRef.current!.querySelector('textarea'));
retrieveAutoCompleteInfo(http, settingsService, settingsService.getAutocomplete());
autocompleteInfo.retrieve(settingsService, settingsService.getAutocomplete());
const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor);
setupAutosave();
return () => {
unsubscribeResizer();
clearSubscriptions();
autocompleteInfo.clearSubscriptions();
window.removeEventListener('hashchange', onHashChange);
if (editorInstanceRef.current) {
editorInstanceRef.current.getCoreEditor().destroy();
@ -217,6 +222,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) {
setInputEditor,
settingsService,
http,
autocompleteInfo,
]);
useEffect(() => {

View file

@ -8,11 +8,8 @@
import React from 'react';
import type { HttpSetup } from '@kbn/core/public';
import { AutocompleteOptions, DevToolsSettingsModal } from '../components';
// @ts-ignore
import { retrieveAutoCompleteInfo } from '../../lib/mappings/mappings';
import { useServicesContext, useEditorActionContext } from '../contexts';
import { DevToolsSettings, Settings as SettingsService } from '../../services';
import type { SenseEditor } from '../models';
@ -27,48 +24,6 @@ const getAutocompleteDiff = (
}) as AutocompleteOptions[];
};
const refreshAutocompleteSettings = (
http: HttpSetup,
settings: SettingsService,
selectedSettings: DevToolsSettings['autocomplete']
) => {
retrieveAutoCompleteInfo(http, settings, selectedSettings);
};
const fetchAutocompleteSettingsIfNeeded = (
http: HttpSetup,
settings: SettingsService,
newSettings: DevToolsSettings,
prevSettings: DevToolsSettings
) => {
// We'll only retrieve settings if polling is on. The expectation here is that if the user
// disables polling it's because they want manual control over the fetch request (possibly
// because it's a very expensive request given their cluster and bandwidth). In that case,
// they would be unhappy with any request that's sent automatically.
if (newSettings.polling) {
const autocompleteDiff = getAutocompleteDiff(newSettings, prevSettings);
const isSettingsChanged = autocompleteDiff.length > 0;
const isPollingChanged = prevSettings.polling !== newSettings.polling;
if (isSettingsChanged) {
// If the user has changed one of the autocomplete settings, then we'll fetch just the
// ones which have changed.
const changedSettings: DevToolsSettings['autocomplete'] = autocompleteDiff.reduce(
(changedSettingsAccum, setting) => {
changedSettingsAccum[setting] = newSettings.autocomplete[setting];
return changedSettingsAccum;
},
{} as DevToolsSettings['autocomplete']
);
retrieveAutoCompleteInfo(http, settings, changedSettings);
} else if (isPollingChanged && newSettings.polling) {
// If the user has turned polling on, then we'll fetch all selected autocomplete settings.
retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete());
}
}
};
export interface Props {
onClose: () => void;
editorInstance: SenseEditor | null;
@ -76,14 +31,57 @@ export interface Props {
export function Settings({ onClose, editorInstance }: Props) {
const {
services: { settings, http },
services: { settings, autocompleteInfo },
} = useServicesContext();
const dispatch = useEditorActionContext();
const refreshAutocompleteSettings = (
settingsService: SettingsService,
selectedSettings: DevToolsSettings['autocomplete']
) => {
autocompleteInfo.retrieve(settingsService, selectedSettings);
};
const fetchAutocompleteSettingsIfNeeded = (
settingsService: SettingsService,
newSettings: DevToolsSettings,
prevSettings: DevToolsSettings
) => {
// We'll only retrieve settings if polling is on. The expectation here is that if the user
// disables polling it's because they want manual control over the fetch request (possibly
// because it's a very expensive request given their cluster and bandwidth). In that case,
// they would be unhappy with any request that's sent automatically.
if (newSettings.polling) {
const autocompleteDiff = getAutocompleteDiff(newSettings, prevSettings);
const isSettingsChanged = autocompleteDiff.length > 0;
const isPollingChanged = prevSettings.polling !== newSettings.polling;
if (isSettingsChanged) {
// If the user has changed one of the autocomplete settings, then we'll fetch just the
// ones which have changed.
const changedSettings: DevToolsSettings['autocomplete'] = autocompleteDiff.reduce(
(changedSettingsAccum, setting) => {
changedSettingsAccum[setting] = newSettings.autocomplete[setting];
return changedSettingsAccum;
},
{} as DevToolsSettings['autocomplete']
);
autocompleteInfo.retrieve(settingsService, {
...settingsService.getAutocomplete(),
...changedSettings,
});
} else if (isPollingChanged && newSettings.polling) {
// If the user has turned polling on, then we'll fetch all selected autocomplete settings.
autocompleteInfo.retrieve(settingsService, settingsService.getAutocomplete());
}
}
};
const onSaveSettings = (newSettings: DevToolsSettings) => {
const prevSettings = settings.toJSON();
fetchAutocompleteSettingsIfNeeded(http, settings, newSettings, prevSettings);
fetchAutocompleteSettingsIfNeeded(settings, newSettings, prevSettings);
// Update the new settings in localStorage
settings.updateSettings(newSettings);
@ -101,7 +99,7 @@ export function Settings({ onClose, editorInstance }: Props) {
onClose={onClose}
onSaveSettings={onSaveSettings}
refreshAutocompleteSettings={(selectedSettings) =>
refreshAutocompleteSettings(http, settings, selectedSettings)
refreshAutocompleteSettings(settings, selectedSettings)
}
settings={settings.toJSON()}
editorInstance={editorInstance}

View file

@ -17,6 +17,7 @@ import type { ObjectStorageClient } from '../../../common/types';
import { HistoryMock } from '../../services/history.mock';
import { SettingsMock } from '../../services/settings.mock';
import { StorageMock } from '../../services/storage.mock';
import { AutocompleteInfoMock } from '../../services/autocomplete.mock';
import { createApi, createEsHostService } from '../lib';
import { ContextValue } from './services_context';
@ -38,6 +39,7 @@ export const serviceContextMock = {
notifications: notificationServiceMock.createSetupContract(),
objectStorageClient: {} as unknown as ObjectStorageClient,
http,
autocompleteInfo: new AutocompleteInfoMock(),
},
docLinkVersion: 'NA',
theme$: themeServiceMock.create().start().theme$,

View file

@ -10,7 +10,7 @@ import React, { createContext, useContext, useEffect } from 'react';
import { Observable } from 'rxjs';
import type { NotificationsSetup, CoreTheme, DocLinksStart, HttpSetup } from '@kbn/core/public';
import { History, Settings, Storage } from '../../services';
import { AutocompleteInfo, History, Settings, Storage } from '../../services';
import { ObjectStorageClient } from '../../../common/types';
import { MetricsTracker } from '../../types';
import { EsHostService } from '../lib';
@ -24,6 +24,7 @@ interface ContextServices {
trackUiMetric: MetricsTracker;
esHostService: EsHostService;
http: HttpSetup;
autocompleteInfo: AutocompleteInfo;
}
export interface ContextValue {

View file

@ -11,8 +11,6 @@ import { useCallback } from 'react';
import { toMountPoint } from '../../../shared_imports';
import { isQuotaExceededError } from '../../../services/history';
// @ts-ignore
import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings';
import { instance as registry } from '../../contexts/editor_context/editor_registry';
import { useRequestActionContext, useServicesContext } from '../../contexts';
import { StorageQuotaError } from '../../components/storage_quota_error';
@ -21,7 +19,7 @@ import { track } from './track';
export const useSendCurrentRequest = () => {
const {
services: { history, settings, notifications, trackUiMetric, http },
services: { history, settings, notifications, trackUiMetric, http, autocompleteInfo },
theme$,
} = useServicesContext();
@ -102,7 +100,7 @@ export const useSendCurrentRequest = () => {
// or templates may have changed, so we'll need to update this data. Assume that if
// the user disables polling they're trying to optimize performance or otherwise
// preserve resources, so they won't want this request sent either.
retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete());
autocompleteInfo.retrieve(settings, settings.getAutocomplete());
}
dispatch({
@ -129,5 +127,14 @@ export const useSendCurrentRequest = () => {
});
}
}
}, [dispatch, http, settings, notifications.toasts, trackUiMetric, history, theme$]);
}, [
dispatch,
http,
settings,
notifications.toasts,
trackUiMetric,
history,
theme$,
autocompleteInfo,
]);
};

View file

@ -19,7 +19,7 @@ import {
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { KibanaThemeProvider } from '../shared_imports';
import { createStorage, createHistory, createSettings } from '../services';
import { createStorage, createHistory, createSettings, AutocompleteInfo } from '../services';
import { createUsageTracker } from '../services/tracker';
import * as localStorageObjectClient from '../lib/local_storage_object_client';
import { Main } from './containers';
@ -35,6 +35,7 @@ export interface BootDependencies {
element: HTMLElement;
theme$: Observable<CoreTheme>;
docLinks: DocLinksStart['links'];
autocompleteInfo: AutocompleteInfo;
}
export function renderApp({
@ -46,6 +47,7 @@ export function renderApp({
http,
theme$,
docLinks,
autocompleteInfo,
}: BootDependencies) {
const trackUiMetric = createUsageTracker(usageCollection);
trackUiMetric.load('opened_app');
@ -76,6 +78,7 @@ export function renderApp({
trackUiMetric,
objectStorageClient,
http,
autocompleteInfo,
},
theme$,
}}

View file

@ -12,10 +12,12 @@ import _ from 'lodash';
import $ from 'jquery';
import * as kb from '../../../lib/kb/kb';
import * as mappings from '../../../lib/mappings/mappings';
import { AutocompleteInfo, setAutocompleteInfo } from '../../../services';
describe('Integration', () => {
let senseEditor;
let autocompleteInfo;
beforeEach(() => {
// Set up our document body
document.body.innerHTML =
@ -24,10 +26,14 @@ describe('Integration', () => {
senseEditor = create(document.querySelector('#ConAppEditor'));
$(senseEditor.getCoreEditor().getContainer()).show();
senseEditor.autocomplete._test.removeChangeListener();
autocompleteInfo = new AutocompleteInfo();
setAutocompleteInfo(autocompleteInfo);
});
afterEach(() => {
$(senseEditor.getCoreEditor().getContainer()).hide();
senseEditor.autocomplete._test.addChangeListener();
autocompleteInfo = null;
setAutocompleteInfo(null);
});
function processContextTest(data, mapping, kbSchemes, requestLine, testToRun) {
@ -45,8 +51,8 @@ describe('Integration', () => {
testToRun.cursor.lineNumber += lineOffset;
mappings.clear();
mappings.loadMappings(mapping);
autocompleteInfo.clear();
autocompleteInfo.mapping.loadMappings(mapping);
const json = {};
json[test.name] = kbSchemes || {};
const testApi = kb._test.loadApisFromJson(json);

View file

@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
import { getComponentTemplates } from '../../mappings/mappings';
import { getAutocompleteInfo } from '../../../services';
import { ListComponent } from './list_component';
export class ComponentTemplateAutocompleteComponent extends ListComponent {
constructor(name, parent) {
super(name, getComponentTemplates, parent, true, true);
super(name, getAutocompleteInfo().getEntityProvider('componentTemplates'), parent, true, true);
}
getContextKey() {

View file

@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
import { getDataStreams } from '../../mappings/mappings';
import { ListComponent } from './list_component';
import { getAutocompleteInfo } from '../../../services';
export class DataStreamAutocompleteComponent extends ListComponent {
constructor(name, parent, multiValued) {
super(name, getDataStreams, parent, multiValued);
super(name, getAutocompleteInfo().getEntityProvider('dataStreams'), parent, multiValued);
}
getContextKey() {

View file

@ -7,11 +7,11 @@
*/
import _ from 'lodash';
import { getFields } from '../../mappings/mappings';
import { getAutocompleteInfo } from '../../../services';
import { ListComponent } from './list_component';
function FieldGenerator(context) {
return _.map(getFields(context.indices, context.types), function (field) {
return _.map(getAutocompleteInfo().getEntityProvider('fields', context), function (field) {
return { name: field.name, meta: field.type };
});
}

View file

@ -7,14 +7,16 @@
*/
import _ from 'lodash';
import { getIndices } from '../../mappings/mappings';
import { getAutocompleteInfo } from '../../../services';
import { ListComponent } from './list_component';
function nonValidIndexType(token) {
return !(token === '_all' || token[0] !== '_');
}
export class IndexAutocompleteComponent extends ListComponent {
constructor(name, parent, multiValued) {
super(name, getIndices, parent, multiValued);
super(name, getAutocompleteInfo().getEntityProvider('indices'), parent, multiValued);
}
validateTokens(tokens) {
if (!this.multiValued && tokens.length > 1) {

View file

@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
import { getIndexTemplates } from '../../mappings/mappings';
import { getAutocompleteInfo } from '../../../services';
import { ListComponent } from './list_component';
export class IndexTemplateAutocompleteComponent extends ListComponent {
constructor(name, parent) {
super(name, getIndexTemplates, parent, true, true);
super(name, getAutocompleteInfo().getEntityProvider('indexTemplates'), parent, true, true);
}
getContextKey() {

View file

@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
import { getLegacyTemplates } from '../../../mappings/mappings';
import { getAutocompleteInfo } from '../../../../services';
import { ListComponent } from '../list_component';
export class LegacyTemplateAutocompleteComponent extends ListComponent {
constructor(name, parent) {
super(name, getLegacyTemplates, parent, true, true);
super(name, getAutocompleteInfo().getEntityProvider('legacyTemplates'), parent, true, true);
}
getContextKey() {
return 'template';

View file

@ -8,7 +8,7 @@
import _ from 'lodash';
import { ListComponent } from './list_component';
import { getTypes } from '../../mappings/mappings';
import { getTypes } from '../../autocomplete_entities';
function TypeGenerator(context) {
return getTypes(context.indices);
}

View file

@ -7,14 +7,16 @@
*/
import _ from 'lodash';
import { getIndices } from '../../mappings/mappings';
import { getAutocompleteInfo } from '../../../services';
import { ListComponent } from './list_component';
function nonValidUsernameType(token) {
return token[0] === '_';
}
export class UsernameAutocompleteComponent extends ListComponent {
constructor(name, parent, multiValued) {
super(name, getIndices, parent, multiValued);
super(name, getAutocompleteInfo().getEntityProvider('indices'), parent, multiValued);
}
validateTokens(tokens) {
if (!this.multiValued && tokens.length > 1) {

View file

@ -0,0 +1,65 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { IndicesGetAliasResponse } from '@elastic/elasticsearch/lib/api/types';
import type { BaseMapping } from './mapping';
interface BaseAlias {
getIndices(includeAliases: boolean, collaborator: BaseMapping): string[];
loadAliases(aliases: IndicesGetAliasResponse, collaborator: BaseMapping): void;
clearAliases(): void;
}
export class Alias implements BaseAlias {
public perAliasIndexes: Record<string, string[]> = {};
getIndices = (includeAliases: boolean, collaborator: BaseMapping): string[] => {
const ret: string[] = [];
const perIndexTypes = collaborator.perIndexTypes;
Object.keys(perIndexTypes).forEach((index) => {
// ignore .ds* indices in the suggested indices list.
if (!index.startsWith('.ds')) {
ret.push(index);
}
});
if (typeof includeAliases === 'undefined' ? true : includeAliases) {
Object.keys(this.perAliasIndexes).forEach((alias) => {
ret.push(alias);
});
}
return ret;
};
loadAliases = (aliases: IndicesGetAliasResponse, collaborator: BaseMapping) => {
this.perAliasIndexes = {};
const perIndexTypes = collaborator.perIndexTypes;
Object.entries(aliases).forEach(([index, indexAliases]) => {
// verify we have an index defined. useful when mapping loading is disabled
perIndexTypes[index] = perIndexTypes[index] || {};
Object.keys(indexAliases.aliases || {}).forEach((alias) => {
if (alias === index) {
return;
} // alias which is identical to index means no index.
let curAliases = this.perAliasIndexes[alias];
if (!curAliases) {
curAliases = [];
this.perAliasIndexes[alias] = curAliases;
}
curAliases.push(index);
});
});
const includeAliases = false;
this.perAliasIndexes._all = this.getIndices(includeAliases, collaborator);
};
clearAliases = () => {
this.perAliasIndexes = {};
};
}

View file

@ -0,0 +1,315 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import '../../application/models/sense_editor/sense_editor.test.mocks';
import { setAutocompleteInfo, AutocompleteInfo } from '../../services';
import { expandAliases } from './expand_aliases';
function fc(f1, f2) {
if (f1.name < f2.name) {
return -1;
}
if (f1.name > f2.name) {
return 1;
}
return 0;
}
function f(name, type) {
return { name, type: type || 'string' };
}
describe('Autocomplete entities', () => {
let mapping;
let alias;
let legacyTemplate;
let indexTemplate;
let componentTemplate;
let dataStream;
let autocompleteInfo;
beforeEach(() => {
autocompleteInfo = new AutocompleteInfo();
setAutocompleteInfo(autocompleteInfo);
mapping = autocompleteInfo.mapping;
alias = autocompleteInfo.alias;
legacyTemplate = autocompleteInfo.legacyTemplate;
indexTemplate = autocompleteInfo.indexTemplate;
componentTemplate = autocompleteInfo.componentTemplate;
dataStream = autocompleteInfo.dataStream;
});
afterEach(() => {
autocompleteInfo.clear();
autocompleteInfo = null;
});
describe('Mappings', function () {
test('Multi fields 1.0 style', function () {
mapping.loadMappings({
index: {
properties: {
first_name: {
type: 'string',
index: 'analyzed',
path: 'just_name',
fields: {
any_name: { type: 'string', index: 'analyzed' },
},
},
last_name: {
type: 'string',
index: 'no',
fields: {
raw: { type: 'string', index: 'analyzed' },
},
},
},
},
});
expect(mapping.getMappings('index').sort(fc)).toEqual([
f('any_name', 'string'),
f('first_name', 'string'),
f('last_name', 'string'),
f('last_name.raw', 'string'),
]);
});
test('Simple fields', function () {
mapping.loadMappings({
index: {
properties: {
str: {
type: 'string',
},
number: {
type: 'int',
},
},
},
});
expect(mapping.getMappings('index').sort(fc)).toEqual([
f('number', 'int'),
f('str', 'string'),
]);
});
test('Simple fields - 1.0 style', function () {
mapping.loadMappings({
index: {
mappings: {
properties: {
str: {
type: 'string',
},
number: {
type: 'int',
},
},
},
},
});
expect(mapping.getMappings('index').sort(fc)).toEqual([
f('number', 'int'),
f('str', 'string'),
]);
});
test('Nested fields', function () {
mapping.loadMappings({
index: {
properties: {
person: {
type: 'object',
properties: {
name: {
properties: {
first_name: { type: 'string' },
last_name: { type: 'string' },
},
},
sid: { type: 'string', index: 'not_analyzed' },
},
},
message: { type: 'string' },
},
},
});
expect(mapping.getMappings('index', []).sort(fc)).toEqual([
f('message'),
f('person.name.first_name'),
f('person.name.last_name'),
f('person.sid'),
]);
});
test('Enabled fields', function () {
mapping.loadMappings({
index: {
properties: {
person: {
type: 'object',
properties: {
name: {
type: 'object',
enabled: false,
},
sid: { type: 'string', index: 'not_analyzed' },
},
},
message: { type: 'string' },
},
},
});
expect(mapping.getMappings('index', []).sort(fc)).toEqual([f('message'), f('person.sid')]);
});
test('Path tests', function () {
mapping.loadMappings({
index: {
properties: {
name1: {
type: 'object',
path: 'just_name',
properties: {
first1: { type: 'string' },
last1: { type: 'string', index_name: 'i_last_1' },
},
},
name2: {
type: 'object',
path: 'full',
properties: {
first2: { type: 'string' },
last2: { type: 'string', index_name: 'i_last_2' },
},
},
},
},
});
expect(mapping.getMappings().sort(fc)).toEqual([
f('first1'),
f('i_last_1'),
f('name2.first2'),
f('name2.i_last_2'),
]);
});
test('Use index_name tests', function () {
mapping.loadMappings({
index: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
});
expect(mapping.getMappings().sort(fc)).toEqual([f('i_last_1')]);
});
});
describe('Aliases', function () {
test('Aliases', function () {
alias.loadAliases(
{
test_index1: {
aliases: {
alias1: {},
},
},
test_index2: {
aliases: {
alias2: {
filter: {
term: {
FIELD: 'VALUE',
},
},
},
alias1: {},
},
},
},
mapping
);
mapping.loadMappings({
test_index1: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
test_index2: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
});
expect(alias.getIndices(true, mapping).sort()).toEqual([
'_all',
'alias1',
'alias2',
'test_index1',
'test_index2',
]);
expect(alias.getIndices(false, mapping).sort()).toEqual(['test_index1', 'test_index2']);
expect(expandAliases(['alias1', 'test_index2']).sort()).toEqual([
'test_index1',
'test_index2',
]);
expect(expandAliases('alias2')).toEqual('test_index2');
});
});
describe('Templates', function () {
test('legacy templates, index templates, component templates', function () {
legacyTemplate.loadTemplates({
test_index1: { order: 0 },
test_index2: { order: 0 },
test_index3: { order: 0 },
});
indexTemplate.loadTemplates({
index_templates: [
{ name: 'test_index1' },
{ name: 'test_index2' },
{ name: 'test_index3' },
],
});
componentTemplate.loadTemplates({
component_templates: [
{ name: 'test_index1' },
{ name: 'test_index2' },
{ name: 'test_index3' },
],
});
const expectedResult = ['test_index1', 'test_index2', 'test_index3'];
expect(legacyTemplate.getTemplates()).toEqual(expectedResult);
expect(indexTemplate.getTemplates()).toEqual(expectedResult);
expect(componentTemplate.getTemplates()).toEqual(expectedResult);
});
});
describe('Data streams', function () {
test('data streams', function () {
dataStream.loadDataStreams({
data_streams: [{ name: 'test_index1' }, { name: 'test_index2' }, { name: 'test_index3' }],
});
const expectedResult = ['test_index1', 'test_index2', 'test_index3'];
expect(dataStream.getDataStreams()).toEqual(expectedResult);
});
});
});

View file

@ -0,0 +1,21 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export abstract class BaseTemplate<T> {
protected templates: string[] = [];
public abstract loadTemplates(templates: T): void;
public getTemplates = (): string[] => {
return [...this.templates];
};
public clearTemplates = () => {
this.templates = [];
};
}

View file

@ -0,0 +1,16 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { ClusterGetComponentTemplateResponse } from '@elastic/elasticsearch/lib/api/types';
import { BaseTemplate } from './base_template';
export class ComponentTemplate extends BaseTemplate<ClusterGetComponentTemplateResponse> {
loadTemplates = (templates: ClusterGetComponentTemplateResponse) => {
this.templates = (templates.component_templates ?? []).map(({ name }) => name).sort();
};
}

View file

@ -0,0 +1,25 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/types';
export class DataStream {
private dataStreams: string[] = [];
getDataStreams = (): string[] => {
return [...this.dataStreams];
};
loadDataStreams = (dataStreams: IndicesGetDataStreamResponse) => {
this.dataStreams = (dataStreams.data_streams ?? []).map(({ name }) => name).sort();
};
clearDataStreams = () => {
this.dataStreams = [];
};
}

View file

@ -0,0 +1,41 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { getAutocompleteInfo } from '../../services';
export function expandAliases(indicesOrAliases: string | string[]) {
// takes a list of indices or aliases or a string which may be either and returns a list of indices
// returns a list for multiple values or a string for a single.
const perAliasIndexes = getAutocompleteInfo().alias.perAliasIndexes;
if (!indicesOrAliases) {
return indicesOrAliases;
}
if (typeof indicesOrAliases === 'string') {
indicesOrAliases = [indicesOrAliases];
}
indicesOrAliases = indicesOrAliases.flatMap((iOrA) => {
if (perAliasIndexes[iOrA]) {
return perAliasIndexes[iOrA];
}
return [iOrA];
});
let ret = ([] as string[]).concat.apply([], indicesOrAliases);
ret.sort();
ret = ret.reduce((result, value, index, array) => {
const last = array[index - 1];
if (last !== value) {
result.push(value);
}
return result;
}, [] as string[]);
return ret.length > 1 ? ret : ret[0];
}

View file

@ -0,0 +1,15 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { Alias } from './alias';
export { Mapping } from './mapping';
export { DataStream } from './data_stream';
export { LegacyTemplate } from './legacy';
export { IndexTemplate } from './index_template';
export { ComponentTemplate } from './component_template';
export { getTypes } from './type';

View file

@ -0,0 +1,16 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { IndicesGetIndexTemplateResponse } from '@elastic/elasticsearch/lib/api/types';
import { BaseTemplate } from './base_template';
export class IndexTemplate extends BaseTemplate<IndicesGetIndexTemplateResponse> {
loadTemplates = (templates: IndicesGetIndexTemplateResponse) => {
this.templates = (templates.index_templates ?? []).map(({ name }) => name).sort();
};
}

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { LegacyTemplate } from './legacy_template';

View file

@ -0,0 +1,16 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { IndicesGetTemplateResponse } from '@elastic/elasticsearch/lib/api/types';
import { BaseTemplate } from '../base_template';
export class LegacyTemplate extends BaseTemplate<IndicesGetTemplateResponse> {
loadTemplates = (templates: IndicesGetTemplateResponse) => {
this.templates = Object.keys(templates).sort();
};
}

View file

@ -0,0 +1,164 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import type { IndicesGetMappingResponse } from '@elastic/elasticsearch/lib/api/types';
import { expandAliases } from './expand_aliases';
import type { Field, FieldMapping } from './types';
function getFieldNamesFromProperties(properties: Record<string, FieldMapping> = {}) {
const fieldList = Object.entries(properties).flatMap(([fieldName, fieldMapping]) => {
return getFieldNamesFromFieldMapping(fieldName, fieldMapping);
});
// deduping
return _.uniqBy(fieldList, function (f) {
return f.name + ':' + f.type;
});
}
function getFieldNamesFromFieldMapping(
fieldName: string,
fieldMapping: FieldMapping
): Array<{ name: string; type: string | undefined }> {
if (fieldMapping.enabled === false) {
return [];
}
let nestedFields;
function applyPathSettings(nestedFieldNames: Array<{ name: string; type: string | undefined }>) {
const pathType = fieldMapping.path || 'full';
if (pathType === 'full') {
return nestedFieldNames.map((f) => {
f.name = fieldName + '.' + f.name;
return f;
});
}
return nestedFieldNames;
}
if (fieldMapping.properties) {
// derived object type
nestedFields = getFieldNamesFromProperties(fieldMapping.properties);
return applyPathSettings(nestedFields);
}
const fieldType = fieldMapping.type;
const ret = { name: fieldName, type: fieldType };
if (fieldMapping.index_name) {
ret.name = fieldMapping.index_name;
}
if (fieldMapping.fields) {
nestedFields = Object.entries(fieldMapping.fields).flatMap(([name, mapping]) => {
return getFieldNamesFromFieldMapping(name, mapping);
});
nestedFields = applyPathSettings(nestedFields);
nestedFields.unshift(ret);
return nestedFields;
}
return [ret];
}
export interface BaseMapping {
perIndexTypes: Record<string, object>;
getMappings(indices: string | string[], types?: string | string[]): Field[];
loadMappings(mappings: IndicesGetMappingResponse): void;
clearMappings(): void;
}
export class Mapping implements BaseMapping {
public perIndexTypes: Record<string, object> = {};
getMappings = (indices: string | string[], types?: string | string[]) => {
// get fields for indices and types. Both can be a list, a string or null (meaning all).
let ret: Field[] = [];
indices = expandAliases(indices);
if (typeof indices === 'string') {
const typeDict = this.perIndexTypes[indices] as Record<string, unknown>;
if (!typeDict) {
return [];
}
if (typeof types === 'string') {
const f = typeDict[types];
if (Array.isArray(f)) {
ret = f;
}
} else {
// filter what we need
Object.entries(typeDict).forEach(([type, fields]) => {
if (!types || types.length === 0 || types.includes(type)) {
ret.push(fields as Field);
}
});
ret = ([] as Field[]).concat.apply([], ret);
}
} else {
// multi index mode.
Object.keys(this.perIndexTypes).forEach((index) => {
if (!indices || indices.length === 0 || indices.includes(index)) {
ret.push(this.getMappings(index, types) as unknown as Field);
}
});
ret = ([] as Field[]).concat.apply([], ret);
}
return _.uniqBy(ret, function (f) {
return f.name + ':' + f.type;
});
};
loadMappings = (mappings: IndicesGetMappingResponse) => {
const maxMappingSize = Object.keys(mappings).length > 10 * 1024 * 1024;
let mappingsResponse;
if (maxMappingSize) {
// eslint-disable-next-line no-console
console.warn(
`Mapping size is larger than 10MB (${
Object.keys(mappings).length / 1024 / 1024
} MB). Ignoring...`
);
mappingsResponse = {};
} else {
mappingsResponse = mappings;
}
this.perIndexTypes = {};
Object.entries(mappingsResponse).forEach(([index, indexMapping]) => {
const normalizedIndexMappings: Record<string, object[]> = {};
let transformedMapping: Record<string, any> = indexMapping;
// Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping.
if (indexMapping.mappings && Object.keys(indexMapping).length === 1) {
transformedMapping = indexMapping.mappings;
}
Object.entries(transformedMapping).forEach(([typeName, typeMapping]) => {
if (typeName === 'properties') {
const fieldList = getFieldNamesFromProperties(typeMapping);
normalizedIndexMappings[typeName] = fieldList;
} else {
normalizedIndexMappings[typeName] = [];
}
});
this.perIndexTypes[index] = normalizedIndexMappings;
});
};
clearMappings = () => {
this.perIndexTypes = {};
};
}

View file

@ -0,0 +1,44 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import { getAutocompleteInfo } from '../../services';
import { expandAliases } from './expand_aliases';
export function getTypes(indices: string | string[]) {
let ret: string[] = [];
const perIndexTypes = getAutocompleteInfo().mapping.perIndexTypes;
indices = expandAliases(indices);
if (typeof indices === 'string') {
const typeDict = perIndexTypes[indices];
if (!typeDict) {
return [];
}
// filter what we need
if (Array.isArray(typeDict)) {
typeDict.forEach((type) => {
ret.push(type);
});
} else if (typeof typeDict === 'object') {
Object.keys(typeDict).forEach((type) => {
ret.push(type);
});
}
} else {
// multi index mode.
Object.keys(perIndexTypes).forEach((index) => {
if (!indices || indices.includes(index)) {
ret.push(getTypes(index) as unknown as string);
}
});
ret = ([] as string[]).concat.apply([], ret);
}
return _.uniq(ret);
}

View file

@ -0,0 +1,39 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type {
ClusterGetComponentTemplateResponse,
IndicesGetAliasResponse,
IndicesGetDataStreamResponse,
IndicesGetIndexTemplateResponse,
IndicesGetMappingResponse,
IndicesGetTemplateResponse,
} from '@elastic/elasticsearch/lib/api/types';
export interface Field {
name: string;
type: string;
}
export interface FieldMapping {
enabled?: boolean;
path?: string;
properties?: Record<string, FieldMapping>;
type?: string;
index_name?: string;
fields?: FieldMapping[];
}
export interface MappingsApiResponse {
mappings: IndicesGetMappingResponse;
aliases: IndicesGetAliasResponse;
dataStreams: IndicesGetDataStreamResponse;
legacyTemplates: IndicesGetTemplateResponse;
indexTemplates: IndicesGetIndexTemplateResponse;
componentTemplates: ClusterGetComponentTemplateResponse;
}

View file

@ -11,16 +11,20 @@ import { populateContext } from '../autocomplete/engine';
import '../../application/models/sense_editor/sense_editor.test.mocks';
import * as kb from '.';
import * as mappings from '../mappings/mappings';
import { AutocompleteInfo, setAutocompleteInfo } from '../../services';
describe('Knowledge base', () => {
let autocompleteInfo;
beforeEach(() => {
mappings.clear();
kb.setActiveApi(kb._test.loadApisFromJson({}));
autocompleteInfo = new AutocompleteInfo();
setAutocompleteInfo(autocompleteInfo);
autocompleteInfo.mapping.clearMappings();
});
afterEach(() => {
mappings.clear();
kb.setActiveApi(kb._test.loadApisFromJson({}));
autocompleteInfo = null;
setAutocompleteInfo(null);
});
const MAPPING = {
@ -122,7 +126,7 @@ describe('Knowledge base', () => {
kb.setActiveApi(testApi);
mappings.loadMappings(MAPPING);
autocompleteInfo.mapping.loadMappings(MAPPING);
testUrlContext(tokenPath, otherTokenValues, expectedContext);
});
}
@ -165,7 +169,7 @@ describe('Knowledge base', () => {
);
kb.setActiveApi(testApi);
mappings.loadMappings(MAPPING);
autocompleteInfo.mapping.loadMappings(MAPPING);
testUrlContext(tokenPath, otherTokenValues, expectedContext);
});

View file

@ -1,278 +0,0 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import '../../application/models/sense_editor/sense_editor.test.mocks';
import * as mappings from './mappings';
describe('Mappings', () => {
beforeEach(() => {
mappings.clear();
});
afterEach(() => {
mappings.clear();
});
function fc(f1, f2) {
if (f1.name < f2.name) {
return -1;
}
if (f1.name > f2.name) {
return 1;
}
return 0;
}
function f(name, type) {
return { name: name, type: type || 'string' };
}
test('Multi fields 1.0 style', function () {
mappings.loadMappings({
index: {
properties: {
first_name: {
type: 'string',
index: 'analyzed',
path: 'just_name',
fields: {
any_name: { type: 'string', index: 'analyzed' },
},
},
last_name: {
type: 'string',
index: 'no',
fields: {
raw: { type: 'string', index: 'analyzed' },
},
},
},
},
});
expect(mappings.getFields('index').sort(fc)).toEqual([
f('any_name', 'string'),
f('first_name', 'string'),
f('last_name', 'string'),
f('last_name.raw', 'string'),
]);
});
test('Simple fields', function () {
mappings.loadMappings({
index: {
properties: {
str: {
type: 'string',
},
number: {
type: 'int',
},
},
},
});
expect(mappings.getFields('index').sort(fc)).toEqual([f('number', 'int'), f('str', 'string')]);
});
test('Simple fields - 1.0 style', function () {
mappings.loadMappings({
index: {
mappings: {
properties: {
str: {
type: 'string',
},
number: {
type: 'int',
},
},
},
},
});
expect(mappings.getFields('index').sort(fc)).toEqual([f('number', 'int'), f('str', 'string')]);
});
test('Nested fields', function () {
mappings.loadMappings({
index: {
properties: {
person: {
type: 'object',
properties: {
name: {
properties: {
first_name: { type: 'string' },
last_name: { type: 'string' },
},
},
sid: { type: 'string', index: 'not_analyzed' },
},
},
message: { type: 'string' },
},
},
});
expect(mappings.getFields('index', []).sort(fc)).toEqual([
f('message'),
f('person.name.first_name'),
f('person.name.last_name'),
f('person.sid'),
]);
});
test('Enabled fields', function () {
mappings.loadMappings({
index: {
properties: {
person: {
type: 'object',
properties: {
name: {
type: 'object',
enabled: false,
},
sid: { type: 'string', index: 'not_analyzed' },
},
},
message: { type: 'string' },
},
},
});
expect(mappings.getFields('index', []).sort(fc)).toEqual([f('message'), f('person.sid')]);
});
test('Path tests', function () {
mappings.loadMappings({
index: {
properties: {
name1: {
type: 'object',
path: 'just_name',
properties: {
first1: { type: 'string' },
last1: { type: 'string', index_name: 'i_last_1' },
},
},
name2: {
type: 'object',
path: 'full',
properties: {
first2: { type: 'string' },
last2: { type: 'string', index_name: 'i_last_2' },
},
},
},
},
});
expect(mappings.getFields().sort(fc)).toEqual([
f('first1'),
f('i_last_1'),
f('name2.first2'),
f('name2.i_last_2'),
]);
});
test('Use index_name tests', function () {
mappings.loadMappings({
index: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
});
expect(mappings.getFields().sort(fc)).toEqual([f('i_last_1')]);
});
test('Aliases', function () {
mappings.loadAliases({
test_index1: {
aliases: {
alias1: {},
},
},
test_index2: {
aliases: {
alias2: {
filter: {
term: {
FIELD: 'VALUE',
},
},
},
alias1: {},
},
},
});
mappings.loadMappings({
test_index1: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
test_index2: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
});
expect(mappings.getIndices().sort()).toEqual([
'_all',
'alias1',
'alias2',
'test_index1',
'test_index2',
]);
expect(mappings.getIndices(false).sort()).toEqual(['test_index1', 'test_index2']);
expect(mappings.expandAliases(['alias1', 'test_index2']).sort()).toEqual([
'test_index1',
'test_index2',
]);
expect(mappings.expandAliases('alias2')).toEqual('test_index2');
});
test('Templates', function () {
mappings.loadLegacyTemplates({
test_index1: { order: 0 },
test_index2: { order: 0 },
test_index3: { order: 0 },
});
mappings.loadIndexTemplates({
index_templates: [{ name: 'test_index1' }, { name: 'test_index2' }, { name: 'test_index3' }],
});
mappings.loadComponentTemplates({
component_templates: [
{ name: 'test_index1' },
{ name: 'test_index2' },
{ name: 'test_index3' },
],
});
const expectedResult = ['test_index1', 'test_index2', 'test_index3'];
expect(mappings.getLegacyTemplates()).toEqual(expectedResult);
expect(mappings.getIndexTemplates()).toEqual(expectedResult);
expect(mappings.getComponentTemplates()).toEqual(expectedResult);
});
test('Data streams', function () {
mappings.loadDataStreams({
data_streams: [{ name: 'test_index1' }, { name: 'test_index2' }, { name: 'test_index3' }],
});
const expectedResult = ['test_index1', 'test_index2', 'test_index3'];
expect(mappings.getDataStreams()).toEqual(expectedResult);
});
});

View file

@ -1,410 +0,0 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import * as es from '../es/es';
let pollTimeoutId;
let perIndexTypes = {};
let perAliasIndexes = {};
let legacyTemplates = [];
let indexTemplates = [];
let componentTemplates = [];
let dataStreams = [];
export function expandAliases(indicesOrAliases) {
// takes a list of indices or aliases or a string which may be either and returns a list of indices
// returns a list for multiple values or a string for a single.
if (!indicesOrAliases) {
return indicesOrAliases;
}
if (typeof indicesOrAliases === 'string') {
indicesOrAliases = [indicesOrAliases];
}
indicesOrAliases = indicesOrAliases.map((iOrA) => {
if (perAliasIndexes[iOrA]) {
return perAliasIndexes[iOrA];
}
return [iOrA];
});
let ret = [].concat.apply([], indicesOrAliases);
ret.sort();
ret = ret.reduce((result, value, index, array) => {
const last = array[index - 1];
if (last !== value) {
result.push(value);
}
return result;
}, []);
return ret.length > 1 ? ret : ret[0];
}
export function getLegacyTemplates() {
return [...legacyTemplates];
}
export function getIndexTemplates() {
return [...indexTemplates];
}
export function getComponentTemplates() {
return [...componentTemplates];
}
export function getDataStreams() {
return [...dataStreams];
}
export function getFields(indices, types) {
// get fields for indices and types. Both can be a list, a string or null (meaning all).
let ret = [];
indices = expandAliases(indices);
if (typeof indices === 'string') {
const typeDict = perIndexTypes[indices];
if (!typeDict) {
return [];
}
if (typeof types === 'string') {
const f = typeDict[types];
ret = f ? f : [];
} else {
// filter what we need
Object.entries(typeDict).forEach(([type, fields]) => {
if (!types || types.length === 0 || types.includes(type)) {
ret.push(fields);
}
});
ret = [].concat.apply([], ret);
}
} else {
// multi index mode.
Object.keys(perIndexTypes).forEach((index) => {
if (!indices || indices.length === 0 || indices.includes(index)) {
ret.push(getFields(index, types));
}
});
ret = [].concat.apply([], ret);
}
return _.uniqBy(ret, function (f) {
return f.name + ':' + f.type;
});
}
export function getTypes(indices) {
let ret = [];
indices = expandAliases(indices);
if (typeof indices === 'string') {
const typeDict = perIndexTypes[indices];
if (!typeDict) {
return [];
}
// filter what we need
if (Array.isArray(typeDict)) {
typeDict.forEach((type) => {
ret.push(type);
});
} else if (typeof typeDict === 'object') {
Object.keys(typeDict).forEach((type) => {
ret.push(type);
});
}
} else {
// multi index mode.
Object.keys(perIndexTypes).forEach((index) => {
if (!indices || indices.includes(index)) {
ret.push(getTypes(index));
}
});
ret = [].concat.apply([], ret);
}
return _.uniq(ret);
}
export function getIndices(includeAliases) {
const ret = [];
Object.keys(perIndexTypes).forEach((index) => {
// ignore .ds* indices in the suggested indices list.
if (!index.startsWith('.ds')) {
ret.push(index);
}
});
if (typeof includeAliases === 'undefined' ? true : includeAliases) {
Object.keys(perAliasIndexes).forEach((alias) => {
ret.push(alias);
});
}
return ret;
}
function getFieldNamesFromFieldMapping(fieldName, fieldMapping) {
if (fieldMapping.enabled === false) {
return [];
}
let nestedFields;
function applyPathSettings(nestedFieldNames) {
const pathType = fieldMapping.path || 'full';
if (pathType === 'full') {
return nestedFieldNames.map((f) => {
f.name = fieldName + '.' + f.name;
return f;
});
}
return nestedFieldNames;
}
if (fieldMapping.properties) {
// derived object type
nestedFields = getFieldNamesFromProperties(fieldMapping.properties);
return applyPathSettings(nestedFields);
}
const fieldType = fieldMapping.type;
const ret = { name: fieldName, type: fieldType };
if (fieldMapping.index_name) {
ret.name = fieldMapping.index_name;
}
if (fieldMapping.fields) {
nestedFields = Object.entries(fieldMapping.fields).flatMap(([fieldName, fieldMapping]) => {
return getFieldNamesFromFieldMapping(fieldName, fieldMapping);
});
nestedFields = applyPathSettings(nestedFields);
nestedFields.unshift(ret);
return nestedFields;
}
return [ret];
}
function getFieldNamesFromProperties(properties = {}) {
const fieldList = Object.entries(properties).flatMap(([fieldName, fieldMapping]) => {
return getFieldNamesFromFieldMapping(fieldName, fieldMapping);
});
// deduping
return _.uniqBy(fieldList, function (f) {
return f.name + ':' + f.type;
});
}
export function loadLegacyTemplates(templatesObject = {}) {
legacyTemplates = Object.keys(templatesObject);
}
export function loadIndexTemplates(data) {
indexTemplates = (data.index_templates ?? []).map(({ name }) => name);
}
export function loadComponentTemplates(data) {
componentTemplates = (data.component_templates ?? []).map(({ name }) => name);
}
export function loadDataStreams(data) {
dataStreams = (data.data_streams ?? []).map(({ name }) => name);
}
export function loadMappings(mappings) {
perIndexTypes = {};
Object.entries(mappings).forEach(([index, indexMapping]) => {
const normalizedIndexMappings = {};
// Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping.
if (indexMapping.mappings && Object.keys(indexMapping).length === 1) {
indexMapping = indexMapping.mappings;
}
Object.entries(indexMapping).forEach(([typeName, typeMapping]) => {
if (typeName === 'properties') {
const fieldList = getFieldNamesFromProperties(typeMapping);
normalizedIndexMappings[typeName] = fieldList;
} else {
normalizedIndexMappings[typeName] = [];
}
});
perIndexTypes[index] = normalizedIndexMappings;
});
}
export function loadAliases(aliases) {
perAliasIndexes = {};
Object.entries(aliases).forEach(([index, omdexAliases]) => {
// verify we have an index defined. useful when mapping loading is disabled
perIndexTypes[index] = perIndexTypes[index] || {};
Object.keys(omdexAliases.aliases || {}).forEach((alias) => {
if (alias === index) {
return;
} // alias which is identical to index means no index.
let curAliases = perAliasIndexes[alias];
if (!curAliases) {
curAliases = [];
perAliasIndexes[alias] = curAliases;
}
curAliases.push(index);
});
});
perAliasIndexes._all = getIndices(false);
}
export function clear() {
perIndexTypes = {};
perAliasIndexes = {};
legacyTemplates = [];
indexTemplates = [];
componentTemplates = [];
}
function retrieveSettings(http, settingsKey, settingsToRetrieve) {
const settingKeyToPathMap = {
fields: '_mapping',
indices: '_aliases',
legacyTemplates: '_template',
indexTemplates: '_index_template',
componentTemplates: '_component_template',
dataStreams: '_data_stream',
};
// Fetch autocomplete info if setting is set to true, and if user has made changes.
if (settingsToRetrieve[settingsKey] === true) {
// Use pretty=false in these request in order to compress the response by removing whitespace
const path = `${settingKeyToPathMap[settingsKey]}?pretty=false`;
const method = 'GET';
const asSystemRequest = true;
const withProductOrigin = true;
return es.send({ http, method, path, asSystemRequest, withProductOrigin });
} else {
if (settingsToRetrieve[settingsKey] === false) {
// If the user doesn't want autocomplete suggestions, then clear any that exist
return Promise.resolve({});
// return settingsPromise.resolveWith(this, [{}]);
} else {
// If the user doesn't want autocomplete suggestions, then clear any that exist
return Promise.resolve();
}
}
}
// Retrieve all selected settings by default.
// TODO: We should refactor this to be easier to consume. Ideally this function should retrieve
// whatever settings are specified, otherwise just use the saved settings. This requires changing
// the behavior to not *clear* whatever settings have been unselected, but it's hard to tell if
// this is possible without altering the autocomplete behavior. These are the scenarios we need to
// support:
// 1. Manual refresh. Specify what we want. Fetch specified, leave unspecified alone.
// 2. Changed selection and saved: Specify what we want. Fetch changed and selected, leave
// unchanged alone (both selected and unselected).
// 3. Poll: Use saved. Fetch selected. Ignore unselected.
export function clearSubscriptions() {
if (pollTimeoutId) {
clearTimeout(pollTimeoutId);
}
}
const retrieveMappings = async (http, settingsToRetrieve) => {
const mappings = await retrieveSettings(http, 'fields', settingsToRetrieve);
if (mappings) {
const maxMappingSize = Object.keys(mappings).length > 10 * 1024 * 1024;
let mappingsResponse;
if (maxMappingSize) {
console.warn(
`Mapping size is larger than 10MB (${
Object.keys(mappings).length / 1024 / 1024
} MB). Ignoring...`
);
mappingsResponse = '{}';
} else {
mappingsResponse = mappings;
}
loadMappings(mappingsResponse);
}
};
const retrieveAliases = async (http, settingsToRetrieve) => {
const aliases = await retrieveSettings(http, 'indices', settingsToRetrieve);
if (aliases) {
loadAliases(aliases);
}
};
const retrieveTemplates = async (http, settingsToRetrieve) => {
const legacyTemplates = await retrieveSettings(http, 'legacyTemplates', settingsToRetrieve);
const indexTemplates = await retrieveSettings(http, 'indexTemplates', settingsToRetrieve);
const componentTemplates = await retrieveSettings(http, 'componentTemplates', settingsToRetrieve);
if (legacyTemplates) {
loadLegacyTemplates(legacyTemplates);
}
if (indexTemplates) {
loadIndexTemplates(indexTemplates);
}
if (componentTemplates) {
loadComponentTemplates(componentTemplates);
}
};
const retrieveDataStreams = async (http, settingsToRetrieve) => {
const dataStreams = await retrieveSettings(http, 'dataStreams', settingsToRetrieve);
if (dataStreams) {
loadDataStreams(dataStreams);
}
};
/**
*
* @param settings Settings A way to retrieve the current settings
* @param settingsToRetrieve any
*/
export function retrieveAutoCompleteInfo(http, settings, settingsToRetrieve) {
clearSubscriptions();
const templatesSettingToRetrieve = {
...settingsToRetrieve,
legacyTemplates: settingsToRetrieve.templates,
indexTemplates: settingsToRetrieve.templates,
componentTemplates: settingsToRetrieve.templates,
};
Promise.allSettled([
retrieveMappings(http, settingsToRetrieve),
retrieveAliases(http, settingsToRetrieve),
retrieveTemplates(http, templatesSettingToRetrieve),
retrieveDataStreams(http, settingsToRetrieve),
]).then(() => {
// Schedule next request.
pollTimeoutId = setTimeout(() => {
// This looks strange/inefficient, but it ensures correct behavior because we don't want to send
// a scheduled request if the user turns off polling.
if (settings.getPolling()) {
retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete());
}
}, settings.getPollInterval());
});
}

View file

@ -15,8 +15,10 @@ import {
ConsolePluginSetup,
ConsoleUILocatorParams,
} from './types';
import { AutocompleteInfo, setAutocompleteInfo } from './services';
export class ConsoleUIPlugin implements Plugin<void, void, AppSetupUIPluginDependencies> {
private readonly autocompleteInfo = new AutocompleteInfo();
constructor(private ctx: PluginInitializerContext) {}
public setup(
@ -27,6 +29,9 @@ export class ConsoleUIPlugin implements Plugin<void, void, AppSetupUIPluginDepen
ui: { enabled: isConsoleUiEnabled },
} = this.ctx.config.get<ClientConfigType>();
this.autocompleteInfo.setup(http);
setAutocompleteInfo(this.autocompleteInfo);
if (isConsoleUiEnabled) {
if (home) {
home.featureCatalogue.register({
@ -70,6 +75,7 @@ export class ConsoleUIPlugin implements Plugin<void, void, AppSetupUIPluginDepen
usageCollection,
element,
theme$,
autocompleteInfo: this.autocompleteInfo,
});
},
});

View file

@ -0,0 +1,17 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { AutocompleteInfo } from './autocomplete';
export class AutocompleteInfoMock extends AutocompleteInfo {
setup = jest.fn();
getEntityProvider = jest.fn();
retrieve = jest.fn();
clearSubscriptions = jest.fn();
clear = jest.fn();
}

View file

@ -0,0 +1,107 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
import type { HttpSetup } from '@kbn/core/public';
import type { MappingsApiResponse } from '../lib/autocomplete_entities/types';
import { API_BASE_PATH } from '../../common/constants';
import {
Alias,
DataStream,
Mapping,
LegacyTemplate,
IndexTemplate,
ComponentTemplate,
} from '../lib/autocomplete_entities';
import { DevToolsSettings, Settings } from './settings';
export class AutocompleteInfo {
public readonly alias = new Alias();
public readonly mapping = new Mapping();
public readonly dataStream = new DataStream();
public readonly legacyTemplate = new LegacyTemplate();
public readonly indexTemplate = new IndexTemplate();
public readonly componentTemplate = new ComponentTemplate();
private http!: HttpSetup;
private pollTimeoutId: ReturnType<typeof setTimeout> | undefined;
public setup(http: HttpSetup) {
this.http = http;
}
public getEntityProvider(
type: string,
context: { indices: string[]; types: string[] } = { indices: [], types: [] }
) {
switch (type) {
case 'indices':
const includeAliases = true;
const collaborator = this.mapping;
return () => this.alias.getIndices(includeAliases, collaborator);
case 'fields':
return this.mapping.getMappings(context.indices, context.types);
case 'indexTemplates':
return () => this.indexTemplate.getTemplates();
case 'componentTemplates':
return () => this.componentTemplate.getTemplates();
case 'legacyTemplates':
return () => this.legacyTemplate.getTemplates();
case 'dataStreams':
return () => this.dataStream.getDataStreams();
default:
throw new Error(`Unsupported type: ${type}`);
}
}
public retrieve(settings: Settings, settingsToRetrieve: DevToolsSettings['autocomplete']) {
this.clearSubscriptions();
this.http
.get<MappingsApiResponse>(`${API_BASE_PATH}/autocomplete_entities`, {
query: { ...settingsToRetrieve },
})
.then((data) => {
this.load(data);
// Schedule next request.
this.pollTimeoutId = setTimeout(() => {
// This looks strange/inefficient, but it ensures correct behavior because we don't want to send
// a scheduled request if the user turns off polling.
if (settings.getPolling()) {
this.retrieve(settings, settings.getAutocomplete());
}
}, settings.getPollInterval());
});
}
public clearSubscriptions() {
if (this.pollTimeoutId) {
clearTimeout(this.pollTimeoutId);
}
}
private load(data: MappingsApiResponse) {
this.mapping.loadMappings(data.mappings);
const collaborator = this.mapping;
this.alias.loadAliases(data.aliases, collaborator);
this.indexTemplate.loadTemplates(data.indexTemplates);
this.componentTemplate.loadTemplates(data.componentTemplates);
this.legacyTemplate.loadTemplates(data.legacyTemplates);
this.dataStream.loadDataStreams(data.dataStreams);
}
public clear() {
this.alias.clearAliases();
this.mapping.clearMappings();
this.dataStream.clearDataStreams();
this.legacyTemplate.clearTemplates();
this.indexTemplate.clearTemplates();
this.componentTemplate.clearTemplates();
}
}
export const [getAutocompleteInfo, setAutocompleteInfo] =
createGetterSetter<AutocompleteInfo>('AutocompleteInfo');

View file

@ -10,3 +10,4 @@ export { createHistory, History } from './history';
export { createStorage, Storage, StorageKeys } from './storage';
export type { DevToolsSettings } from './settings';
export { createSettings, Settings, DEFAULT_SETTINGS } from './settings';
export { AutocompleteInfo, getAutocompleteInfo, setAutocompleteInfo } from './autocomplete';

View file

@ -16,6 +16,7 @@ import { ConsoleConfig, ConsoleConfig7x } from './config';
import { registerRoutes } from './routes';
import { ESConfigForProxy, ConsoleSetup, ConsoleStart } from './types';
import { handleEsError } from './shared_imports';
export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
log: Logger;
@ -58,6 +59,9 @@ export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
esLegacyConfigService: this.esLegacyConfigService,
specDefinitionService: this.specDefinitionsService,
},
lib: {
handleEsError,
},
proxy: {
readLegacyESConfig: async (): Promise<ESConfigForProxy> => {
const legacyConfig = await this.esLegacyConfigService.readConfig();

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { registerMappingsRoute } from './register_mappings_route';

View file

@ -0,0 +1,95 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { IScopedClusterClient } from '@kbn/core/server';
import { parse } from 'query-string';
import type { RouteDependencies } from '../../..';
import { API_BASE_PATH } from '../../../../../common/constants';
interface Settings {
indices: boolean;
fields: boolean;
templates: boolean;
dataStreams: boolean;
}
async function getMappings(esClient: IScopedClusterClient, settings: Settings) {
if (settings.fields) {
return esClient.asInternalUser.indices.getMapping();
}
// If the user doesn't want autocomplete suggestions, then clear any that exist.
return Promise.resolve({});
}
async function getAliases(esClient: IScopedClusterClient, settings: Settings) {
if (settings.indices) {
return esClient.asInternalUser.indices.getAlias();
}
// If the user doesn't want autocomplete suggestions, then clear any that exist.
return Promise.resolve({});
}
async function getDataStreams(esClient: IScopedClusterClient, settings: Settings) {
if (settings.dataStreams) {
return esClient.asInternalUser.indices.getDataStream();
}
// If the user doesn't want autocomplete suggestions, then clear any that exist.
return Promise.resolve({});
}
async function getTemplates(esClient: IScopedClusterClient, settings: Settings) {
if (settings.templates) {
return Promise.all([
esClient.asInternalUser.indices.getTemplate(),
esClient.asInternalUser.indices.getIndexTemplate(),
esClient.asInternalUser.cluster.getComponentTemplate(),
]);
}
// If the user doesn't want autocomplete suggestions, then clear any that exist.
return Promise.resolve([]);
}
export function registerGetRoute({ router, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: `${API_BASE_PATH}/autocomplete_entities`,
validate: false,
},
async (ctx, request, response) => {
try {
const settings = parse(request.url.search, { parseBooleans: true }) as unknown as Settings;
// If no settings are provided return 400
if (Object.keys(settings).length === 0) {
return response.badRequest({
body: 'Request must contain a query param of autocomplete settings',
});
}
const esClient = (await ctx.core).elasticsearch.client;
const mappings = await getMappings(esClient, settings);
const aliases = await getAliases(esClient, settings);
const dataStreams = await getDataStreams(esClient, settings);
const [legacyTemplates = {}, indexTemplates = {}, componentTemplates = {}] =
await getTemplates(esClient, settings);
return response.ok({
body: {
mappings,
aliases,
dataStreams,
legacyTemplates,
indexTemplates,
componentTemplates,
},
});
} catch (e) {
return handleEsError({ error: e, response });
}
}
);
}

View file

@ -0,0 +1,14 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { RouteDependencies } from '../../..';
import { registerGetRoute } from './register_get_route';
export function registerMappingsRoute(deps: RouteDependencies) {
registerGetRoute(deps);
}

View file

@ -17,6 +17,7 @@ import { MAJOR_VERSION } from '../../../../../common/constants';
import { ProxyConfigCollection } from '../../../../lib';
import { RouteDependencies, ProxyDependencies } from '../../..';
import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services';
import { handleEsError } from '../../../../shared_imports';
const kibanaVersion = new SemVer(MAJOR_VERSION);
@ -65,5 +66,6 @@ export const getProxyRouteHandlerDeps = ({
: defaultProxyValue,
log,
kibanaVersion,
lib: { handleEsError },
};
};

View file

@ -12,10 +12,12 @@ import { SemVer } from 'semver';
import { EsLegacyConfigService, SpecDefinitionsService } from '../services';
import { ESConfigForProxy } from '../types';
import { ProxyConfigCollection } from '../lib';
import { handleEsError } from '../shared_imports';
import { registerEsConfigRoute } from './api/console/es_config';
import { registerProxyRoute } from './api/console/proxy';
import { registerSpecDefinitionsRoute } from './api/console/spec_definitions';
import { registerMappingsRoute } from './api/console/autocomplete_entities';
export interface ProxyDependencies {
readLegacyESConfig: () => Promise<ESConfigForProxy>;
@ -31,6 +33,9 @@ export interface RouteDependencies {
esLegacyConfigService: EsLegacyConfigService;
specDefinitionService: SpecDefinitionsService;
};
lib: {
handleEsError: typeof handleEsError;
};
kibanaVersion: SemVer;
}
@ -38,4 +43,5 @@ export const registerRoutes = (dependencies: RouteDependencies) => {
registerEsConfigRoute(dependencies);
registerProxyRoute(dependencies);
registerSpecDefinitionsRoute(dependencies);
registerMappingsRoute(dependencies);
};

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { handleEsError } from '@kbn/es-ui-shared-plugin/server';

View file

@ -0,0 +1,133 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import type { Response } from 'superagent';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
function utilTest(name: string, query: object, test: (response: Response) => void) {
it(name, async () => {
const response = await supertest.get('/api/console/autocomplete_entities').query(query);
test(response);
});
}
describe('/api/console/autocomplete_entities', () => {
utilTest('should not succeed if no settings are provided in query params', {}, (response) => {
const { status } = response;
expect(status).to.be(400);
});
utilTest(
'should return an object with properties of "mappings", "aliases", "dataStreams", "legacyTemplates", "indexTemplates", "componentTemplates"',
{
indices: true,
fields: true,
templates: true,
dataStreams: true,
},
(response) => {
const { body, status } = response;
expect(status).to.be(200);
expect(Object.keys(body).sort()).to.eql([
'aliases',
'componentTemplates',
'dataStreams',
'indexTemplates',
'legacyTemplates',
'mappings',
]);
}
);
utilTest(
'should return empty payload with all settings are set to false',
{
indices: false,
fields: false,
templates: false,
dataStreams: false,
},
(response) => {
const { body, status } = response;
expect(status).to.be(200);
expect(body.legacyTemplates).to.eql({});
expect(body.indexTemplates).to.eql({});
expect(body.componentTemplates).to.eql({});
expect(body.aliases).to.eql({});
expect(body.mappings).to.eql({});
expect(body.dataStreams).to.eql({});
}
);
utilTest(
'should return empty templates with templates setting is set to false',
{
indices: true,
fields: true,
templates: false,
dataStreams: true,
},
(response) => {
const { body, status } = response;
expect(status).to.be(200);
expect(body.legacyTemplates).to.eql({});
expect(body.indexTemplates).to.eql({});
expect(body.componentTemplates).to.eql({});
}
);
utilTest(
'should return empty data streams with dataStreams setting is set to false',
{
indices: true,
fields: true,
templates: true,
dataStreams: false,
},
(response) => {
const { body, status } = response;
expect(status).to.be(200);
expect(body.dataStreams).to.eql({});
}
);
utilTest(
'should return empty aliases with indices setting is set to false',
{
indices: false,
fields: true,
templates: true,
dataStreams: true,
},
(response) => {
const { body, status } = response;
expect(status).to.be(200);
expect(body.aliases).to.eql({});
}
);
utilTest(
'should return empty mappings with fields setting is set to false',
{
indices: true,
fields: false,
templates: true,
dataStreams: true,
},
(response) => {
const { body, status } = response;
expect(status).to.be(200);
expect(body.mappings).to.eql({});
}
);
});
};

View file

@ -11,5 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('core', () => {
loadTestFile(require.resolve('./proxy_route'));
loadTestFile(require.resolve('./autocomplete_entities'));
});
}