Fix field autocomplete suggestions in Console (#38948) (#39097)

* Retrieve autocomplete fields when a request has completed.
 - Updating polling logic so that dispatching a request resets the poll timer.
This commit is contained in:
CJ Cenizal 2019-06-17 14:43:46 -07:00 committed by GitHub
parent 70b0d7f825
commit 6de6e36622
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 174 additions and 176 deletions

View file

@ -34,7 +34,7 @@ describe('app initialization', () => {
ajaxDoneStub = sinon.stub();
sandbox.stub($, 'ajax').returns({ done: ajaxDoneStub });
sandbox.stub(history, 'getSavedEditorState');
sandbox.stub(mappings, 'startRetrievingAutoCompleteInfo');
sandbox.stub(mappings, 'startPolling');
inputMock = {
update: sinon.stub(),

View file

@ -131,5 +131,5 @@ export default function init(input, output, sourceLocation = 'stored') {
};
setupAutosave();
loadSavedState();
mappings.startRetrievingAutoCompleteInfo();
mappings.startPolling();
}

View file

@ -20,6 +20,7 @@
require('brace');
require('brace/ext/searchbox');
import Autocomplete from './autocomplete';
import mappings from './mappings';
const SenseEditor = require('./sense_editor/editor');
const settings = require('./settings');
const utils = require('./utils');
@ -110,13 +111,9 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
if (reqId !== CURRENT_REQ_ID) {
return;
}
let xhr;
if (dataOrjqXHR.promise) {
xhr = dataOrjqXHR;
}
else {
xhr = jqXhrORerrorThrown;
}
const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown;
function modeForContentType(contentType) {
if (contentType.indexOf('text/plain') >= 0) {
return 'ace/mode/text';
@ -127,14 +124,20 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
return null;
}
if (typeof xhr.status === 'number' &&
// things like DELETE index where the index is not there are OK.
((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404)
) {
const isSuccess = typeof xhr.status === 'number' &&
// Things like DELETE index where the index is not there are OK.
((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404);
if (isSuccess) {
if (xhr.status !== 404) {
// If the user has submitted a request against ES, something in the fields, indices, aliases,
// or templates may have changed, so we'll need to update this data.
mappings.retrieveAutoCompleteInfo();
}
// we have someone on the other side. Add to history
history.addToHistory(esPath, esMethod, esData);
let value = xhr.responseText;
const mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || '');
@ -166,8 +169,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
isFirstRequest = false;
// single request terminate via sendNextRequest as well
sendNextRequest();
}
else {
} else {
let value;
let mode;
if (xhr.responseText) {

View file

@ -22,7 +22,10 @@ const _ = require('lodash');
const es = require('./es');
const settings = require('./settings');
// NOTE: If this value ever changes to be a few seconds or less, it might introduce flakiness
// due to timing issues in our app.js tests.
const POLL_INTERVAL = 60000;
let pollTimeoutId;
let perIndexTypes = {};
let perAliasIndexes = [];
@ -61,12 +64,13 @@ function expandAliases(indicesOrAliases) {
function getTemplates() {
return [ ...templates ];
}
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') {
if (typeof indices === 'string') {
const typeDict = perIndexTypes[indices];
if (!typeDict) {
return [];
@ -75,8 +79,7 @@ function getFields(indices, types) {
if (typeof types === 'string') {
const f = typeDict[types];
ret = f ? f : [];
}
else {
} else {
// filter what we need
$.each(typeDict, function (type, fields) {
if (!types || types.length === 0 || $.inArray(type, types) !== -1) {
@ -86,8 +89,7 @@ function getFields(indices, types) {
ret = [].concat.apply([], ret);
}
}
else {
} else {
// multi index mode.
$.each(perIndexTypes, function (index) {
if (!indices || indices.length === 0 || $.inArray(index, indices) !== -1) {
@ -128,10 +130,8 @@ function getTypes(indices) {
}
return _.uniq(ret);
}
function getIndices(includeAliases) {
const ret = [];
$.each(perIndexTypes, function (index) {
@ -164,7 +164,7 @@ function getFieldNamesFromFieldMapping(fieldName, fieldMapping) {
if (fieldMapping.properties) {
// derived object type
nestedFields = getFieldNamesFromTypeMapping(fieldMapping);
nestedFields = getFieldNamesFromProperties(fieldMapping.properties);
return applyPathSettings(nestedFields);
}
@ -196,9 +196,9 @@ function getFieldNamesFromFieldMapping(fieldName, fieldMapping) {
return [ret];
}
function getFieldNamesFromTypeMapping(typeMapping) {
function getFieldNamesFromProperties(properties = {}) {
const fieldList =
$.map(typeMapping.properties || {}, function (fieldMapping, fieldName) {
$.map(properties, function (fieldMapping, fieldName) {
return getFieldNamesFromFieldMapping(fieldName, fieldMapping);
});
@ -214,16 +214,24 @@ function loadTemplates(templatesObject = {}) {
function loadMappings(mappings) {
perIndexTypes = {};
$.each(mappings, function (index, indexMapping) {
const normalizedIndexMappings = {};
// 1.0.0 mapping format has changed, extract underlying mapping
// Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping.
if (indexMapping.mappings && _.keys(indexMapping).length === 1) {
indexMapping = indexMapping.mappings;
}
$.each(indexMapping, function (typeName, typeMapping) {
const fieldList = getFieldNamesFromTypeMapping(typeMapping);
normalizedIndexMappings[typeName] = fieldList;
if (typeName === 'properties') {
const fieldList = getFieldNamesFromProperties(typeMapping);
normalizedIndexMappings[typeName] = fieldList;
} else {
normalizedIndexMappings[typeName] = [];
}
});
perIndexTypes[index] = normalizedIndexMappings;
});
}
@ -256,20 +264,21 @@ function clear() {
templates = [];
}
function retrieveSettings(settingsKey, changedFields) {
const autocompleteSettings = settings.getAutocomplete();
function retrieveSettings(settingsKey, settingsToRetrieve) {
const currentSettings = settings.getAutocomplete();
const settingKeyToPathMap = {
fields: '_mapping',
indices: '_aliases',
templates: '_template',
};
// Fetch autocomplete info if setting is set to true, and if user has made changes
if (autocompleteSettings[settingsKey] && changedFields[settingsKey]) {
// Fetch autocomplete info if setting is set to true, and if user has made changes.
if (currentSettings[settingsKey] && settingsToRetrieve[settingsKey]) {
return es.send('GET', settingKeyToPathMap[settingsKey], null, null, true);
} else {
const settingsPromise = new $.Deferred();
// If a user has saved settings, but a field remains checked and unchanged, no need to make changes
if (autocompleteSettings[settingsKey]) {
if (currentSettings[settingsKey]) {
return settingsPromise.resolve();
}
// If the user doesn't want autocomplete suggestions, then clear any that exist
@ -277,10 +286,15 @@ function retrieveSettings(settingsKey, changedFields) {
}
}
function retrieveAutocompleteInfoFromServer(changedFields) {
const mappingPromise = retrieveSettings('fields', changedFields);
const aliasesPromise = retrieveSettings('indices', changedFields);
const templatesPromise = retrieveSettings('templates', changedFields);
// Retrieve all selected settings by default.
function retrieveAutoCompleteInfo(settingsToRetrieve = settings.getAutocomplete()) {
if (pollTimeoutId) {
clearTimeout(pollTimeoutId);
}
const mappingPromise = retrieveSettings('fields', settingsToRetrieve);
const aliasesPromise = retrieveSettings('indices', settingsToRetrieve);
const templatesPromise = retrieveSettings('templates', settingsToRetrieve);
$.when(mappingPromise, aliasesPromise, templatesPromise)
.done((mappings, aliases, templates) => {
@ -308,26 +322,28 @@ function retrieveAutocompleteInfoFromServer(changedFields) {
// Trigger an update event with the mappings, aliases
$(mappingObj).trigger('update', [mappingsResponse, aliases[0]]);
}
// Schedule next request.
pollTimeoutId = setTimeout(retrieveAutoCompleteInfo, POLL_INTERVAL);
});
}
function autocompleteRetriever() {
const changedFields = settings.getAutocomplete();
retrieveAutocompleteInfoFromServer(changedFields);
setTimeout(function () {
autocompleteRetriever();
}, 60000);
function startPolling() {
// Technically, we don't need this method and we could just expose retrieveAutoCompleteInfo.
// However, we'll want to allow the user to turn polling on and off eventually so we'll leave this
// here to support this eventual functionality.
retrieveAutoCompleteInfo();
}
export default _.assign(mappingObj, {
getFields: getFields,
getTemplates: getTemplates,
getIndices: getIndices,
getTypes: getTypes,
loadMappings: loadMappings,
loadAliases: loadAliases,
expandAliases: expandAliases,
clear: clear,
startRetrievingAutoCompleteInfo: autocompleteRetriever,
retrieveAutoCompleteInfo: retrieveAutocompleteInfoFromServer
getFields,
getTemplates,
getIndices,
getTypes,
loadMappings,
loadAliases,
expandAliases,
clear,
startPolling,
retrieveAutoCompleteInfo,
});

View file

@ -201,7 +201,7 @@ describe('Integration', () => {
endpoints: {
_search: {
methods: ['GET', 'POST'],
patterns: ['{indices}/{types}/_search', '{indices}/_search', '_search'],
patterns: ['{indices}/_search', '_search'],
data_autocomplete_rules: {
query: {
match_all: {},
@ -221,19 +221,15 @@ describe('Integration', () => {
const MAPPING = {
index1: {
'type1.1': {
properties: {
'field1.1.1': { type: 'string' },
'field1.1.2': { type: 'string' },
},
properties: {
'field1.1.1': { type: 'string' },
'field1.1.2': { type: 'string' },
},
},
index2: {
'type2.1': {
properties: {
'field2.1.1': { type: 'string' },
'field2.1.2': { type: 'string' },
},
properties: {
'field2.1.1': { type: 'string' },
'field2.1.2': { type: 'string' },
},
},
};
@ -710,6 +706,9 @@ describe('Integration', () => {
]
);
// NOTE: This test emits "error while getting completion terms Error: failed to resolve link
// [GLOBAL.broken]: Error: failed to resolve global components for ['broken']". but that's
// expected.
contextTests(
{
a: {
@ -980,6 +979,7 @@ describe('Integration', () => {
]
);
// NOTE: This test emits "Can't extract a valid url token path", but that's expected.
contextTests('POST _search\n', MAPPING, SEARCH_KB, null, [
{
name: 'initial doc start',
@ -1014,7 +1014,7 @@ describe('Integration', () => {
const CLUSTER_KB = {
endpoints: {
_search: {
patterns: ['_search', '{indices}/{types}/_search', '{indices}/_search'],
patterns: ['_search', '{indices}/_search'],
url_params: {
search_type: ['count', 'query_then_fetch'],
scroll: '10m',

View file

@ -47,23 +47,21 @@ describe('Mappings', () => {
test('Multi fields', function () {
mappings.loadMappings({
index: {
tweet: {
properties: {
first_name: {
type: 'multi_field',
path: 'just_name',
fields: {
first_name: { type: 'string', index: 'analyzed' },
any_name: { type: 'string', index: 'analyzed' },
},
properties: {
first_name: {
type: 'multi_field',
path: 'just_name',
fields: {
first_name: { type: 'string', index: 'analyzed' },
any_name: { type: 'string', index: 'analyzed' },
},
last_name: {
type: 'multi_field',
path: 'just_name',
fields: {
last_name: { type: 'string', index: 'analyzed' },
any_name: { type: 'string', index: 'analyzed' },
},
},
last_name: {
type: 'multi_field',
path: 'just_name',
fields: {
last_name: { type: 'string', index: 'analyzed' },
any_name: { type: 'string', index: 'analyzed' },
},
},
},
@ -80,22 +78,20 @@ describe('Mappings', () => {
test('Multi fields 1.0 style', function () {
mappings.loadMappings({
index: {
tweet: {
properties: {
first_name: {
type: 'string',
index: 'analyzed',
path: 'just_name',
fields: {
any_name: { type: 'string', index: 'analyzed' },
},
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' },
},
},
last_name: {
type: 'string',
index: 'no',
fields: {
raw: { type: 'string', index: 'analyzed' },
},
},
},
@ -113,7 +109,27 @@ describe('Mappings', () => {
test('Simple fields', function () {
mappings.loadMappings({
index: {
tweet: {
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',
@ -132,54 +148,28 @@ describe('Mappings', () => {
]);
});
test('Simple fields - 1.0 style', function () {
mappings.loadMappings({
index: {
mappings: {
tweet: {
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: {
tweet: {
properties: {
person: {
type: 'object',
properties: {
name: {
properties: {
first_name: { type: 'string' },
last_name: { type: 'string' },
},
properties: {
person: {
type: 'object',
properties: {
name: {
properties: {
first_name: { type: 'string' },
last_name: { type: 'string' },
},
sid: { type: 'string', index: 'not_analyzed' },
},
sid: { type: 'string', index: 'not_analyzed' },
},
message: { type: 'string' },
},
message: { type: 'string' },
},
},
});
expect(mappings.getFields('index', ['tweet']).sort(fc)).toEqual([
expect(mappings.getFields('index', []).sort(fc)).toEqual([
f('message'),
f('person.name.first_name'),
f('person.name.last_name'),
@ -190,25 +180,23 @@ describe('Mappings', () => {
test('Enabled fields', function () {
mappings.loadMappings({
index: {
tweet: {
properties: {
person: {
type: 'object',
properties: {
name: {
type: 'object',
enabled: false,
},
sid: { type: 'string', index: 'not_analyzed' },
properties: {
person: {
type: 'object',
properties: {
name: {
type: 'object',
enabled: false,
},
sid: { type: 'string', index: 'not_analyzed' },
},
message: { type: 'string' },
},
message: { type: 'string' },
},
},
});
expect(mappings.getFields('index', ['tweet']).sort(fc)).toEqual([
expect(mappings.getFields('index', []).sort(fc)).toEqual([
f('message'),
f('person.sid'),
]);
@ -217,23 +205,21 @@ describe('Mappings', () => {
test('Path tests', function () {
mappings.loadMappings({
index: {
person: {
properties: {
name1: {
type: 'object',
path: 'just_name',
properties: {
first1: { type: 'string' },
last1: { type: 'string', index_name: 'i_last_1' },
},
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' },
},
},
name2: {
type: 'object',
path: 'full',
properties: {
first2: { type: 'string' },
last2: { type: 'string', index_name: 'i_last_2' },
},
},
},
@ -251,10 +237,8 @@ describe('Mappings', () => {
test('Use index_name tests', function () {
mappings.loadMappings({
index: {
person: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
});
@ -284,17 +268,13 @@ describe('Mappings', () => {
});
mappings.loadMappings({
test_index1: {
type1: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
test_index2: {
type2: {
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
properties: {
last1: { type: 'string', index_name: 'i_last_1' },
},
},
});