mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [indexPatterns] support cross cluster patterns
* [vis] remove unused `hasTimeField` param
* [indexPatterns/create] fix method name in view
* [indexPatterns/create] disallow expanding with ccs
* [indexPatterns/create] field fetching is cheaper, react faster
* [indexPatterns/resolveTimePattern/tests] increase readability
* [tests/apiIntegration/indexPatterns] test conflict field output
* [indexPatterns/fieldCaps/readFieldCapsResponse] add unit tests
* [test/apiIntegration] ensure random word will not be valid
* [indexPatterns/ui/client] remove unused import
* remove use of auto-release-sinon
* [indexPatterns/create] don't allow expand when cross cluster
* [indexPatternsApiClient/stub] use angular promises
* [indexPatterns/create] add tests for base create ui behaviors
(cherry picked from commit e5deca679c
)
This commit is contained in:
parent
4edae39733
commit
60db93758b
98 changed files with 2692 additions and 938 deletions
|
@ -4,7 +4,6 @@ import Promise from 'bluebird';
|
|||
import { mkdirp as mkdirpNode } from 'mkdirp';
|
||||
|
||||
import manageUuid from './server/lib/manage_uuid';
|
||||
import ingest from './server/routes/api/ingest';
|
||||
import search from './server/routes/api/search';
|
||||
import settings from './server/routes/api/settings';
|
||||
import scripts from './server/routes/api/scripts';
|
||||
|
@ -147,7 +146,6 @@ module.exports = function (kibana) {
|
|||
// uuid
|
||||
manageUuid(server);
|
||||
// routes
|
||||
ingest(server);
|
||||
search(server);
|
||||
settings(server);
|
||||
scripts(server);
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
import angular from 'angular';
|
||||
import ngMock from 'ng_mock';
|
||||
import jQuery from 'jquery';
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import createIndexPatternTemplate from '../create_index_pattern.html';
|
||||
import { StubIndexPatternsApiClientModule } from 'ui/index_patterns/__tests__/stub_index_patterns_api_client';
|
||||
import { IndexPatternsApiClientProvider } from 'ui/index_patterns';
|
||||
import MockLogstashFieldsProvider from 'fixtures/logstash_fields';
|
||||
|
||||
describe('createIndexPattern UI', () => {
|
||||
let setup;
|
||||
const trash = [];
|
||||
|
||||
beforeEach(ngMock.module('kibana', StubIndexPatternsApiClientModule, ($provide) => {
|
||||
$provide.constant('buildSha', 'abc1234');
|
||||
$provide.constant('$route', {
|
||||
current: {
|
||||
params: {},
|
||||
locals: {
|
||||
indexPatternIds: []
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(ngMock.inject(($injector) => {
|
||||
setup = function () {
|
||||
const Private = $injector.get('Private');
|
||||
const Promise = $injector.get('Promise');
|
||||
const $compile = $injector.get('$compile');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
|
||||
const fields = Private(MockLogstashFieldsProvider);
|
||||
const indexPatternsApiClient = Private(IndexPatternsApiClientProvider);
|
||||
const $scope = $rootScope.$new();
|
||||
const $view = jQuery($compile(angular.element('<div>').html(createIndexPatternTemplate))($scope));
|
||||
trash.push(() => $scope.$destroy());
|
||||
$scope.$apply();
|
||||
|
||||
// prevents errors when switching to time pattern
|
||||
indexPatternsApiClient.testTimePattern = sinon.spy(() => Promise.resolve({
|
||||
all: ['logstash-0', 'logstash-2017.01.01'],
|
||||
matches: ['logstash-2017.01.01'],
|
||||
}));
|
||||
|
||||
const setNameTo = (name) => {
|
||||
$view.findTestSubject('createIndexPatternNameInput')
|
||||
.val(name)
|
||||
.change()
|
||||
.blur();
|
||||
|
||||
// ensure that name successfully applied
|
||||
const form = $view.find('form').scope().form;
|
||||
expect(form.name).to.have.property('$viewValue', name);
|
||||
};
|
||||
|
||||
return {
|
||||
$view,
|
||||
$scope,
|
||||
setNameTo,
|
||||
indexPatternsApiClient,
|
||||
fields
|
||||
};
|
||||
};
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
trash.forEach(fn => fn());
|
||||
trash.length = 0;
|
||||
});
|
||||
|
||||
describe('defaults', () => {
|
||||
it('renders `logstash-*` into the name input', () => {
|
||||
const { $view } = setup();
|
||||
|
||||
const $name = $view.findTestSubject('createIndexPatternNameInput');
|
||||
expect($name).to.have.length(1);
|
||||
expect($name.val()).to.be('logstash-*');
|
||||
});
|
||||
|
||||
it('attempts to getFieldsForWildcard for `logstash-*`', () => {
|
||||
const { indexPatternsApiClient } = setup();
|
||||
const { getFieldsForWildcard } = indexPatternsApiClient;
|
||||
|
||||
sinon.assert.called(getFieldsForWildcard);
|
||||
const calledWithPattern = getFieldsForWildcard.getCalls().some(call => {
|
||||
const [params] = call.args;
|
||||
return (
|
||||
params &&
|
||||
params.pattern &&
|
||||
params.pattern === 'logstash-*'
|
||||
);
|
||||
});
|
||||
|
||||
if (!calledWithPattern) {
|
||||
throw new Error('expected indexPatternsApiClient.getFieldsForWildcard to be called with pattern = logstash-*');
|
||||
}
|
||||
});
|
||||
|
||||
it('loads the time fields into the select box', () => {
|
||||
const { $view, fields } = setup();
|
||||
|
||||
const timeFieldOptions = $view.findTestSubject('createIndexPatternTimeFieldSelect')
|
||||
.find('option')
|
||||
.toArray()
|
||||
.map(option => option.innerText);
|
||||
|
||||
fields.forEach((field) => {
|
||||
if (!field.scripted && field.type === 'date') {
|
||||
expect(timeFieldOptions).to.contain(field.name);
|
||||
} else {
|
||||
expect(timeFieldOptions).to.not.contain(field.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the option (off) to expand wildcards', () => {
|
||||
const { $view } = setup();
|
||||
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
|
||||
expect($enableExpand).to.have.length(1);
|
||||
expect($enableExpand.is(':checked')).to.be(false);
|
||||
});
|
||||
|
||||
it('displays the option (off) to use time patterns', () => {
|
||||
const { $view } = setup();
|
||||
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
|
||||
expect($enableTimePattern).to.have.length(1);
|
||||
expect($enableTimePattern.is(':checked')).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross cluster pattern', () => {
|
||||
it('name input accepts `cluster2:logstash-*` pattern', () => {
|
||||
const { $view, setNameTo } = setup();
|
||||
setNameTo('cluster2:logstash-*');
|
||||
|
||||
const $name = $view.findTestSubject('createIndexPatternNameInput');
|
||||
const classes = [...$name.get(0).classList];
|
||||
expect(classes).to.contain('ng-valid');
|
||||
expect(classes).to.not.contain('ng-invalid');
|
||||
});
|
||||
|
||||
it('removes the option to expand wildcards', () => {
|
||||
const { $view, setNameTo } = setup();
|
||||
setNameTo('cluster2:logstash-*');
|
||||
|
||||
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
|
||||
expect($enableExpand).to.have.length(0);
|
||||
});
|
||||
|
||||
it('removes the option to use time patterns', () => {
|
||||
const { $view, setNameTo } = setup();
|
||||
setNameTo('cluster2:logstash-*');
|
||||
|
||||
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
|
||||
expect($enableTimePattern).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expand selected', () => {
|
||||
it('removes the option to use time patterns', () => {
|
||||
const { $view } = setup();
|
||||
|
||||
const { controller } = $view.findTestSubject('createIndexPatternContainer').scope();
|
||||
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
|
||||
expect($enableExpand).to.have.length(1);
|
||||
$enableExpand.click();
|
||||
expect(controller.isExpandWildcardEnabled()).to.be(true);
|
||||
|
||||
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
|
||||
expect($enableTimePattern).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('time pattern selected', () => {
|
||||
it('removes the option to use wildcard expansion', () => {
|
||||
const { $view } = setup();
|
||||
|
||||
const { controller } = $view.findTestSubject('createIndexPatternContainer').scope();
|
||||
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
|
||||
expect($enableTimePattern).to.have.length(1);
|
||||
$enableTimePattern.click();
|
||||
expect(controller.formValues.nameIsPattern).to.be(true);
|
||||
|
||||
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
|
||||
expect($enableExpand).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,6 +2,7 @@
|
|||
<kbn-management-indices>
|
||||
<div
|
||||
ng-controller="managementIndicesCreate as controller"
|
||||
data-test-subj="createIndexPatternContainer"
|
||||
class="kuiViewContent"
|
||||
>
|
||||
<h1
|
||||
|
@ -34,8 +35,7 @@
|
|||
class="kuiTextInput kuiTextInput--large"
|
||||
data-test-subj="createIndexPatternNameInput"
|
||||
ng-model="controller.formValues.name"
|
||||
ng-attr-placeholder="{{controller.formValues.defaultName}}"
|
||||
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 2500, 'blur': 0} }"
|
||||
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"
|
||||
validate-index-name
|
||||
allow-wildcard
|
||||
name="name"
|
||||
|
@ -124,12 +124,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Expand index pattern checkbox -->
|
||||
<div class="kuiVerticalRhythm" ng-if="controller.canExpandIndices()">
|
||||
<div class="kuiVerticalRhythm" ng-if="controller.canEnableExpandWildcard()">
|
||||
<label class="kuiCheckBoxLabel kuiVerticalRhythm">
|
||||
<input
|
||||
class="kuiCheckBox"
|
||||
type="checkbox"
|
||||
ng-model="controller.formValues.expandable"
|
||||
data-test-subj="createIndexPatternEnableExpand"
|
||||
ng-model="controller.formValues.expandWildcard"
|
||||
>
|
||||
<span class="kuiCheckBoxLabel__text">
|
||||
<span translate="KIBANA-EXPAND_INDEX_PATTERN"></span>
|
||||
|
|
|
@ -26,7 +26,7 @@ uiModules.get('apps/management')
|
|||
this.formValues = {
|
||||
name: config.get('indexPattern:placeholder'),
|
||||
nameIsPattern: false,
|
||||
expandable: false,
|
||||
expandWildcard: false,
|
||||
nameInterval: _.find(intervals, { name: 'daily' }),
|
||||
timeFieldOption: null,
|
||||
};
|
||||
|
@ -41,20 +41,20 @@ uiModules.get('apps/management')
|
|||
this.patternErrors = [];
|
||||
|
||||
const getTimeFieldOptions = () => {
|
||||
const missingPattern = !this.formValues.name;
|
||||
const missingInterval = this.formValues.nameIsPattern && !this.formValues.nameInterval;
|
||||
if (missingPattern || missingInterval) {
|
||||
return Promise.resolve({ options: [] });
|
||||
}
|
||||
|
||||
loadingCount += 1;
|
||||
return indexPatterns.mapper.clearCache(this.formValues.name)
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const pattern = mockIndexPattern(this.formValues);
|
||||
const { nameIsPattern, name } = this.formValues;
|
||||
|
||||
return indexPatterns.mapper.getFieldsForIndexPattern(pattern, {
|
||||
skipIndexPatternCache: true,
|
||||
});
|
||||
if (!name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (nameIsPattern) {
|
||||
return indexPatterns.fieldsFetcher.fetchForTimePattern(name);
|
||||
}
|
||||
|
||||
return indexPatterns.fieldsFetcher.fetchForWildcard(name);
|
||||
})
|
||||
.then(fields => {
|
||||
const dateFields = fields.filter(field => field.type === 'date');
|
||||
|
@ -132,14 +132,6 @@ uiModules.get('apps/management')
|
|||
this.existing = null;
|
||||
};
|
||||
|
||||
function mockIndexPattern(index) {
|
||||
// trick the mapper into thinking this is an indexPattern
|
||||
return {
|
||||
id: index.name,
|
||||
intervalName: index.nameInterval
|
||||
};
|
||||
}
|
||||
|
||||
const updateSamples = () => {
|
||||
const patternErrors = [];
|
||||
|
||||
|
@ -147,14 +139,8 @@ uiModules.get('apps/management')
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const pattern = mockIndexPattern(this.formValues);
|
||||
|
||||
loadingCount += 1;
|
||||
return indexPatterns.mapper.getIndicesForIndexPattern(pattern)
|
||||
.catch(err => {
|
||||
if (err instanceof IndexPatternMissingIndices) return;
|
||||
notify.error(err);
|
||||
})
|
||||
return indexPatterns.fieldsFetcher.testTimePattern(this.formValues.name)
|
||||
.then(existing => {
|
||||
const all = _.get(existing, 'all', []);
|
||||
const matches = _.get(existing, 'matches', []);
|
||||
|
@ -197,16 +183,35 @@ uiModules.get('apps/management')
|
|||
return Boolean(this.formValues.timeFieldOption.fieldName);
|
||||
};
|
||||
|
||||
this.canExpandIndices = () => {
|
||||
this.canEnableExpandWildcard = () => {
|
||||
return (
|
||||
this.isTimeBased() &&
|
||||
!this.isCrossClusterName() &&
|
||||
!this.formValues.nameIsPattern &&
|
||||
_.includes(this.formValues.name, '*')
|
||||
);
|
||||
};
|
||||
|
||||
this.isExpandWildcardEnabled = () => {
|
||||
return (
|
||||
this.canEnableExpandWildcard() &&
|
||||
!!this.formValues.expandWildcard
|
||||
);
|
||||
};
|
||||
|
||||
this.canUseTimePattern = () => {
|
||||
return this.isTimeBased() && !this.formValues.expandable;
|
||||
return (
|
||||
this.isTimeBased() &&
|
||||
!this.isExpandWildcardEnabled() &&
|
||||
!this.isCrossClusterName()
|
||||
);
|
||||
};
|
||||
|
||||
this.isCrossClusterName = () => {
|
||||
return (
|
||||
this.formValues.name &&
|
||||
this.formValues.name.includes(':')
|
||||
);
|
||||
};
|
||||
|
||||
this.isLoading = () => {
|
||||
|
@ -261,7 +266,6 @@ uiModules.get('apps/management')
|
|||
timeFieldOption,
|
||||
nameIsPattern,
|
||||
nameInterval,
|
||||
expandable
|
||||
} = this.formValues;
|
||||
|
||||
const id = name;
|
||||
|
@ -270,10 +274,9 @@ uiModules.get('apps/management')
|
|||
? timeFieldOption.fieldName
|
||||
: undefined;
|
||||
|
||||
// this seems wrong, but it's the original logic... https://git.io/vHYFo
|
||||
const notExpandable = (this.canExpandIndices() && !expandable)
|
||||
? true
|
||||
: undefined;
|
||||
const notExpandable = this.isExpandWildcardEnabled()
|
||||
? undefined
|
||||
: true;
|
||||
|
||||
// Only event-time-based index patterns set an intervalName.
|
||||
const intervalName = (this.canUseTimePattern() && nameIsPattern && nameInterval)
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import createMappingsFromPatternFields from '../create_mappings_from_pattern_fields';
|
||||
import expect from 'expect.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
let testFields;
|
||||
|
||||
describe('createMappingsFromPatternFields', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
testFields = [
|
||||
{
|
||||
'name': 'ip',
|
||||
'type': 'ip'
|
||||
},
|
||||
{
|
||||
'name': 'agent',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'bytes',
|
||||
'type': 'number'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
it('should throw an error if the argument is empty', function () {
|
||||
expect(createMappingsFromPatternFields).to.throwException(/argument must not be empty/);
|
||||
});
|
||||
|
||||
it('should not modify the original argument', function () {
|
||||
const testFieldClone = _.cloneDeep(testFields);
|
||||
const mappings = createMappingsFromPatternFields(testFields);
|
||||
|
||||
expect(mappings.ip).to.not.be(testFields[0]);
|
||||
expect(_.isEqual(testFields, testFieldClone)).to.be.ok();
|
||||
});
|
||||
|
||||
it('should set the same default mapping for all non-strings', function () {
|
||||
const mappings = createMappingsFromPatternFields(testFields);
|
||||
|
||||
_.forEach(mappings, function (mapping) {
|
||||
if (mapping.type !== 'text') {
|
||||
expect(_.isEqual(mapping, {
|
||||
type: mapping.type,
|
||||
index: true,
|
||||
doc_values: true
|
||||
})).to.be.ok();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should give strings a multi-field mapping with a "text" base type', function () {
|
||||
const mappings = createMappingsFromPatternFields(testFields);
|
||||
|
||||
_.forEach(mappings, function (mapping) {
|
||||
if (mapping.type === 'text') {
|
||||
expect(mapping).to.have.property('fields');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested fields', function () {
|
||||
testFields.push({ name: 'geo.coordinates', type: 'geo_point' });
|
||||
const mappings = createMappingsFromPatternFields(testFields);
|
||||
|
||||
expect(mappings).to.have.property('geo');
|
||||
expect(mappings.geo).to.have.property('properties');
|
||||
expect(mappings.geo.properties).to.have.property('coordinates');
|
||||
expect(_.isEqual(mappings.geo.properties.coordinates, {
|
||||
type: 'geo_point',
|
||||
index: true,
|
||||
doc_values: true
|
||||
})).to.be.ok();
|
||||
});
|
||||
|
||||
it('should map all number fields as an ES double', function () {
|
||||
const mappings = createMappingsFromPatternFields(testFields);
|
||||
|
||||
expect(mappings).to.have.property('bytes');
|
||||
expect(mappings.bytes).to.have.property('type', 'double');
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
// Creates an ES field mapping from a single field object in a kibana index pattern
|
||||
module.exports = function createMappingsFromPatternFields(fields) {
|
||||
if (_.isEmpty(fields)) {
|
||||
throw new Error('argument must not be empty');
|
||||
}
|
||||
|
||||
const mappings = {};
|
||||
|
||||
_.forEach(fields, function (field) {
|
||||
let mapping;
|
||||
|
||||
if (field.type === 'string') {
|
||||
mapping = {
|
||||
type: 'text',
|
||||
fields: {
|
||||
keyword: { type: 'keyword', ignore_above: 256 }
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
const fieldType = field.type === 'number' ? 'double' : field.type;
|
||||
mapping = {
|
||||
type: fieldType,
|
||||
index: true,
|
||||
doc_values: true
|
||||
};
|
||||
}
|
||||
|
||||
_.set(mappings, field.name.split('.').join('.properties.'), mapping);
|
||||
});
|
||||
|
||||
return mappings;
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
module.exports = Joi.object({
|
||||
id: Joi.string().required(),
|
||||
title: Joi.string().required(),
|
||||
time_field_name: Joi.string(),
|
||||
interval_name: Joi.string(),
|
||||
not_expandable: Joi.boolean(),
|
||||
source_filters: Joi.array(),
|
||||
fields: Joi.array().items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
count: Joi.number().integer(),
|
||||
scripted: Joi.boolean(),
|
||||
doc_values: Joi.boolean(),
|
||||
analyzed: Joi.boolean(),
|
||||
indexed: Joi.boolean(),
|
||||
script: Joi.string(),
|
||||
lang: Joi.string()
|
||||
})
|
||||
).required().min(1),
|
||||
field_format_map: Joi.object()
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import Joi from 'joi';
|
||||
import indexPatternSchema from './index_pattern_schema';
|
||||
|
||||
module.exports = Joi.object({
|
||||
index_pattern: indexPatternSchema.required()
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
import { registerFieldCapabilities } from './register_field_capabilities';
|
||||
|
||||
export default function (server) {
|
||||
registerFieldCapabilities(server);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import handleESError from '../../../lib/handle_es_error';
|
||||
import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values';
|
||||
|
||||
export function registerFieldCapabilities(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/{indices}/field_capabilities',
|
||||
method: ['GET'],
|
||||
handler: function (req, reply) {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
const indices = req.params.indices || '';
|
||||
|
||||
return callWithRequest(req, 'fieldStats', {
|
||||
fields: '*',
|
||||
level: 'cluster',
|
||||
index: indices,
|
||||
allowNoIndices: false
|
||||
})
|
||||
.then(
|
||||
(res) => {
|
||||
const fields = _.get(res, 'indices._all.fields', {});
|
||||
const fieldsFilteredValues = _.mapValues(fields, (value) => {
|
||||
return {
|
||||
searchable: value.searchable,
|
||||
aggregatable: value.aggregatable,
|
||||
readFromDocValues: shouldReadFieldFromDocValues(value.aggregatable, value.type)
|
||||
};
|
||||
});
|
||||
|
||||
const retVal = { fields: fieldsFilteredValues };
|
||||
if (res._shards && res._shards.failed) {
|
||||
retVal.shard_failure_response = res;
|
||||
}
|
||||
|
||||
reply(retVal);
|
||||
},
|
||||
(error) => {
|
||||
reply(handleESError(error));
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { castEsToKbnFieldTypeName } from '../utils';
|
||||
import { shouldReadFieldFromDocValues } from '../core_plugins/kibana/server/routes/api/ingest/should_read_field_from_doc_values';
|
||||
import { shouldReadFieldFromDocValues } from '../server/index_patterns/service/lib/field_capabilities/should_read_field_from_doc_values';
|
||||
|
||||
function stubbedLogstashFields() {
|
||||
return [
|
||||
// |aggregatable
|
||||
// | |searchable
|
||||
// name esType | | |metadata
|
||||
// name esType | | |metadata
|
||||
['bytes', 'long', true, true, { count: 10 } ],
|
||||
['ssl', 'boolean', true, true, { count: 20 } ],
|
||||
['@timestamp', 'date', true, true, { count: 30 } ],
|
||||
|
|
|
@ -40,8 +40,7 @@ module.exports = function VislibFixtures(Private) {
|
|||
defaultYExtents: false,
|
||||
setYExtents: false,
|
||||
yAxis: {},
|
||||
type: 'histogram',
|
||||
hasTimeField: true
|
||||
type: 'histogram'
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
|
|
@ -25,6 +25,11 @@ export function createFunctionalTestRunner({ log, configFile, configOverrides })
|
|||
const config = await readConfigFile(log, configFile, configOverrides);
|
||||
log.info('Config loaded');
|
||||
|
||||
if (config.get('testFiles').length === 0) {
|
||||
log.warning('No test files defined.');
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = createProviderCollection(lifecycle, log, config);
|
||||
await providers.loadAll();
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ export function ConsoleReporterProvider({ getService }) {
|
|||
|
||||
onPending = test => {
|
||||
log.write('-> ' + colors.pending(test.title));
|
||||
log.indent(2);
|
||||
}
|
||||
|
||||
onPass = test => {
|
||||
|
|
1
src/server/index_patterns/index.js
Normal file
1
src/server/index_patterns/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { indexPatternsMixin } from './mixin';
|
31
src/server/index_patterns/mixin.js
Normal file
31
src/server/index_patterns/mixin.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { IndexPatternsService } from './service';
|
||||
|
||||
import {
|
||||
createTestTimePatternRoute,
|
||||
createFieldsForWildcardRoute,
|
||||
createFieldsForTimePatternRoute,
|
||||
} from './routes';
|
||||
|
||||
export function indexPatternsMixin(kbnServer, server) {
|
||||
const pre = {
|
||||
/**
|
||||
* Create an instance of the `indexPatterns` service
|
||||
* @type {Hapi.Pre}
|
||||
*/
|
||||
getIndexPatternsService: {
|
||||
assign: 'indexPatterns',
|
||||
method(req, reply) {
|
||||
const dataCluster = req.server.plugins.elasticsearch.getCluster('data');
|
||||
const callDataCluster = (...args) => (
|
||||
dataCluster.callWithRequest(req, ...args)
|
||||
);
|
||||
|
||||
reply(new IndexPatternsService(callDataCluster));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server.route(createTestTimePatternRoute(pre));
|
||||
server.route(createFieldsForWildcardRoute(pre));
|
||||
server.route(createFieldsForTimePatternRoute(pre));
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
export const createFieldsForTimePatternRoute = pre => ({
|
||||
path: '/api/index_patterns/_fields_for_time_pattern',
|
||||
method: 'GET',
|
||||
config: {
|
||||
pre: [pre.getIndexPatternsService],
|
||||
validate: {
|
||||
query: Joi.object().keys({
|
||||
pattern: Joi.string().required(),
|
||||
look_back: Joi.number().min(1).required(),
|
||||
meta_fields: Joi.array().items(Joi.string()).default([]),
|
||||
}).default()
|
||||
},
|
||||
handler(req, reply) {
|
||||
const { indexPatterns } = req.pre;
|
||||
const {
|
||||
pattern,
|
||||
interval,
|
||||
look_back: lookBack,
|
||||
meta_fields: metaFields,
|
||||
} = req.query;
|
||||
|
||||
reply(
|
||||
indexPatterns.getFieldsForTimePattern({
|
||||
pattern,
|
||||
interval,
|
||||
lookBack,
|
||||
metaFields
|
||||
})
|
||||
.then(fields => ({ fields }))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
export const createFieldsForWildcardRoute = pre => ({
|
||||
path: '/api/index_patterns/_fields_for_wildcard',
|
||||
method: 'GET',
|
||||
config: {
|
||||
pre: [pre.getIndexPatternsService],
|
||||
validate: {
|
||||
query: Joi.object().keys({
|
||||
pattern: Joi.string().required(),
|
||||
meta_fields: Joi.array().items(Joi.string()).default([])
|
||||
}).default()
|
||||
},
|
||||
handler(req, reply) {
|
||||
const { indexPatterns } = req.pre;
|
||||
const {
|
||||
pattern,
|
||||
meta_fields: metaFields,
|
||||
} = req.query;
|
||||
|
||||
reply(
|
||||
indexPatterns.getFieldsForWildcard({
|
||||
pattern,
|
||||
metaFields
|
||||
})
|
||||
.then(fields => ({ fields }))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
3
src/server/index_patterns/routes/index.js
Normal file
3
src/server/index_patterns/routes/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { createTestTimePatternRoute } from './test_time_pattern_route';
|
||||
export { createFieldsForWildcardRoute } from './fields_for_wildcard_route';
|
||||
export { createFieldsForTimePatternRoute } from './fields_for_time_pattern_route';
|
22
src/server/index_patterns/routes/test_time_pattern_route.js
Normal file
22
src/server/index_patterns/routes/test_time_pattern_route.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
export const createTestTimePatternRoute = pre => ({
|
||||
path: '/api/index_patterns/_test_time_pattern',
|
||||
method: 'GET',
|
||||
config: {
|
||||
pre: [pre.getIndexPatternsService],
|
||||
validate: {
|
||||
query: Joi.object().keys({
|
||||
pattern: Joi.string().required()
|
||||
}).default()
|
||||
},
|
||||
handler(req, reply) {
|
||||
const { indexPatterns } = req.pre;
|
||||
const { pattern } = req.query;
|
||||
|
||||
reply(indexPatterns.testTimePattern({
|
||||
pattern,
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
1
src/server/index_patterns/service/index.js
Normal file
1
src/server/index_patterns/service/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { IndexPatternsService } from './index_patterns_service';
|
74
src/server/index_patterns/service/index_patterns_service.js
Normal file
74
src/server/index_patterns/service/index_patterns_service.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
getFieldCapabilities,
|
||||
resolveTimePattern,
|
||||
createNoMatchingIndicesError,
|
||||
isNoMatchingIndicesError,
|
||||
} from './lib';
|
||||
|
||||
export class IndexPatternsService {
|
||||
constructor(callDataCluster) {
|
||||
this._callDataCluster = callDataCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of field objects for an index pattern that may contain wildcards
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* @property {String} options.pattern The moment compatible time pattern
|
||||
* @property {Number} options.metaFields The list of underscore prefixed fields that should
|
||||
* be left in the field list (all others are removed).
|
||||
* @return {Promise<Array<Fields>>}
|
||||
*/
|
||||
async getFieldsForWildcard(options = {}) {
|
||||
const { pattern, metaFields } = options;
|
||||
return await getFieldCapabilities(this._callDataCluster, pattern, metaFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a time pattern into a list of indexes it could
|
||||
* have matched and ones it did match.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* @property {String} options.pattern The moment compatible time pattern
|
||||
* @return {Promise<Object>} object that lists the indices that match based
|
||||
* on a wildcard version of the time pattern (all)
|
||||
* and the indices that actually match the time
|
||||
* pattern (matches);
|
||||
*/
|
||||
async testTimePattern(options = {}) {
|
||||
const { pattern } = options;
|
||||
try {
|
||||
return await resolveTimePattern(this._callDataCluster, pattern);
|
||||
} catch (err) {
|
||||
if (isNoMatchingIndicesError(err)) {
|
||||
return {
|
||||
all: [],
|
||||
matches: []
|
||||
};
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of field objects for a time pattern
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* @property {String} options.pattern The moment compatible time pattern
|
||||
* @property {Number} options.lookBack The number of indices we will pull mappings for
|
||||
* @property {Number} options.metaFields The list of underscore prefixed fields that should
|
||||
* be left in the field list (all others are removed).
|
||||
* @return {Promise<Array<Fields>>}
|
||||
*/
|
||||
async getFieldsForTimePattern(options = {}) {
|
||||
const { pattern, lookBack, metaFields } = options;
|
||||
const { matches } = await resolveTimePattern(this._callDataCluster, pattern);
|
||||
const indices = matches.slice(0, lookBack);
|
||||
if (indices.length === 0) {
|
||||
throw createNoMatchingIndicesError(pattern);
|
||||
}
|
||||
return await getFieldCapabilities(this._callDataCluster, indices, metaFields);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// errors module is tested in test/api_integration/apis/index_patterns/es_errors/errors.js
|
||||
// so it can get real errors from elasticsearch and the es client to test with
|
124
src/server/index_patterns/service/lib/__tests__/es_api.js
Normal file
124
src/server/index_patterns/service/lib/__tests__/es_api.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
|
||||
import { convertEsIndexNotFoundError } from '../errors';
|
||||
import * as convertEsIndexNotFoundErrorNS from '../errors';
|
||||
|
||||
import { callIndexAliasApi, callFieldCapsApi } from '../es_api';
|
||||
|
||||
describe('server/index_patterns/service/lib/es_api', () => {
|
||||
describe('#callIndexAliasApi()', () => {
|
||||
let sandbox;
|
||||
beforeEach(() => sandbox = sinon.sandbox.create());
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
it('calls indices.getAlias() via callCluster', async () => {
|
||||
const callCluster = sinon.stub();
|
||||
await callIndexAliasApi(callCluster);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
sinon.assert.calledWith(callCluster, 'indices.getAlias');
|
||||
});
|
||||
|
||||
it('passes indices directly to es api', async () => {
|
||||
const football = {};
|
||||
const callCluster = sinon.stub();
|
||||
await callIndexAliasApi(callCluster, football);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
expect(callCluster.args[0][1].index).to.be(football);
|
||||
});
|
||||
|
||||
it('returns the es response directly', async () => {
|
||||
const football = {};
|
||||
const callCluster = sinon.stub().returns(football);
|
||||
const resp = await callIndexAliasApi(callCluster);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
expect(resp).to.be(football);
|
||||
});
|
||||
|
||||
it('sets ignoreUnavailable and allowNoIndices params', async () => {
|
||||
const callCluster = sinon.stub();
|
||||
await callIndexAliasApi(callCluster);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
|
||||
const passedOpts = callCluster.args[0][1];
|
||||
expect(passedOpts).to.have.property('ignoreUnavailable', true);
|
||||
expect(passedOpts).to.have.property('allowNoIndices', false);
|
||||
});
|
||||
|
||||
it('handles errors with convertEsIndexNotFoundError()', async () => {
|
||||
const indices = [];
|
||||
const esError = new Error('esError');
|
||||
const convertedError = new Error('convertedError');
|
||||
|
||||
sandbox.stub(convertEsIndexNotFoundErrorNS, 'convertEsIndexNotFoundError', () => { throw convertedError; });
|
||||
const callCluster = sinon.spy(async () => { throw esError; });
|
||||
try {
|
||||
await callIndexAliasApi(callCluster, indices);
|
||||
throw new Error('expected callIndexAliasApi() to throw');
|
||||
} catch (error) {
|
||||
expect(error).to.be(convertedError);
|
||||
sinon.assert.calledOnce(convertEsIndexNotFoundError);
|
||||
expect(convertEsIndexNotFoundError.args[0][0]).to.be(indices);
|
||||
expect(convertEsIndexNotFoundError.args[0][1]).to.be(esError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#callFieldCapsApi()', () => {
|
||||
let sandbox;
|
||||
beforeEach(() => sandbox = sinon.sandbox.create());
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
it('calls fieldCaps() via callCluster', async () => {
|
||||
const callCluster = sinon.stub();
|
||||
await callFieldCapsApi(callCluster);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
sinon.assert.calledWith(callCluster, 'fieldCaps');
|
||||
});
|
||||
|
||||
it('passes indices directly to es api', async () => {
|
||||
const football = {};
|
||||
const callCluster = sinon.stub();
|
||||
await callFieldCapsApi(callCluster, football);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
expect(callCluster.args[0][1].index).to.be(football);
|
||||
});
|
||||
|
||||
it('returns the es response directly', async () => {
|
||||
const football = {};
|
||||
const callCluster = sinon.stub().returns(football);
|
||||
const resp = await callFieldCapsApi(callCluster);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
expect(resp).to.be(football);
|
||||
});
|
||||
|
||||
it('sets ignoreUnavailable, allowNoIndices, and fields params', async () => {
|
||||
const callCluster = sinon.stub();
|
||||
await callFieldCapsApi(callCluster);
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
|
||||
const passedOpts = callCluster.args[0][1];
|
||||
expect(passedOpts).to.have.property('fields', '*');
|
||||
expect(passedOpts).to.have.property('ignoreUnavailable', true);
|
||||
expect(passedOpts).to.have.property('allowNoIndices', false);
|
||||
});
|
||||
|
||||
it('handles errors with convertEsIndexNotFoundError()', async () => {
|
||||
const indices = [];
|
||||
const esError = new Error('esError');
|
||||
const convertedError = new Error('convertedError');
|
||||
|
||||
sandbox.stub(convertEsIndexNotFoundErrorNS, 'convertEsIndexNotFoundError', () => { throw convertedError; });
|
||||
const callCluster = sinon.spy(async () => { throw esError; });
|
||||
try {
|
||||
await callFieldCapsApi(callCluster, indices);
|
||||
throw new Error('expected callFieldCapsApi() to throw');
|
||||
} catch (error) {
|
||||
expect(error).to.be(convertedError);
|
||||
sinon.assert.calledOnce(convertEsIndexNotFoundError);
|
||||
expect(convertEsIndexNotFoundError.args[0][0]).to.be(indices);
|
||||
expect(convertEsIndexNotFoundError.args[0][1]).to.be(esError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { callIndexAliasApi } from '../es_api';
|
||||
import * as callIndexAliasApiNS from '../es_api';
|
||||
import { timePatternToWildcard } from '../time_pattern_to_wildcard';
|
||||
import * as timePatternToWildcardNS from '../time_pattern_to_wildcard';
|
||||
|
||||
import { resolveTimePattern } from '../resolve_time_pattern';
|
||||
|
||||
const TIME_PATTERN = '[logs-]dddd-YYYY.w';
|
||||
|
||||
describe('server/index_patterns/service/lib/resolve_time_pattern', () => {
|
||||
let sandbox;
|
||||
beforeEach(() => sandbox = sinon.sandbox.create());
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('resolveTimePattern()', () => {
|
||||
describe('pre request', () => {
|
||||
it('uses callIndexAliasApi() fn', async () => {
|
||||
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({});
|
||||
await resolveTimePattern(noop, TIME_PATTERN);
|
||||
sinon.assert.calledOnce(callIndexAliasApi);
|
||||
});
|
||||
|
||||
it('converts the time pattern to a wildcard with timePatternToWildcard', async () => {
|
||||
const timePattern = {};
|
||||
const wildcard = {};
|
||||
|
||||
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard')
|
||||
.returns(wildcard);
|
||||
|
||||
await resolveTimePattern(noop, timePattern);
|
||||
sinon.assert.calledOnce(timePatternToWildcard);
|
||||
expect(timePatternToWildcard.firstCall.args).to.eql([timePattern]);
|
||||
});
|
||||
|
||||
it('passes the converted wildcard as the index to callIndexAliasApi()', async () => {
|
||||
const timePattern = {};
|
||||
const wildcard = {};
|
||||
|
||||
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({});
|
||||
sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard')
|
||||
.returns(wildcard);
|
||||
|
||||
await resolveTimePattern(noop, timePattern);
|
||||
sinon.assert.calledOnce(callIndexAliasApi);
|
||||
expect(callIndexAliasApi.firstCall.args[1]).to.be(wildcard);
|
||||
});
|
||||
});
|
||||
|
||||
describe('read response', () => {
|
||||
it('returns all aliases names in result.all, ordered by time desc', async () => {
|
||||
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({
|
||||
'logs-2016.2': {},
|
||||
'logs-Saturday-2017.1': {},
|
||||
'logs-2016.1': {},
|
||||
'logs-Sunday-2017.1': {},
|
||||
'logs-2015': {},
|
||||
'logs-2016.3': {},
|
||||
'logs-Friday-2017.1': {},
|
||||
});
|
||||
|
||||
const resp = await resolveTimePattern(noop, TIME_PATTERN);
|
||||
expect(resp).to.have.property('all');
|
||||
expect(resp.all).to.eql([
|
||||
'logs-Saturday-2017.1',
|
||||
'logs-Friday-2017.1',
|
||||
'logs-Sunday-2017.1',
|
||||
'logs-2016.3',
|
||||
'logs-2016.2',
|
||||
'logs-2016.1',
|
||||
'logs-2015',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns all indices matching the time pattern in matches, ordered by time desc', async () => {
|
||||
sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({
|
||||
'logs-2016.2': {},
|
||||
'logs-Saturday-2017.1': {},
|
||||
'logs-2016.1': {},
|
||||
'logs-Sunday-2017.1': {},
|
||||
'logs-2015': {},
|
||||
'logs-2016.3': {},
|
||||
'logs-Friday-2017.1': {},
|
||||
});
|
||||
|
||||
const resp = await resolveTimePattern(noop, TIME_PATTERN);
|
||||
expect(resp).to.have.property('matches');
|
||||
expect(resp.matches).to.eql([
|
||||
'logs-Saturday-2017.1',
|
||||
'logs-Friday-2017.1',
|
||||
'logs-Sunday-2017.1'
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { timePatternToWildcard } from '../time_pattern_to_wildcard';
|
||||
|
||||
describe('server/index_patterns/service/lib/time_pattern_to_wildcard', () => {
|
||||
const tests = [
|
||||
['[logstash-]YYYY.MM.DD', 'logstash-*'],
|
||||
['YYYY[-department-].w', '*-department-*'],
|
||||
['YYYY.MM[.department].w', '*.department*'],
|
||||
['YYYY.MM.[department].w[-old]', '*department*-old'],
|
||||
];
|
||||
|
||||
tests.forEach(([input, expected]) => {
|
||||
it(`parses ${input}`, () => {
|
||||
const output = timePatternToWildcard(input);
|
||||
if (output !== expected) {
|
||||
throw new Error(`expected ${input} to parse to ${expected} but got ${output}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
51
src/server/index_patterns/service/lib/errors.js
Normal file
51
src/server/index_patterns/service/lib/errors.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import Boom from 'boom';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const ERR_ES_INDEX_NOT_FOUND = 'index_not_found_exception';
|
||||
const ERR_NO_MATCHING_INDICES = 'no_matching_indices';
|
||||
|
||||
/**
|
||||
* Determines if an error is an elasticsearch error that's
|
||||
* describing a failure caused by missing index/indices
|
||||
* @param {Any} err
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function isEsIndexNotFoundError(err) {
|
||||
return get(err, ['body', 'error', 'type']) === ERR_ES_INDEX_NOT_FOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error that informs that no indices match the given pattern.
|
||||
*
|
||||
* @param {String} pattern the pattern which indexes were supposed to match
|
||||
* @return {Boom}
|
||||
*/
|
||||
export function createNoMatchingIndicesError(pattern) {
|
||||
const err = Boom.notFound(`No indices match pattern "${pattern}"`);
|
||||
err.output.payload.code = ERR_NO_MATCHING_INDICES;
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determins if an error is produced by `createNoMatchingIndicesError()`
|
||||
*
|
||||
* @param {Any} err
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function isNoMatchingIndicesError(err) {
|
||||
return get(err, ['output', 'payload', 'code']) === ERR_NO_MATCHING_INDICES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap "index_not_found_exception" errors in custom Boom errors
|
||||
* automatically
|
||||
* @param {[type]} indices [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
export function convertEsIndexNotFoundError(indices, error) {
|
||||
if (isEsIndexNotFoundError(error)) {
|
||||
return createNoMatchingIndicesError(indices);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
51
src/server/index_patterns/service/lib/es_api.js
Normal file
51
src/server/index_patterns/service/lib/es_api.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { convertEsIndexNotFoundError } from './errors';
|
||||
|
||||
/**
|
||||
* Call the index.getAlias API for a list of indices.
|
||||
*
|
||||
* If `indices` is an array or comma-separated list and some of the
|
||||
* values don't match anything but others do this will return the
|
||||
* matches and not throw an error.
|
||||
*
|
||||
* If not a single index matches then a NoMatchingIndicesError will
|
||||
* be thrown.
|
||||
*
|
||||
* @param {Function} callCluster bound function for accessing an es client
|
||||
* @param {Array<String>|String} indices
|
||||
* @return {Promise<IndexAliasResponse>}
|
||||
*/
|
||||
export async function callIndexAliasApi(callCluster, indices) {
|
||||
try {
|
||||
return await callCluster('indices.getAlias', {
|
||||
index: indices,
|
||||
ignoreUnavailable: true,
|
||||
allowNoIndices: false
|
||||
});
|
||||
} catch (error) {
|
||||
throw convertEsIndexNotFoundError(indices, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the fieldCaps API for a list of indices.
|
||||
*
|
||||
* Just like callIndexAliasApi(), callFieldCapsApi() throws
|
||||
* if no indexes are matched, but will return potentially
|
||||
* "partial" results if even a single index is matched.
|
||||
*
|
||||
* @param {Function} callCluster bound function for accessing an es client
|
||||
* @param {Array<String>|String} indices
|
||||
* @return {Promise<FieldCapsResponse>}
|
||||
*/
|
||||
export async function callFieldCapsApi(callCluster, indices) {
|
||||
try {
|
||||
return await callCluster('fieldCaps', {
|
||||
index: indices,
|
||||
fields: '*',
|
||||
ignoreUnavailable: true,
|
||||
allowNoIndices: false
|
||||
});
|
||||
} catch (error) {
|
||||
throw convertEsIndexNotFoundError(indices, error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { identity, shuffle, sortBy } from 'lodash';
|
||||
|
||||
import { getFieldCapabilities } from '../field_capabilities';
|
||||
|
||||
import { callFieldCapsApi } from '../../es_api';
|
||||
import * as callFieldCapsApiNS from '../../es_api';
|
||||
|
||||
import { readFieldCapsResponse } from '../field_caps_response';
|
||||
import * as readFieldCapsResponseNS from '../field_caps_response';
|
||||
|
||||
import { mergeOverrides } from '../overrides';
|
||||
import * as mergeOverridesNS from '../overrides';
|
||||
|
||||
describe('index_patterns/field_capabilities/field_capabilities', () => {
|
||||
let sandbox;
|
||||
beforeEach(() => sandbox = sinon.sandbox.create());
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
const footballs = [
|
||||
{ 'could be aything': true },
|
||||
{ 'used to verify that values are directly passed through': true }
|
||||
];
|
||||
|
||||
// assert that the stub was called with the exact `args`, using === matching
|
||||
const calledWithExactly = (stub, args, matcher = sinon.match.same) => {
|
||||
sinon.assert.calledWithExactly(stub, ...args.map(arg => matcher(arg)));
|
||||
};
|
||||
|
||||
const stubDeps = (options = {}) => {
|
||||
const {
|
||||
esResponse = {},
|
||||
fieldsFromFieldCaps = [],
|
||||
mergeOverrides = identity
|
||||
} = options;
|
||||
|
||||
sandbox.stub(callFieldCapsApiNS, 'callFieldCapsApi', async () => esResponse);
|
||||
sandbox.stub(readFieldCapsResponseNS, 'readFieldCapsResponse', () => fieldsFromFieldCaps);
|
||||
sandbox.stub(mergeOverridesNS, 'mergeOverrides', mergeOverrides);
|
||||
};
|
||||
|
||||
describe('calls `callFieldCapsApi()`', () => {
|
||||
it('passes exact `callCluster` and `indices` args through', async () => {
|
||||
stubDeps();
|
||||
|
||||
await getFieldCapabilities(footballs[0], footballs[1]);
|
||||
sinon.assert.calledOnce(callFieldCapsApi);
|
||||
calledWithExactly(callFieldCapsApi, [footballs[0], footballs[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calls `readFieldCapsResponse`', () => {
|
||||
it('passes exact es response', async () => {
|
||||
stubDeps({
|
||||
esResponse: footballs[0]
|
||||
});
|
||||
|
||||
await getFieldCapabilities();
|
||||
sinon.assert.calledOnce(readFieldCapsResponse);
|
||||
calledWithExactly(readFieldCapsResponse, [footballs[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('response order', () => {
|
||||
it('always returns fields in alphabetical order', async () => {
|
||||
const letters = 'ambcdfjopngihkel'.split('');
|
||||
const sortedLetters = sortBy(letters);
|
||||
|
||||
stubDeps({
|
||||
fieldsFromFieldCaps: shuffle(letters.map(name => ({ name })))
|
||||
});
|
||||
|
||||
const fieldNames = (await getFieldCapabilities()).map(field => field.name);
|
||||
expect(fieldNames).to.eql(sortedLetters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metaFields', () => {
|
||||
it('ensures there is a response for each metaField', async () => {
|
||||
stubDeps({
|
||||
fieldsFromFieldCaps: [
|
||||
{ name: 'foo' },
|
||||
{ name: 'bar' },
|
||||
]
|
||||
});
|
||||
|
||||
const resp = await getFieldCapabilities(undefined, undefined, ['meta1', 'meta2']);
|
||||
expect(resp).to.have.length(4);
|
||||
expect(resp.map(field => field.name)).to.eql(['bar', 'foo', 'meta1', 'meta2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaults', () => {
|
||||
const properties = [
|
||||
'name',
|
||||
'type',
|
||||
'searchable',
|
||||
'aggregatable',
|
||||
'readFromDocValues',
|
||||
];
|
||||
|
||||
const createField = () => ({
|
||||
name: footballs[0],
|
||||
type: footballs[0],
|
||||
searchable: footballs[0],
|
||||
aggregatable: footballs[0],
|
||||
readFromDocValues: footballs[0]
|
||||
});
|
||||
|
||||
describe('ensures that every field has property:', () => {
|
||||
properties.forEach(property => {
|
||||
it(property, async () => {
|
||||
const field = createField();
|
||||
delete field[property];
|
||||
|
||||
stubDeps({
|
||||
fieldsFromFieldCaps: [field]
|
||||
});
|
||||
|
||||
const resp = await getFieldCapabilities();
|
||||
expect(resp).to.have.length(1);
|
||||
expect(resp[0]).to.have.property(property);
|
||||
expect(resp[0][property]).to.not.be(footballs[0]);
|
||||
|
||||
// ensure field object was not mutated
|
||||
expect(field).to.not.have.property(property);
|
||||
Object.keys(field).forEach(key => {
|
||||
// ensure response field has original values from field
|
||||
expect(resp[0][key]).to.be(footballs[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overrides', () => {
|
||||
it('passes each field to `mergeOverrides()`', async () => {
|
||||
const fieldsFromFieldCaps = [
|
||||
{ name: 'foo' },
|
||||
{ name: 'bar' },
|
||||
{ name: 'baz' },
|
||||
];
|
||||
|
||||
stubDeps({ fieldsFromFieldCaps });
|
||||
|
||||
sinon.assert.notCalled(mergeOverrides);
|
||||
await getFieldCapabilities();
|
||||
sinon.assert.calledThrice(mergeOverrides);
|
||||
|
||||
expect(mergeOverrides.args[0][0]).to.have.property('name', 'foo');
|
||||
expect(mergeOverrides.args[1][0]).to.have.property('name', 'bar');
|
||||
expect(mergeOverrides.args[2][0]).to.have.property('name', 'baz');
|
||||
});
|
||||
|
||||
it('replaces field with return value', async () => {
|
||||
const fieldsFromFieldCaps = [
|
||||
{ name: 'foo', bar: 1 },
|
||||
{ name: 'baz', box: 2 },
|
||||
];
|
||||
|
||||
stubDeps({
|
||||
fieldsFromFieldCaps,
|
||||
mergeOverrides() {
|
||||
return { notFieldAnymore: 1 };
|
||||
}
|
||||
});
|
||||
|
||||
expect(await getFieldCapabilities()).to.eql([
|
||||
{ notFieldAnymore: 1 },
|
||||
{ notFieldAnymore: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import * as shouldReadFieldFromDocValuesNS from '../should_read_field_from_doc_values';
|
||||
import { shouldReadFieldFromDocValues } from '../should_read_field_from_doc_values';
|
||||
|
||||
import { getKbnFieldType } from '../../../../../../utils';
|
||||
import { readFieldCapsResponse } from '../field_caps_response';
|
||||
import esResponse from './fixtures/es_field_caps_response.json';
|
||||
|
||||
describe('index_patterns/field_capabilities/field_caps_response', () => {
|
||||
let sandbox;
|
||||
beforeEach(() => sandbox = sinon.sandbox.create());
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('readFieldCapsResponse()', () => {
|
||||
describe('conflicts', () => {
|
||||
it('returns a field for each in response, no filtering', () => {
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
expect(fields).to.have.length(13);
|
||||
});
|
||||
|
||||
it('includes only name, type, searchable, aggregatable, readFromDocValues, and maybe conflictDescriptions of each field', () => {
|
||||
const responseClone = cloneDeep(esResponse);
|
||||
// try to trick it into including an extra field
|
||||
responseClone.fields['@timestamp'].date.extraCapability = true;
|
||||
const fields = readFieldCapsResponse(responseClone);
|
||||
|
||||
fields.forEach(field => {
|
||||
if (field.conflictDescriptions) {
|
||||
delete field.conflictDescriptions;
|
||||
}
|
||||
|
||||
expect(Object.keys(field)).to.eql([
|
||||
'name',
|
||||
'type',
|
||||
'searchable',
|
||||
'aggregatable',
|
||||
'readFromDocValues'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls shouldReadFieldFromDocValues() for each non-conflict field', () => {
|
||||
sandbox.spy(shouldReadFieldFromDocValuesNS, 'shouldReadFieldFromDocValues');
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
const conflictCount = fields.filter(f => f.type === 'conflict').length;
|
||||
sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount);
|
||||
});
|
||||
|
||||
it('converts es types to kibana types', () => {
|
||||
readFieldCapsResponse(esResponse).forEach(field => {
|
||||
if (!getKbnFieldType(field.type)) {
|
||||
throw new Error(`expected field to have kibana type, got ${field.type}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns fields with multiple types as conflicts', () => {
|
||||
const fields = readFieldCapsResponse(esResponse);
|
||||
const conflicts = fields.filter(f => f.type === 'conflict');
|
||||
expect(conflicts).to.eql([
|
||||
{
|
||||
name: 'success',
|
||||
type: 'conflict',
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
conflictDescriptions: {
|
||||
boolean: [
|
||||
'index1'
|
||||
],
|
||||
keyword: [
|
||||
'index2'
|
||||
]
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"fields": {
|
||||
"_routing": {
|
||||
"_routing": {
|
||||
"type": "_routing",
|
||||
"searchable": true,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"_index": {
|
||||
"_index": {
|
||||
"type": "_index",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"_type": {
|
||||
"_type": {
|
||||
"type": "_type",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"_all": {
|
||||
"_all": {
|
||||
"type": "_all",
|
||||
"searchable": true,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"_seq_no": {
|
||||
"_seq_no": {
|
||||
"type": "_seq_no",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"_parent": {
|
||||
"_parent": {
|
||||
"type": "_parent",
|
||||
"searchable": false,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"@timestamp": {
|
||||
"date": {
|
||||
"type": "date",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"_field_names": {
|
||||
"_field_names": {
|
||||
"type": "_field_names",
|
||||
"searchable": true,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"boolean": {
|
||||
"type": "boolean",
|
||||
"searchable": true,
|
||||
"aggregatable": true,
|
||||
"indices": [
|
||||
"index1"
|
||||
]
|
||||
},
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"searchable": true,
|
||||
"aggregatable": true,
|
||||
"indices": [
|
||||
"index2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"_source": {
|
||||
"_source": {
|
||||
"type": "_source",
|
||||
"searchable": false,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"_id": {
|
||||
"_id": {
|
||||
"type": "_id",
|
||||
"searchable": true,
|
||||
"aggregatable": true
|
||||
}
|
||||
},
|
||||
"_version": {
|
||||
"_version": {
|
||||
"type": "_version",
|
||||
"searchable": false,
|
||||
"aggregatable": false
|
||||
}
|
||||
},
|
||||
"_uid": {
|
||||
"_uid": {
|
||||
"type": "_uid",
|
||||
"searchable": false,
|
||||
"aggregatable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { defaults, indexBy, sortBy } from 'lodash';
|
||||
|
||||
import { callFieldCapsApi } from '../es_api';
|
||||
import { readFieldCapsResponse } from './field_caps_response';
|
||||
import { mergeOverrides } from './overrides';
|
||||
|
||||
export const concatIfUniq = (arr, value) => (
|
||||
arr.includes(value) ? arr : arr.concat(value)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the field capabilities for field in `indices`, excluding
|
||||
* all internal/underscore-prefixed fields that are not in `metaFields`
|
||||
*
|
||||
* @param {Function} callCluster bound function for accessing an es client
|
||||
* @param {Array} [indices=[]] the list of indexes to check
|
||||
* @param {Array} [metaFields=[]] the list of internal fields to include
|
||||
* @return {Promise<Array<FieldInfo>>}
|
||||
*/
|
||||
export async function getFieldCapabilities(callCluster, indices = [], metaFields = []) {
|
||||
const esFieldCaps = await callFieldCapsApi(callCluster, indices);
|
||||
const fieldsFromFieldCapsByName = indexBy(readFieldCapsResponse(esFieldCaps), 'name');
|
||||
|
||||
const allFieldsUnsorted = Object
|
||||
.keys(fieldsFromFieldCapsByName)
|
||||
.filter(name => !name.startsWith('_'))
|
||||
.concat(metaFields)
|
||||
.reduce(concatIfUniq, [])
|
||||
.map(name => defaults({}, fieldsFromFieldCapsByName[name], {
|
||||
name,
|
||||
type: 'string',
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false
|
||||
}))
|
||||
.map(mergeOverrides);
|
||||
|
||||
return sortBy(allFieldsUnsorted, 'name');
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import { castEsToKbnFieldTypeName } from '../../../../../utils';
|
||||
import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values';
|
||||
|
||||
/**
|
||||
* Read the response from the _field_caps API to determine the type and
|
||||
* "aggregatable"/"searchable" status of each field.
|
||||
*
|
||||
* For reference, the _field_caps response should look like this:
|
||||
*
|
||||
* {
|
||||
* "fields": {
|
||||
* "<fieldName>": {
|
||||
* "<esType>": {
|
||||
* "type": "<esType>",
|
||||
* "searchable": true,
|
||||
* "aggregatable": false,
|
||||
* // "indices" is only included when multiple
|
||||
* // types are found for a single field
|
||||
* "indices": [
|
||||
* "<index>"
|
||||
* ]
|
||||
* },
|
||||
* "<esType2>": {
|
||||
* "type": "<esType2>",
|
||||
* "searchable": true,
|
||||
* ...
|
||||
*
|
||||
* Returned array includes an object for each field in the _field_caps
|
||||
* response. When the field uses the same configuation across all indices
|
||||
* it should look something like this:
|
||||
*
|
||||
* {
|
||||
* "name": "<fieldName>"
|
||||
* "type": "<kbnType>",
|
||||
* "aggregatable": <bool>,
|
||||
* "searchable": <bool>,
|
||||
* }
|
||||
*
|
||||
* If the field has different data types in indices it will be of type
|
||||
* "conflict" and include a description of where conflicts can be found
|
||||
*
|
||||
* {
|
||||
* "name": "<fieldName>",
|
||||
* "type": "conflict",
|
||||
* "aggregatable": false,
|
||||
* "searchable": false,
|
||||
* conflictDescriptions: {
|
||||
* "<esType1>": [
|
||||
* "<index1>"
|
||||
* ],
|
||||
* "<esType2>": [
|
||||
* "<index2>"
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param {FieldCapsResponse} fieldCapsResponse
|
||||
* @return {Promise<Array<FieldInfo>>}
|
||||
*/
|
||||
export function readFieldCapsResponse(fieldCapsResponse) {
|
||||
const capsByNameThenType = fieldCapsResponse.fields;
|
||||
return Object.keys(capsByNameThenType).map(fieldName => {
|
||||
const capsByType = capsByNameThenType[fieldName];
|
||||
const types = Object.keys(capsByType);
|
||||
|
||||
if (types.length > 1) {
|
||||
return {
|
||||
name: fieldName,
|
||||
type: 'conflict',
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
conflictDescriptions: types.reduce((acc, esType) => ({
|
||||
...acc,
|
||||
[esType]: capsByType[esType].indices
|
||||
}), {})
|
||||
};
|
||||
}
|
||||
|
||||
const esType = types[0];
|
||||
const caps = capsByType[esType];
|
||||
return {
|
||||
name: fieldName,
|
||||
type: castEsToKbnFieldTypeName(esType),
|
||||
searchable: caps.searchable,
|
||||
aggregatable: caps.aggregatable,
|
||||
readFromDocValues: shouldReadFieldFromDocValues(caps.aggregatable, esType),
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { getFieldCapabilities } from './field_capabilities';
|
|
@ -0,0 +1,32 @@
|
|||
import { merge } from 'lodash';
|
||||
|
||||
const OVERRIDES = {
|
||||
_source: { type: '_source' },
|
||||
_index: { type: 'string' },
|
||||
_type: { type: 'string' },
|
||||
_id: { type: 'string' },
|
||||
_timestamp: {
|
||||
type: 'date',
|
||||
searchable: true,
|
||||
aggregatable: true
|
||||
},
|
||||
_score: {
|
||||
type: 'number',
|
||||
searchable: false,
|
||||
aggregatable: false
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge overrides for specific metaFields
|
||||
*
|
||||
* @param {FieldInfo} field
|
||||
* @return {FieldInfo}
|
||||
*/
|
||||
export function mergeOverrides(field) {
|
||||
if (OVERRIDES.hasOwnProperty(field.name)) {
|
||||
return merge(field, OVERRIDES[field.name]);
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
// see https://github.com/elastic/kibana/issues/11141 for details
|
||||
export function shouldReadFieldFromDocValues(aggregatable, esType) {
|
||||
return aggregatable && esType !== 'text' && !esType.startsWith('_');
|
||||
}
|
3
src/server/index_patterns/service/lib/index.js
Normal file
3
src/server/index_patterns/service/lib/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { getFieldCapabilities } from './field_capabilities';
|
||||
export { resolveTimePattern } from './resolve_time_pattern';
|
||||
export { createNoMatchingIndicesError, isNoMatchingIndicesError } from './errors';
|
|
@ -0,0 +1,57 @@
|
|||
import { chain } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { timePatternToWildcard } from './time_pattern_to_wildcard';
|
||||
import { callIndexAliasApi } from './es_api';
|
||||
|
||||
/**
|
||||
* Convert a time pattern into a list of indexes it could
|
||||
* have matched and ones it did match.
|
||||
*
|
||||
* @param {Function} callCluster bound function for accessing an es client
|
||||
* @param {String} timePattern
|
||||
* @return {Promise<Object>} object that lists the indices that match based
|
||||
* on a wildcard version of the time pattern (all)
|
||||
* and the indices that actually match the time
|
||||
* pattern (matches);
|
||||
*/
|
||||
export async function resolveTimePattern(callCluster, timePattern) {
|
||||
const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern));
|
||||
|
||||
const allIndexDetails = chain(aliases)
|
||||
.reduce((acc, index, indexName) => acc.concat(
|
||||
indexName,
|
||||
Object.keys(index.aliases || {})
|
||||
), [])
|
||||
.sort()
|
||||
.uniq(true)
|
||||
.map(indexName => {
|
||||
const parsed = moment(indexName, timePattern, true);
|
||||
if (!parsed.isValid()) {
|
||||
return {
|
||||
valid: false,
|
||||
indexName,
|
||||
order: indexName,
|
||||
isMatch: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
indexName,
|
||||
order: parsed,
|
||||
isMatch: indexName === parsed.format(timePattern)
|
||||
};
|
||||
})
|
||||
.sortByOrder(['valid', 'order'], ['desc', 'desc'])
|
||||
.value();
|
||||
|
||||
return {
|
||||
all: allIndexDetails
|
||||
.map(details => details.indexName),
|
||||
|
||||
matches: allIndexDetails
|
||||
.filter(details => details.isMatch)
|
||||
.map(details => details.indexName),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Convert a moment time pattern to an index wildcard
|
||||
* by extracting all of the "plain text" component and
|
||||
* replacing all moment pattern components with "*"
|
||||
*
|
||||
* @param {String} timePattern
|
||||
* @return {String}
|
||||
*/
|
||||
export function timePatternToWildcard(timePattern) {
|
||||
let wildcard = '';
|
||||
let inEscape = false;
|
||||
let inPattern = false;
|
||||
|
||||
for (let i = 0; i < timePattern.length; i++) {
|
||||
const ch = timePattern.charAt(i);
|
||||
switch (ch) {
|
||||
case '[':
|
||||
inPattern = false;
|
||||
if (!inEscape) {
|
||||
inEscape = true;
|
||||
} else {
|
||||
wildcard += ch;
|
||||
}
|
||||
break;
|
||||
case ']':
|
||||
if (inEscape) {
|
||||
inEscape = false;
|
||||
} else if (!inPattern) {
|
||||
wildcard += ch;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (inEscape) {
|
||||
wildcard += ch;
|
||||
} else if (!inPattern) {
|
||||
wildcard += '*';
|
||||
inPattern = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wildcard;
|
||||
}
|
|
@ -19,6 +19,7 @@ import uiMixin from '../ui';
|
|||
import { uiSettingsMixin } from '../ui';
|
||||
import optimizeMixin from '../optimize';
|
||||
import pluginsInitializeMixin from './plugins/initialize';
|
||||
import { indexPatternsMixin } from './index_patterns';
|
||||
import { savedObjectsMixin } from './saved_objects';
|
||||
|
||||
const rootDir = fromRoot('.');
|
||||
|
@ -57,6 +58,7 @@ module.exports = class KbnServer {
|
|||
|
||||
// setup this.uiExports and this.bundles
|
||||
uiMixin,
|
||||
indexPatternsMixin,
|
||||
|
||||
// setup saved object routes
|
||||
savedObjectsMixin,
|
||||
|
|
|
@ -12,27 +12,26 @@ export default function (Private) {
|
|||
const fieldFormats = Private(RegistryFieldFormatsProvider);
|
||||
const flattenHit = Private(IndexPatternsFlattenHitProvider);
|
||||
const FieldList = Private(IndexPatternsFieldListProvider);
|
||||
const IndexPattern = Private(IndexPatternProvider);
|
||||
|
||||
function StubIndexPattern(pattern, timeField, fields) {
|
||||
this.id = pattern;
|
||||
this.popularizeField = sinon.spy();
|
||||
this.popularizeField = sinon.stub();
|
||||
this.timeFieldName = timeField;
|
||||
this.getNonScriptedFields = sinon.spy();
|
||||
this.getScriptedFields = sinon.spy();
|
||||
this.getSourceFiltering = sinon.spy();
|
||||
this.getNonScriptedFields = sinon.spy(IndexPattern.prototype.getNonScriptedFields);
|
||||
this.getScriptedFields = sinon.spy(IndexPattern.prototype.getScriptedFields);
|
||||
this.getSourceFiltering = sinon.stub();
|
||||
this.metaFields = ['_id', '_type', '_source'];
|
||||
this.fieldFormatMap = {};
|
||||
this.routes = IndexPatternProvider.routes;
|
||||
|
||||
this.toIndexList = _.constant(Promise.resolve([pattern]));
|
||||
this.toDetailedIndexList = _.constant(Promise.resolve([
|
||||
{
|
||||
index: pattern,
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
]));
|
||||
this.getComputedFields = _.bind(getComputedFields, this);
|
||||
this.toIndexList = _.constant(Promise.resolve(pattern.split(',')));
|
||||
this.toDetailedIndexList = _.constant(Promise.resolve(pattern.split(',').map(index => ({
|
||||
index,
|
||||
min: 0,
|
||||
max: 1
|
||||
}))));
|
||||
this.getComputedFields = getComputedFields.bind(this);
|
||||
this.flattenHit = flattenHit(this);
|
||||
this.formatHit = formatHit(this, fieldFormats.getDefaultInstance('string'));
|
||||
this.formatField = this.formatHit.formatField;
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { IndexPatternsMapperProvider } from 'ui/index_patterns/_mapper';
|
||||
import stubbedLogstashFields from 'fixtures/logstash_fields';
|
||||
import sinon from 'sinon';
|
||||
|
||||
export function stubMapper(Private, mockLogstashFields = Private(stubbedLogstashFields)) {
|
||||
const stubbedMapper = Private(IndexPatternsMapperProvider);
|
||||
|
||||
sinon.stub(stubbedMapper, 'getFieldsForIndexPattern', function () {
|
||||
return Promise.resolve(mockLogstashFields.filter(field => field.scripted === false));
|
||||
});
|
||||
|
||||
sinon.stub(stubbedMapper, 'clearCache', function () {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return stubbedMapper;
|
||||
}
|
|
@ -6,8 +6,8 @@ import BluebirdPromise from 'bluebird';
|
|||
import { SavedObjectProvider } from '../saved_object/saved_object';
|
||||
import { IndexPatternProvider } from 'ui/index_patterns/_index_pattern';
|
||||
import { AdminDocSourceProvider } from '../data_source/admin_doc_source';
|
||||
import { stubMapper } from 'test_utils/stub_mapper';
|
||||
|
||||
import { StubIndexPatternsApiClientModule } from '../../index_patterns/__tests__/stub_index_patterns_api_client';
|
||||
|
||||
describe('Saved Object', function () {
|
||||
require('test_utils/no_digest_promises').activateForSuite();
|
||||
|
@ -86,7 +86,9 @@ describe('Saved Object', function () {
|
|||
return savedObject.init();
|
||||
}
|
||||
|
||||
beforeEach(ngMock.module('kibana',
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
StubIndexPatternsApiClientModule,
|
||||
// Use the native window.confirm instead of our specialized version to make testing
|
||||
// this easier.
|
||||
function ($provide) {
|
||||
|
@ -104,7 +106,6 @@ describe('Saved Object', function () {
|
|||
window = $window;
|
||||
|
||||
mockEsService();
|
||||
stubMapper(Private);
|
||||
}));
|
||||
|
||||
describe('save', function () {
|
||||
|
|
|
@ -186,11 +186,8 @@ export function SegmentedRequestProvider(es, Private, Promise, timefilter, confi
|
|||
|
||||
return indexPattern.toDetailedIndexList(timeBounds.min, timeBounds.max, this._direction)
|
||||
.then(queue => {
|
||||
if (!_.isArray(queue)) queue = [queue];
|
||||
|
||||
this._queue = queue;
|
||||
this._queueCreated = true;
|
||||
|
||||
return queue;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('Validate index name directive', function () {
|
|||
'foo',
|
||||
'foo.bar',
|
||||
'[foo-]YYYY-MM-DD',
|
||||
'foo:bar',
|
||||
];
|
||||
|
||||
const wildcardPatterns = [
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class="kuiLink documentTableRow__action"
|
||||
data-test-subj="docTableRowAction"
|
||||
ng-href="{{ getContextAppHref() }}"
|
||||
ng-if="indexPattern.hasTimeField()"
|
||||
ng-if="indexPattern.isTimeBased()"
|
||||
>
|
||||
View surrounding documents
|
||||
</a>
|
||||
|
|
|
@ -17,7 +17,6 @@ describe('Filter Bar Directive', function () {
|
|||
let mapFilter;
|
||||
let $el;
|
||||
let $scope;
|
||||
// require('test_utils/no_digest_promises').activateForSuite();
|
||||
|
||||
beforeEach(ngMock.module('kibana/global_state', function ($provide) {
|
||||
$provide.service('getAppState', _.constant(_.constant(
|
||||
|
|
|
@ -12,14 +12,17 @@ import UtilsMappingSetupProvider from 'ui/utils/mapping_setup';
|
|||
import { IndexPatternsIntervalsProvider } from 'ui/index_patterns/_intervals';
|
||||
import { IndexPatternProvider } from 'ui/index_patterns/_index_pattern';
|
||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
||||
import { stubMapper } from 'test_utils/stub_mapper';
|
||||
import { IndexPatternsCalculateIndicesProvider } from 'ui/index_patterns/_calculate_indices';
|
||||
|
||||
import { FieldsFetcherProvider } from '../fields_fetcher_provider';
|
||||
import { StubIndexPatternsApiClientModule } from './stub_index_patterns_api_client';
|
||||
import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider';
|
||||
import { IndexPatternsCalculateIndicesProvider } from '../_calculate_indices';
|
||||
|
||||
describe('index pattern', function () {
|
||||
NoDigestPromises.activateForSuite();
|
||||
|
||||
let IndexPattern;
|
||||
let mapper;
|
||||
let fieldsFetcher;
|
||||
let mappingSetup;
|
||||
let mockLogstashFields;
|
||||
let DocSource;
|
||||
|
@ -28,16 +31,31 @@ describe('index pattern', function () {
|
|||
let indexPattern;
|
||||
let calculateIndices;
|
||||
let intervals;
|
||||
let indexPatternsApiClient;
|
||||
let defaultTimeField;
|
||||
|
||||
beforeEach(ngMock.module('kibana', StubIndexPatternsApiClientModule, (PrivateProvider) => {
|
||||
PrivateProvider.swap(IndexPatternsCalculateIndicesProvider, () => {
|
||||
// stub calculateIndices
|
||||
calculateIndices = sinon.spy(function () {
|
||||
return Promise.resolve([
|
||||
{ index: 'foo', max: Infinity, min: -Infinity },
|
||||
{ index: 'bar', max: Infinity, min: -Infinity }
|
||||
]);
|
||||
});
|
||||
|
||||
return calculateIndices;
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
mockLogstashFields = Private(FixturesLogstashFieldsProvider);
|
||||
defaultTimeField = mockLogstashFields.find(f => f.type === 'date');
|
||||
docSourceResponse = Private(FixturesStubbedDocSourceResponseProvider);
|
||||
|
||||
DocSource = Private(AdminDocSourceProvider);
|
||||
sinon.stub(DocSource.prototype, 'doIndex');
|
||||
sinon.stub(DocSource.prototype, 'fetch');
|
||||
mapper = stubMapper(Private, mockLogstashFields);
|
||||
|
||||
// stub mappingSetup
|
||||
mappingSetup = Private(UtilsMappingSetupProvider);
|
||||
|
@ -45,15 +63,6 @@ describe('index pattern', function () {
|
|||
return Promise.resolve(true);
|
||||
});
|
||||
|
||||
// stub calculateIndices
|
||||
calculateIndices = sinon.spy(function () {
|
||||
return Promise.resolve([
|
||||
{ index: 'foo', max: Infinity, min: -Infinity },
|
||||
{ index: 'bar', max: Infinity, min: -Infinity }
|
||||
]);
|
||||
});
|
||||
Private.stub(IndexPatternsCalculateIndicesProvider, calculateIndices);
|
||||
|
||||
// spy on intervals
|
||||
intervals = Private(IndexPatternsIntervalsProvider);
|
||||
sinon.stub(intervals, 'toIndexList').returns([
|
||||
|
@ -62,6 +71,8 @@ describe('index pattern', function () {
|
|||
]);
|
||||
|
||||
IndexPattern = Private(IndexPatternProvider);
|
||||
fieldsFetcher = Private(FieldsFetcherProvider);
|
||||
indexPatternsApiClient = Private(IndexPatternsApiClientProvider);
|
||||
}));
|
||||
|
||||
// create an indexPattern instance for each test
|
||||
|
@ -141,81 +152,34 @@ describe('index pattern', function () {
|
|||
});
|
||||
|
||||
describe('refresh fields', function () {
|
||||
// override the default indexPattern, with a truncated field list
|
||||
const indexPatternId = 'test-pattern';
|
||||
let indexPattern;
|
||||
let customFields;
|
||||
it('should fetch fields from the fieldsFetcher', async function () {
|
||||
expect(indexPattern.fields.length).to.be.greaterThan(2);
|
||||
|
||||
beforeEach(function () {
|
||||
customFields = [{
|
||||
analyzed: true,
|
||||
count: 30,
|
||||
filterable: true,
|
||||
indexed: true,
|
||||
name: 'foo',
|
||||
scripted: false,
|
||||
sortable: true,
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: false,
|
||||
readFromDocValues: true
|
||||
},
|
||||
{
|
||||
name: 'script number',
|
||||
type: 'number',
|
||||
scripted: true,
|
||||
script: '1234',
|
||||
lang: 'expression'
|
||||
}];
|
||||
sinon.spy(fieldsFetcher, 'fetch');
|
||||
indexPatternsApiClient.swapStubNonScriptedFields([
|
||||
{ name: 'foo' },
|
||||
{ name: 'bar' }
|
||||
]);
|
||||
|
||||
return create(indexPatternId, {
|
||||
_source: {
|
||||
customFormats: '{}',
|
||||
fields: JSON.stringify(customFields)
|
||||
}
|
||||
}).then(function (pattern) {
|
||||
indexPattern = pattern;
|
||||
});
|
||||
await indexPattern.refreshFields();
|
||||
sinon.assert.calledOnce(fieldsFetcher.fetch);
|
||||
|
||||
const newFields = indexPattern.getNonScriptedFields();
|
||||
expect(newFields).to.have.length(2);
|
||||
expect(newFields.map(f => f.name)).to.eql(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should fetch fields from the doc source', function () {
|
||||
// ensure that we don't have all the fields
|
||||
expect(customFields.length).to.not.equal(mockLogstashFields.length);
|
||||
expect(indexPattern.fields).to.have.length(customFields.length);
|
||||
|
||||
// ensure that all fields will be included in the returned docSource
|
||||
setDocsourcePayload(docSourceResponse(indexPatternId));
|
||||
|
||||
return Promise.all([
|
||||
// read fields from elasticsearch
|
||||
mapper.getFieldsForIndexPattern(),
|
||||
|
||||
// tell the index pattern to do the same
|
||||
indexPattern.refreshFields(),
|
||||
])
|
||||
.then(function (data) {
|
||||
const expected = data[0]; // just the fields in the index
|
||||
const fields = indexPattern.getNonScriptedFields(); // get all but scripted fields
|
||||
|
||||
expect(_.pluck(fields, 'name')).to.eql(_.pluck(expected, 'name'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve the scripted fields', function () {
|
||||
// ensure that all fields will be included in the returned docSource
|
||||
setDocsourcePayload(docSourceResponse(indexPatternId));
|
||||
|
||||
it('should preserve the scripted fields', async function () {
|
||||
// add spy to indexPattern.getScriptedFields
|
||||
const scriptedFieldsSpy = sinon.spy(indexPattern, 'getScriptedFields');
|
||||
sinon.spy(indexPattern, 'getScriptedFields');
|
||||
|
||||
// refresh fields, which will fetch
|
||||
return indexPattern.refreshFields().then(function () {
|
||||
// called to append scripted fields to the response from mapper.getFieldsForIndexPattern
|
||||
expect(scriptedFieldsSpy.callCount).to.equal(1);
|
||||
await indexPattern.refreshFields();
|
||||
|
||||
const expected = _.filter(indexPattern.fields, { scripted: true });
|
||||
expect(_.pluck(expected, 'name')).to.eql(['script number']);
|
||||
});
|
||||
// called to append scripted fields to the response from mapper.getFieldsForIndexPattern
|
||||
sinon.assert.calledOnce(indexPattern.getScriptedFields);
|
||||
expect(indexPattern.getScriptedFields().map(f => f.name))
|
||||
.to.eql(mockLogstashFields.filter(f => f.scripted).map(f => f.name));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -316,35 +280,35 @@ describe('index pattern', function () {
|
|||
beforeEach(function () {
|
||||
interval = 'result:getInterval';
|
||||
sinon.stub(indexPattern, 'getInterval').returns(interval);
|
||||
sinon.stub(indexPattern, 'isTimeBasedInterval').returns(true);
|
||||
});
|
||||
|
||||
it('invokes interval toDetailedIndexList with given start/stop times', async function () {
|
||||
await indexPattern.toDetailedIndexList(1, 2);
|
||||
const id = indexPattern.id;
|
||||
expect(intervals.toIndexList.calledWith(id, interval, 1, 2)).to.be(true);
|
||||
sinon.assert.calledWith(intervals.toIndexList, id, interval, 1, 2);
|
||||
});
|
||||
|
||||
it('is fulfilled by the result of interval toDetailedIndexList', async function () {
|
||||
const indexList = await indexPattern.toDetailedIndexList();
|
||||
expect(indexList[0].index).to.equal('foo');
|
||||
expect(indexList[1].index).to.equal('bar');
|
||||
expect(indexList.map(i => i.index)).to.eql(['foo', 'bar']);
|
||||
});
|
||||
|
||||
describe('with sort order', function () {
|
||||
it('passes the sort order to the intervals module', function () {
|
||||
return indexPattern.toDetailedIndexList(1, 2, 'SORT_DIRECTION')
|
||||
.then(function () {
|
||||
expect(intervals.toIndexList.callCount).to.be(1);
|
||||
expect(intervals.toIndexList.getCall(0).args[4]).to.be('SORT_DIRECTION');
|
||||
});
|
||||
it('passes the sort order to the intervals module', async function () {
|
||||
await indexPattern.toDetailedIndexList(1, 2, 'SORT_DIRECTION');
|
||||
sinon.assert.calledOnce(intervals.toIndexList);
|
||||
expect(intervals.toIndexList.getCall(0).args[4]).to.be('SORT_DIRECTION');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when index pattern is a time-base wildcard', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(indexPattern, 'getInterval').returns(false);
|
||||
sinon.stub(indexPattern, 'hasTimeField').returns(true);
|
||||
sinon.stub(indexPattern, 'isWildcard').returns(true);
|
||||
indexPattern.id = 'logstash-*';
|
||||
indexPattern.timeFieldName = defaultTimeField.name;
|
||||
indexPattern.intervalName = null;
|
||||
indexPattern.notExpandable = false;
|
||||
});
|
||||
|
||||
it('invokes calculateIndices with given start/stop times and sortOrder', async function () {
|
||||
|
@ -363,26 +327,29 @@ describe('index pattern', function () {
|
|||
|
||||
describe('when index pattern is a time-base wildcard that is configured not to expand', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(indexPattern, 'getInterval').returns(false);
|
||||
sinon.stub(indexPattern, 'hasTimeField').returns(true);
|
||||
sinon.stub(indexPattern, 'isWildcard').returns(true);
|
||||
sinon.stub(indexPattern, 'canExpandIndices').returns(false);
|
||||
indexPattern.id = 'logstash-*';
|
||||
indexPattern.timeFieldName = defaultTimeField.name;
|
||||
indexPattern.intervalName = null;
|
||||
indexPattern.notExpandable = true;
|
||||
});
|
||||
|
||||
it('is fulfilled by id', async function () {
|
||||
const indexList = await indexPattern.toDetailedIndexList();
|
||||
expect(indexList.index).to.equal(indexPattern.id);
|
||||
expect(indexList.map(i => i.index)).to.eql([indexPattern.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when index pattern is neither an interval nor a time-based wildcard', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(indexPattern, 'getInterval').returns(false);
|
||||
indexPattern.id = 'logstash-0';
|
||||
indexPattern.timeFieldName = null;
|
||||
indexPattern.intervalName = null;
|
||||
indexPattern.notExpandable = true;
|
||||
});
|
||||
|
||||
it('is fulfilled by id', async function () {
|
||||
const indexList = await indexPattern.toDetailedIndexList();
|
||||
expect(indexList.index).to.equal(indexPattern.id);
|
||||
expect(indexList.map(i => i.index)).to.eql([indexPattern.id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -392,15 +359,19 @@ describe('index pattern', function () {
|
|||
|
||||
let interval;
|
||||
beforeEach(function () {
|
||||
interval = 'result:getInterval';
|
||||
sinon.stub(indexPattern, 'getInterval').returns(interval);
|
||||
indexPattern.id = '[logstash-]YYYY';
|
||||
indexPattern.timeFieldName = defaultTimeField.name;
|
||||
interval = intervals.byName.years;
|
||||
indexPattern.intervalName = interval.name;
|
||||
indexPattern.notExpandable = true;
|
||||
});
|
||||
|
||||
it('invokes interval toIndexList with given start/stop times', async function () {
|
||||
await indexPattern.toIndexList(1, 2);
|
||||
const id = indexPattern.id;
|
||||
expect(intervals.toIndexList.calledWith(id, interval, 1, 2)).to.be(true);
|
||||
sinon.assert.calledWith(intervals.toIndexList, id, interval, 1, 2);
|
||||
});
|
||||
|
||||
it('is fulfilled by the result of interval toIndexList', async function () {
|
||||
const indexList = await indexPattern.toIndexList();
|
||||
expect(indexList[0]).to.equal('foo');
|
||||
|
@ -420,9 +391,10 @@ describe('index pattern', function () {
|
|||
|
||||
describe('when index pattern is a time-base wildcard', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(indexPattern, 'getInterval').returns(false);
|
||||
sinon.stub(indexPattern, 'hasTimeField').returns(true);
|
||||
sinon.stub(indexPattern, 'isWildcard').returns(true);
|
||||
indexPattern.id = 'logstash-*';
|
||||
indexPattern.timeFieldName = defaultTimeField.name;
|
||||
indexPattern.intervalName = null;
|
||||
indexPattern.notExpandable = false;
|
||||
});
|
||||
|
||||
it('invokes calculateIndices with given start/stop times and sortOrder', async function () {
|
||||
|
@ -441,46 +413,49 @@ describe('index pattern', function () {
|
|||
|
||||
describe('when index pattern is a time-base wildcard that is configured not to expand', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(indexPattern, 'getInterval').returns(false);
|
||||
sinon.stub(indexPattern, 'hasTimeField').returns(true);
|
||||
sinon.stub(indexPattern, 'isWildcard').returns(true);
|
||||
sinon.stub(indexPattern, 'canExpandIndices').returns(false);
|
||||
indexPattern.id = 'logstash-*';
|
||||
indexPattern.timeFieldName = defaultTimeField.name;
|
||||
indexPattern.intervalName = null;
|
||||
indexPattern.notExpandable = true;
|
||||
});
|
||||
|
||||
it('is fulfilled by id', async function () {
|
||||
it('is fulfilled using the id', async function () {
|
||||
const indexList = await indexPattern.toIndexList();
|
||||
expect(indexList).to.equal(indexPattern.id);
|
||||
expect(indexList).to.eql([indexPattern.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when index pattern is neither an interval nor a time-based wildcard', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(indexPattern, 'getInterval').returns(false);
|
||||
indexPattern.id = 'logstash-0';
|
||||
indexPattern.timeFieldName = null;
|
||||
indexPattern.intervalName = null;
|
||||
indexPattern.notExpandable = true;
|
||||
});
|
||||
|
||||
it('is fulfilled by id', async function () {
|
||||
const indexList = await indexPattern.toIndexList();
|
||||
expect(indexList).to.equal(indexPattern.id);
|
||||
expect(indexList).to.eql([indexPattern.id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#canExpandIndices()', function () {
|
||||
describe('#isIndexExpansionEnabled()', function () {
|
||||
it('returns true if notExpandable is false', function () {
|
||||
indexPattern.notExpandable = false;
|
||||
expect(indexPattern.canExpandIndices()).to.be(true);
|
||||
expect(indexPattern.isIndexExpansionEnabled()).to.be(true);
|
||||
});
|
||||
it('returns true if notExpandable is not defined', function () {
|
||||
delete indexPattern.notExpandable;
|
||||
expect(indexPattern.canExpandIndices()).to.be(true);
|
||||
expect(indexPattern.isIndexExpansionEnabled()).to.be(true);
|
||||
});
|
||||
it('returns false if notExpandable is true', function () {
|
||||
indexPattern.notExpandable = true;
|
||||
expect(indexPattern.canExpandIndices()).to.be(false);
|
||||
expect(indexPattern.isIndexExpansionEnabled()).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasTimeField()', function () {
|
||||
describe('#isTimeBased()', function () {
|
||||
beforeEach(function () {
|
||||
// for the sake of these tests, it doesn't much matter what type of field
|
||||
// this is so long as it exists
|
||||
|
@ -488,14 +463,18 @@ describe('index pattern', function () {
|
|||
});
|
||||
it('returns false if no time field', function () {
|
||||
delete indexPattern.timeFieldName;
|
||||
expect(indexPattern.hasTimeField()).to.be(false);
|
||||
expect(indexPattern.isTimeBased()).to.be(false);
|
||||
});
|
||||
it('returns false if time field does not actually exist in fields', function () {
|
||||
indexPattern.timeFieldName = 'does not exist';
|
||||
expect(indexPattern.hasTimeField()).to.be(false);
|
||||
expect(indexPattern.isTimeBased()).to.be(false);
|
||||
});
|
||||
it('returns true if fields are not loaded yet', () => {
|
||||
indexPattern.fields = null;
|
||||
expect(indexPattern.isTimeBased()).to.be(true);
|
||||
});
|
||||
it('returns true if valid time field is configured', function () {
|
||||
expect(indexPattern.hasTimeField()).to.be(true);
|
||||
expect(indexPattern.isTimeBased()).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { IndexPatternsMapFieldProvider } from 'ui/index_patterns/_map_field';
|
||||
describe('field mapping normalizer (mapField)', function () {
|
||||
|
||||
let fn;
|
||||
let fields;
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private, $injector, config) {
|
||||
config.set('metaFields', ['_id', '_timestamp']);
|
||||
fn = Private(IndexPatternsMapFieldProvider);
|
||||
fields = require('fixtures/field_mapping').test.mappings.testType;
|
||||
}));
|
||||
|
||||
it('should be a function', function () {
|
||||
expect(fn).to.be.a(Function);
|
||||
});
|
||||
|
||||
it('should return a modified copy of the object, not modify the original', function () {
|
||||
const pristine = _.cloneDeep(fields['foo.bar']);
|
||||
const mapped = fn(fields['foo.bar'], 'foo.bar');
|
||||
|
||||
expect(fields['foo.bar']).to.not.eql(mapped);
|
||||
expect(fields['foo.bar']).to.eql(pristine);
|
||||
});
|
||||
|
||||
it('should not consider _id indexed unless it is', function () {
|
||||
const mapped = fn(fields._id, '_id');
|
||||
expect(mapped.indexed).to.be(false);
|
||||
|
||||
const mapping = _.cloneDeep(fields._id);
|
||||
mapping.mapping._id.index = 'not_analyzed';
|
||||
const mapped2 = fn(mapping, '_id');
|
||||
expect(mapped2.indexed).to.be(true);
|
||||
});
|
||||
|
||||
it('should always consider _timestamp to be an indexed date', function () {
|
||||
const mapped = fn(fields._timestamp, '_timestamp');
|
||||
expect(mapped.indexed).to.be(true);
|
||||
expect(mapped.type).to.be('date');
|
||||
});
|
||||
|
||||
it('should treat falsy and no as false for index', function () {
|
||||
let mapped = fn(fields.index_no_field, 'index_no_field');
|
||||
expect(mapped.indexed).to.be(false);
|
||||
|
||||
fields.index_no_field.index = false;
|
||||
mapped = fn(fields.index_no_field, 'index_no_field');
|
||||
expect(mapped.indexed).to.be(false);
|
||||
});
|
||||
|
||||
it('should treat other values for index as true', function () {
|
||||
const mapped = fn(fields.not_analyzed_field, 'not_analyzed_field');
|
||||
expect(mapped.indexed).to.be(true);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
import expect from 'expect.js';
|
||||
import { IndexPatternsPatternToWildcardProvider } from 'ui/index_patterns/_pattern_to_wildcard';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
describe('Index pattern to wildcard', function () {
|
||||
let indexPatternToWildcard;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPatternToWildcard = Private(IndexPatternsPatternToWildcardProvider);
|
||||
}));
|
||||
|
||||
it('should be a function', function () {
|
||||
expect(indexPatternToWildcard).to.be.a(Function);
|
||||
});
|
||||
|
||||
it('should parse patterns with a single escaped sequence', function () {
|
||||
expect(indexPatternToWildcard('[foo-]YYYY')).to.equal('foo-*');
|
||||
});
|
||||
|
||||
it('should parse patterns with a multiple escaped sequences', function () {
|
||||
expect(indexPatternToWildcard('[foo-]YYYY[-bar]')).to.equal('foo-*-bar');
|
||||
expect(indexPatternToWildcard('[foo-]YYYY[-bar-]MM')).to.equal('foo-*-bar-*');
|
||||
});
|
||||
|
||||
it('should handle leading patterns', function () {
|
||||
expect(indexPatternToWildcard('YYYY[-foo]')).to.equal('*-foo');
|
||||
});
|
||||
|
||||
it('should ignore [ when inside an escape', function () {
|
||||
expect(indexPatternToWildcard('[f[oo-]YYYY')).to.equal('f[oo-*');
|
||||
});
|
||||
|
||||
// Not sure if this behavior is useful, but this is how the code works
|
||||
it('should add ] to the string when outside the pattern', function () {
|
||||
expect(indexPatternToWildcard('[foo-]]YYYY')).to.equal('foo-]*');
|
||||
});
|
||||
|
||||
it('should ignore ] when outside an escape', function () {
|
||||
expect(indexPatternToWildcard('[f]oo-]YYYY')).to.equal('f*');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,4 @@
|
|||
import './_index_pattern';
|
||||
import './_map_field';
|
||||
import './_pattern_to_wildcard';
|
||||
import './_get_computed_fields';
|
||||
import './_field_format';
|
||||
describe('Index Patterns', function () {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import MockLogstashFieldsProvider from 'fixtures/logstash_fields';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider';
|
||||
|
||||
// place in a ngMock.module() call to swap out the IndexPatternsApiClient
|
||||
export function StubIndexPatternsApiClientModule(PrivateProvider) {
|
||||
PrivateProvider.swap(
|
||||
IndexPatternsApiClientProvider,
|
||||
(Private, Promise) => {
|
||||
let nonScriptedFields = Private(MockLogstashFieldsProvider).filter(field => (
|
||||
field.scripted !== true
|
||||
));
|
||||
|
||||
class StubIndexPatternsApiClient {
|
||||
getFieldsForTimePattern = sinon.spy(() => Promise.resolve(nonScriptedFields));
|
||||
getFieldsForWildcard = sinon.spy(() => Promise.resolve(nonScriptedFields));
|
||||
testTimePattern = sinon.spy(() => Promise.resolve({
|
||||
all: [],
|
||||
matches: []
|
||||
}))
|
||||
|
||||
swapStubNonScriptedFields = (newNonScriptedFields) => {
|
||||
nonScriptedFields = newNonScriptedFields;
|
||||
}
|
||||
}
|
||||
|
||||
return new StubIndexPatternsApiClient();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { uniq, where, groupBy, mapValues, pluck } from 'lodash';
|
||||
|
||||
export class ConflictTracker {
|
||||
constructor() {
|
||||
this._history = [];
|
||||
}
|
||||
|
||||
trackField(name, type, index) {
|
||||
this._history.push({ name, type, index });
|
||||
}
|
||||
|
||||
describeConflict(name) {
|
||||
const fieldHistory = where(this._history, { name });
|
||||
const entriesByType = groupBy(fieldHistory, 'type');
|
||||
|
||||
return mapValues(entriesByType, (entries) => {
|
||||
const indices = uniq(pluck(entries, 'index'));
|
||||
|
||||
// keep the list short so we don't polute the .kibana index
|
||||
if (indices.length > 10) {
|
||||
const total = indices.length;
|
||||
indices.length = 9;
|
||||
indices.push(`... and ${total - indices.length} others`);
|
||||
}
|
||||
|
||||
return indices;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import chrome from 'ui/chrome';
|
||||
import _ from 'lodash';
|
||||
import { Notifier } from 'ui/notify/notifier';
|
||||
import { ShardFailure } from 'ui/errors';
|
||||
|
||||
export function EnhanceFieldsWithCapabilitiesProvider($http) {
|
||||
const notifier = new Notifier({
|
||||
location: 'Field Capabilities'
|
||||
});
|
||||
|
||||
return function (fields, indices) {
|
||||
return $http.get(chrome.addBasePath(`/api/kibana/${indices}/field_capabilities`))
|
||||
.then((res) => {
|
||||
if (_.get(res, 'data.shard_failure_response')) {
|
||||
notifier.warning(new ShardFailure(res.data.shard_failure_response));
|
||||
}
|
||||
|
||||
const stats = _.get(res, 'data.fields', {});
|
||||
|
||||
return _.map(fields, (field) => {
|
||||
if (field.type === 'geo_point' && !stats[field.name]) {
|
||||
// FIXME: remove once https://github.com/elastic/elasticsearch/issues/20707 is fixed
|
||||
return _.assign(field, {
|
||||
'searchable': true,
|
||||
'aggregatable': true
|
||||
});
|
||||
}
|
||||
|
||||
return _.assign(field, stats[field.name]);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,23 +1,25 @@
|
|||
import _ from 'lodash';
|
||||
import { SavedObjectNotFound, DuplicateField, IndexPatternMissingIndices } from 'ui/errors';
|
||||
import angular from 'angular';
|
||||
import { getComputedFields } from 'ui/index_patterns/_get_computed_fields';
|
||||
import { formatHit } from 'ui/index_patterns/_format_hit';
|
||||
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
|
||||
import { IndexPatternsGetIdsProvider } from 'ui/index_patterns/_get_ids';
|
||||
import { IndexPatternsMapperProvider } from 'ui/index_patterns/_mapper';
|
||||
import { IndexPatternsIntervalsProvider } from 'ui/index_patterns/_intervals';
|
||||
import { AdminDocSourceProvider } from 'ui/courier/data_source/admin_doc_source';
|
||||
import UtilsMappingSetupProvider from 'ui/utils/mapping_setup';
|
||||
import { IndexPatternsFieldListProvider } from 'ui/index_patterns/_field_list';
|
||||
import { IndexPatternsFlattenHitProvider } from 'ui/index_patterns/_flatten_hit';
|
||||
import { IndexPatternsCalculateIndicesProvider } from 'ui/index_patterns/_calculate_indices';
|
||||
import { IndexPatternsPatternCacheProvider } from 'ui/index_patterns/_pattern_cache';
|
||||
import { Notifier } from 'ui/notify';
|
||||
|
||||
export function IndexPatternProvider(Private, Notifier, config, kbnIndex, Promise, confirmModalPromise) {
|
||||
import { getComputedFields } from './_get_computed_fields';
|
||||
import { formatHit } from './_format_hit';
|
||||
import { IndexPatternsGetIdsProvider } from './_get_ids';
|
||||
import { IndexPatternsIntervalsProvider } from './_intervals';
|
||||
import { IndexPatternsFieldListProvider } from './_field_list';
|
||||
import { IndexPatternsFlattenHitProvider } from './_flatten_hit';
|
||||
import { IndexPatternsCalculateIndicesProvider } from './_calculate_indices';
|
||||
import { IndexPatternsPatternCacheProvider } from './_pattern_cache';
|
||||
import { FieldsFetcherProvider } from './fields_fetcher_provider';
|
||||
|
||||
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise) {
|
||||
const fieldformats = Private(RegistryFieldFormatsProvider);
|
||||
const getIds = Private(IndexPatternsGetIdsProvider);
|
||||
const mapper = Private(IndexPatternsMapperProvider);
|
||||
const fieldsFetcher = Private(FieldsFetcherProvider);
|
||||
const intervals = Private(IndexPatternsIntervalsProvider);
|
||||
const DocSource = Private(AdminDocSourceProvider);
|
||||
const mappingSetup = Private(UtilsMappingSetupProvider);
|
||||
|
@ -47,22 +49,22 @@ export function IndexPatternProvider(Private, Notifier, config, kbnIndex, Promis
|
|||
fieldFormatMap: {
|
||||
type: 'string',
|
||||
_serialize(map = {}) {
|
||||
const serialized = _.transform(map, serialize);
|
||||
const serialized = _.transform(map, serializeFieldFormatMap);
|
||||
return _.isEmpty(serialized) ? undefined : angular.toJson(serialized);
|
||||
},
|
||||
_deserialize(map = '{}') {
|
||||
return _.mapValues(angular.fromJson(map), deserialize);
|
||||
return _.mapValues(angular.fromJson(map), deserializeFieldFormatMap);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function serialize(flat, format, field) {
|
||||
function serializeFieldFormatMap(flat, format, field) {
|
||||
if (format) {
|
||||
flat[field] = format;
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(mapping) {
|
||||
function deserializeFieldFormatMap(mapping) {
|
||||
const FieldFormat = fieldformats.byId[mapping.id];
|
||||
return FieldFormat && new FieldFormat(mapping.params);
|
||||
}
|
||||
|
@ -156,8 +158,8 @@ export function IndexPatternProvider(Private, Notifier, config, kbnIndex, Promis
|
|||
}
|
||||
|
||||
function fetchFields(indexPattern) {
|
||||
return mapper
|
||||
.getFieldsForIndexPattern(indexPattern, { skipIndexPatternCache: true })
|
||||
return Promise.resolve()
|
||||
.then(() => fieldsFetcher.fetch(indexPattern))
|
||||
.then(fields => {
|
||||
const scripted = indexPattern.getScriptedFields();
|
||||
const all = fields.concat(scripted);
|
||||
|
@ -283,33 +285,47 @@ export function IndexPatternProvider(Private, Notifier, config, kbnIndex, Promis
|
|||
|
||||
toDetailedIndexList(start, stop, sortDirection) {
|
||||
return Promise.resolve().then(() => {
|
||||
const interval = this.getInterval();
|
||||
if (interval) {
|
||||
if (this.isTimeBasedInterval()) {
|
||||
return intervals.toIndexList(
|
||||
this.id, interval, start, stop, sortDirection
|
||||
this.id, this.getInterval(), start, stop, sortDirection
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isWildcard() && this.hasTimeField() && this.canExpandIndices()) {
|
||||
if (this.isTimeBasedWildcard() && this.isIndexExpansionEnabled()) {
|
||||
return calculateIndices(
|
||||
this.id, this.timeFieldName, start, stop, sortDirection
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
index: this.id,
|
||||
min: -Infinity,
|
||||
max: Infinity
|
||||
};
|
||||
return [
|
||||
{
|
||||
index: this.id,
|
||||
min: -Infinity,
|
||||
max: Infinity
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
canExpandIndices() {
|
||||
isIndexExpansionEnabled() {
|
||||
return !this.notExpandable;
|
||||
}
|
||||
|
||||
hasTimeField() {
|
||||
return !!(this.timeFieldName && this.fields.byName[this.timeFieldName]);
|
||||
isTimeBased() {
|
||||
return !!this.timeFieldName && (!this.fields || !!this.getTimeField());
|
||||
}
|
||||
|
||||
isTimeBasedInterval() {
|
||||
return this.isTimeBased() && !!this.getInterval();
|
||||
}
|
||||
|
||||
isTimeBasedWildcard() {
|
||||
return this.isTimeBased() && this.isWildcard();
|
||||
}
|
||||
|
||||
getTimeField() {
|
||||
if (!this.timeFieldName || !this.fields || !this.fields.byName) return;
|
||||
return this.fields.byName[this.timeFieldName];
|
||||
}
|
||||
|
||||
isWildcard() {
|
||||
|
@ -370,9 +386,7 @@ export function IndexPatternProvider(Private, Notifier, config, kbnIndex, Promis
|
|||
}
|
||||
|
||||
refreshFields() {
|
||||
return mapper
|
||||
.clearCache(this)
|
||||
.then(() => fetchFields(this))
|
||||
return fetchFields(this)
|
||||
.then(() => this.save())
|
||||
.catch((err) => {
|
||||
notify.error(err);
|
||||
|
@ -383,9 +397,10 @@ export function IndexPatternProvider(Private, Notifier, config, kbnIndex, Promis
|
|||
// but we do not want to potentially make any pages unusable
|
||||
// so do not rethrow the error here
|
||||
if (err instanceof IndexPatternMissingIndices) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
return Promise.reject(err);
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
export function IndexPatternsLocalCacheProvider() {
|
||||
function LocalCache(opts) {
|
||||
opts = opts || {};
|
||||
const _id = opts.id || function (o) { return '' + o; };
|
||||
let _cache = {};
|
||||
|
||||
this.get = function (obj) {
|
||||
const id = _id(obj);
|
||||
return _cache[id] ? JSON.parse(_cache[id]) : null;
|
||||
};
|
||||
|
||||
this.set = function (obj, val) {
|
||||
const id = _id(obj);
|
||||
const clean = !_cache.hasOwnProperty(id);
|
||||
_cache[id] = angular.toJson(val);
|
||||
return clean;
|
||||
};
|
||||
|
||||
this.clear = function (obj) {
|
||||
if (!obj) {
|
||||
_cache = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const id = _id(obj);
|
||||
delete _cache[id];
|
||||
};
|
||||
}
|
||||
|
||||
return LocalCache;
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { castEsToKbnFieldTypeName } from '../../../utils';
|
||||
|
||||
export function IndexPatternsMapFieldProvider(Private, config) {
|
||||
/**
|
||||
* Accepts a field object and its name, and tries to give it a mapping
|
||||
* @param {Object} field - the field mapping returned by elasticsearch
|
||||
* @param {String} type - name of the field
|
||||
* @return {Object} - the resulting field after overrides and tweaking
|
||||
*/
|
||||
return function mapField(field, name) {
|
||||
const keys = Object.keys(field.mapping);
|
||||
if (keys.length === 0 || (name[0] === '_') && !_.contains(config.get('metaFields'), name)) return;
|
||||
|
||||
// Override the mapping, even if elasticsearch says otherwise
|
||||
const mappingOverrides = {
|
||||
_source: { type: '_source' },
|
||||
_index: { type: 'string' },
|
||||
_type: { type: 'string' },
|
||||
_id: { type: 'string' },
|
||||
_timestamp: {
|
||||
type: 'date',
|
||||
indexed: true
|
||||
},
|
||||
_score: {
|
||||
type: 'number',
|
||||
indexed: false
|
||||
}
|
||||
};
|
||||
|
||||
const mapping = _.cloneDeep(field.mapping[keys.shift()]);
|
||||
|
||||
if (!mapping.index || mapping.index === 'no') {
|
||||
// elasticsearch responds with false sometimes and 'no' others
|
||||
mapping.indexed = false;
|
||||
} else {
|
||||
mapping.indexed = true;
|
||||
}
|
||||
|
||||
mapping.analyzed = mapping.index === 'analyzed' || mapping.type === 'text';
|
||||
|
||||
mapping.type = castEsToKbnFieldTypeName(mapping.type);
|
||||
|
||||
if (mappingOverrides[name]) {
|
||||
_.merge(mapping, mappingOverrides[name]);
|
||||
}
|
||||
|
||||
return mapping;
|
||||
};
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
import { IndexPatternMissingIndices } from 'ui/errors';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { EnhanceFieldsWithCapabilitiesProvider } from 'ui/index_patterns/_enhance_fields_with_capabilities';
|
||||
import { IndexPatternsTransformMappingIntoFieldsProvider } from 'ui/index_patterns/_transform_mapping_into_fields';
|
||||
import { IndexPatternsPatternToWildcardProvider } from 'ui/index_patterns/_pattern_to_wildcard';
|
||||
import { IndexPatternsLocalCacheProvider } from 'ui/index_patterns/_local_cache';
|
||||
|
||||
export function IndexPatternsMapperProvider(Private, Promise, es, esAdmin, config, kbnIndex) {
|
||||
const enhanceFieldsWithCapabilities = Private(EnhanceFieldsWithCapabilitiesProvider);
|
||||
const transformMappingIntoFields = Private(IndexPatternsTransformMappingIntoFieldsProvider);
|
||||
const patternToWildcard = Private(IndexPatternsPatternToWildcardProvider);
|
||||
|
||||
const LocalCache = Private(IndexPatternsLocalCacheProvider);
|
||||
|
||||
function Mapper() {
|
||||
|
||||
// Save a reference to mapper
|
||||
const self = this;
|
||||
|
||||
// proper-ish cache, keeps a clean copy of the object, only returns copies of it's copy
|
||||
const fieldCache = self.cache = new LocalCache();
|
||||
|
||||
/**
|
||||
* Gets an object containing all fields with their mappings
|
||||
* @param {dataSource} dataSource
|
||||
* @param {boolean} skipIndexPatternCache - should we ping the index-pattern objects
|
||||
* @returns {Promise}
|
||||
* @async
|
||||
*/
|
||||
self.getFieldsForIndexPattern = function (indexPattern, opts) {
|
||||
const id = indexPattern.id;
|
||||
|
||||
const cache = fieldCache.get(id);
|
||||
if (cache) return Promise.resolve(cache);
|
||||
|
||||
if (!opts.skipIndexPatternCache) {
|
||||
return esAdmin.get({
|
||||
index: kbnIndex,
|
||||
type: 'index-pattern',
|
||||
id: id,
|
||||
_sourceInclude: ['fields']
|
||||
})
|
||||
.then(function (resp) {
|
||||
if (resp.found && resp._source.fields) {
|
||||
fieldCache.set(id, JSON.parse(resp._source.fields));
|
||||
}
|
||||
return self.getFieldsForIndexPattern(indexPattern, { skipIndexPatternCache: true });
|
||||
});
|
||||
}
|
||||
|
||||
let indexList = id;
|
||||
let promise = Promise.resolve();
|
||||
if (indexPattern.intervalName) {
|
||||
promise = self.getIndicesForIndexPattern(indexPattern)
|
||||
.then(function (existing) {
|
||||
if (existing.matches.length === 0) throw new IndexPatternMissingIndices();
|
||||
indexList = existing.matches.slice(-config.get('indexPattern:fieldMapping:lookBack')); // Grab the most recent
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(function () {
|
||||
return es.indices.getFieldMapping({
|
||||
index: indexList,
|
||||
fields: '*',
|
||||
ignoreUnavailable: _.isArray(indexList),
|
||||
allowNoIndices: false,
|
||||
includeDefaults: true
|
||||
});
|
||||
})
|
||||
.catch(handleMissingIndexPattern)
|
||||
.then(transformMappingIntoFields)
|
||||
.then(fields => enhanceFieldsWithCapabilities(fields, indexList))
|
||||
.then(function (fields) {
|
||||
fieldCache.set(id, fields);
|
||||
return fieldCache.get(id);
|
||||
});
|
||||
};
|
||||
|
||||
self.getIndicesForIndexPattern = function (indexPattern) {
|
||||
return es.indices.getAlias({
|
||||
index: patternToWildcard(indexPattern.id)
|
||||
})
|
||||
.then(function (resp) {
|
||||
// let all = Object.keys(resp).sort();
|
||||
const all = _(resp)
|
||||
.map(function (index, key) {
|
||||
if (index.aliases) {
|
||||
return [Object.keys(index.aliases), key];
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
})
|
||||
.flattenDeep()
|
||||
.sort()
|
||||
.uniq(true)
|
||||
.value();
|
||||
|
||||
const matches = all.filter(function (existingIndex) {
|
||||
const parsed = moment(existingIndex, indexPattern.id);
|
||||
return existingIndex === parsed.format(indexPattern.id);
|
||||
});
|
||||
|
||||
return {
|
||||
all: all,
|
||||
matches: matches
|
||||
};
|
||||
})
|
||||
.catch(handleMissingIndexPattern);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears mapping caches from elasticsearch and from local object
|
||||
* @param {dataSource} dataSource
|
||||
* @returns {Promise}
|
||||
* @async
|
||||
*/
|
||||
self.clearCache = function (indexPattern) {
|
||||
fieldCache.clear(indexPattern);
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
function handleMissingIndexPattern(err) {
|
||||
if (err.status >= 400) {
|
||||
// transform specific error type
|
||||
return Promise.reject(new IndexPatternMissingIndices(err.message));
|
||||
} else {
|
||||
// rethrow all others
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return new Mapper();
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
export function IndexPatternsPatternToWildcardProvider() {
|
||||
return function (format) {
|
||||
let wildcard = '';
|
||||
let inEscape = false;
|
||||
let inPattern = false;
|
||||
|
||||
for (let i = 0; i < format.length; i++) {
|
||||
const ch = format.charAt(i);
|
||||
switch (ch) {
|
||||
case '[':
|
||||
inPattern = false;
|
||||
if (!inEscape) {
|
||||
inEscape = true;
|
||||
} else {
|
||||
wildcard += ch;
|
||||
}
|
||||
break;
|
||||
case ']':
|
||||
if (inEscape) {
|
||||
inEscape = false;
|
||||
} else if (!inPattern) {
|
||||
wildcard += ch;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (inEscape) {
|
||||
wildcard += ch;
|
||||
} else if (!inPattern) {
|
||||
wildcard += '*';
|
||||
inPattern = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wildcard;
|
||||
};
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { IndexPatternsMapFieldProvider } from 'ui/index_patterns/_map_field';
|
||||
import { ConflictTracker } from 'ui/index_patterns/_conflict_tracker';
|
||||
|
||||
export function IndexPatternsTransformMappingIntoFieldsProvider(Private, kbnIndex, config) {
|
||||
const mapField = Private(IndexPatternsMapFieldProvider);
|
||||
|
||||
/**
|
||||
* Convert the ES response into the simple map for fields to
|
||||
* mappings which we will cache
|
||||
*
|
||||
* @param {object} response - complex, excessively nested
|
||||
* object returned from ES
|
||||
* @return {object} - simple object that works for all of kibana
|
||||
* use-cases
|
||||
*/
|
||||
return function (response) {
|
||||
const fields = {};
|
||||
const conflictTracker = new ConflictTracker();
|
||||
|
||||
_.each(response, function (index, indexName) {
|
||||
if (indexName === kbnIndex) return;
|
||||
_.each(index.mappings, function (mappings) {
|
||||
_.each(mappings, function (field, name) {
|
||||
const keys = Object.keys(field.mapping);
|
||||
if (keys.length === 0 || (name[0] === '_') && !_.contains(config.get('metaFields'), name)) return;
|
||||
|
||||
const mapping = mapField(field, name);
|
||||
// track the name, type and index for every field encountered so that the source
|
||||
// of conflicts can be described later
|
||||
conflictTracker.trackField(name, mapping.type, indexName);
|
||||
|
||||
if (fields[name]) {
|
||||
if (fields[name].type !== mapping.type) {
|
||||
// conflict fields are not available for much except showing in the discover table
|
||||
// overwrite the entire mapping object to reset all fields
|
||||
fields[name] = { type: 'conflict' };
|
||||
}
|
||||
} else {
|
||||
fields[name] = _.pick(mapping, 'type', 'indexed', 'analyzed', 'doc_values');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
config.get('metaFields').forEach(function (meta) {
|
||||
if (fields[meta]) return;
|
||||
|
||||
const field = { mapping: {} };
|
||||
field.mapping[meta] = {};
|
||||
fields[meta] = mapField(field, meta);
|
||||
});
|
||||
|
||||
return _.map(fields, function (mapping, name) {
|
||||
mapping.name = name;
|
||||
|
||||
if (mapping.type === 'conflict') {
|
||||
mapping.conflictDescriptions = conflictTracker.describeConflict(name);
|
||||
}
|
||||
|
||||
return mapping;
|
||||
});
|
||||
};
|
||||
}
|
35
src/ui/public/index_patterns/fields_fetcher.js
Normal file
35
src/ui/public/index_patterns/fields_fetcher.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
export function createFieldsFetcher(apiClient, config) {
|
||||
class FieldsFetcher {
|
||||
fetch(indexPattern) {
|
||||
if (indexPattern.isTimeBasedInterval()) {
|
||||
const interval = indexPattern.getInterval().name;
|
||||
return this.fetchForTimePattern(indexPattern.id, interval);
|
||||
}
|
||||
|
||||
return this.fetchForWildcard(indexPattern.id);
|
||||
}
|
||||
|
||||
testTimePattern(indexPatternId) {
|
||||
return apiClient.testTimePattern({
|
||||
pattern: indexPatternId
|
||||
});
|
||||
}
|
||||
|
||||
fetchForTimePattern(indexPatternId) {
|
||||
return apiClient.getFieldsForTimePattern({
|
||||
pattern: indexPatternId,
|
||||
lookBack: config.get('indexPattern:fieldMapping:lookBack'),
|
||||
metaFields: config.get('metaFields'),
|
||||
});
|
||||
}
|
||||
|
||||
fetchForWildcard(indexPatternId) {
|
||||
return apiClient.getFieldsForWildcard({
|
||||
pattern: indexPatternId,
|
||||
metaFields: config.get('metaFields'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new FieldsFetcher();
|
||||
}
|
7
src/ui/public/index_patterns/fields_fetcher_provider.js
Normal file
7
src/ui/public/index_patterns/fields_fetcher_provider.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { createFieldsFetcher } from './fields_fetcher';
|
||||
import { IndexPatternsApiClientProvider } from './index_patterns_api_client_provider';
|
||||
|
||||
export function FieldsFetcherProvider(Private, config) {
|
||||
const apiClient = Private(IndexPatternsApiClientProvider);
|
||||
return createFieldsFetcher(apiClient, config);
|
||||
}
|
|
@ -4,12 +4,12 @@ import { IndexPatternProvider } from 'ui/index_patterns/_index_pattern';
|
|||
import { IndexPatternsPatternCacheProvider } from 'ui/index_patterns/_pattern_cache';
|
||||
import { IndexPatternsGetIdsProvider } from 'ui/index_patterns/_get_ids';
|
||||
import { IndexPatternsIntervalsProvider } from 'ui/index_patterns/_intervals';
|
||||
import { IndexPatternsMapperProvider } from 'ui/index_patterns/_mapper';
|
||||
import { IndexPatternsPatternToWildcardProvider } from 'ui/index_patterns/_pattern_to_wildcard';
|
||||
import { FieldsFetcherProvider } from './fields_fetcher_provider';
|
||||
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('kibana/index_patterns');
|
||||
|
||||
export { IndexPatternsApiClientProvider } from './index_patterns_api_client_provider';
|
||||
export function IndexPatternsProvider(esAdmin, Notifier, Private, Promise, kbnIndex) {
|
||||
const self = this;
|
||||
|
||||
|
@ -45,11 +45,9 @@ export function IndexPatternsProvider(esAdmin, Notifier, Private, Promise, kbnIn
|
|||
self.cache = patternCache;
|
||||
self.getIds = Private(IndexPatternsGetIdsProvider);
|
||||
self.intervals = Private(IndexPatternsIntervalsProvider);
|
||||
self.mapper = Private(IndexPatternsMapperProvider);
|
||||
self.patternToWildcard = Private(IndexPatternsPatternToWildcardProvider);
|
||||
self.fieldsFetcher = Private(FieldsFetcherProvider);
|
||||
self.fieldFormats = Private(RegistryFieldFormatsProvider);
|
||||
self.IndexPattern = IndexPattern;
|
||||
}
|
||||
|
||||
module.service('indexPatterns', Private => Private(IndexPatternsProvider));
|
||||
|
||||
|
|
101
src/ui/public/index_patterns/index_patterns_api_client.js
Normal file
101
src/ui/public/index_patterns/index_patterns_api_client.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { resolve as resolveUrl, format as formatUrl } from 'url';
|
||||
|
||||
import { pick, mapValues } from 'lodash';
|
||||
|
||||
import { IndexPatternMissingIndices } from 'ui/errors';
|
||||
import { Notifier } from 'ui/notify';
|
||||
|
||||
export function createIndexPatternsApiClient($http, basePath) {
|
||||
const apiBaseUrl = `${basePath}/api/index_patterns/`;
|
||||
const notify = new Notifier({ location: 'Index Patterns API' });
|
||||
|
||||
function join(...uriComponents) {
|
||||
return uriComponents.filter(Boolean).map(encodeURIComponent).join('/');
|
||||
}
|
||||
|
||||
function getUrl(path, query) {
|
||||
const noNullsQuery = pick(query, value => value != null);
|
||||
const noArraysQuery = mapValues(noNullsQuery, value => (
|
||||
Array.isArray(value) ? JSON.stringify(value) : value
|
||||
));
|
||||
|
||||
return resolveUrl(apiBaseUrl, formatUrl({
|
||||
pathname: join(...path),
|
||||
query: noArraysQuery,
|
||||
}));
|
||||
}
|
||||
|
||||
function request(method, url, body) {
|
||||
return $http({
|
||||
method,
|
||||
url,
|
||||
data: body,
|
||||
})
|
||||
.then(resp => resp.data)
|
||||
.catch((resp) => {
|
||||
// convert $http errors into actual error objects
|
||||
const respBody = resp.data;
|
||||
|
||||
if (resp.status === 404 && respBody.code === 'no_matching_indices') {
|
||||
throw new IndexPatternMissingIndices(respBody.message);
|
||||
}
|
||||
|
||||
const err = new Error(respBody.message || respBody.error || `${resp.status} Response`);
|
||||
err.status = resp.status;
|
||||
err.body = respBody;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
class IndexPatternsApiClient {
|
||||
getFieldsForTimePattern(options = {}) {
|
||||
const {
|
||||
pattern,
|
||||
lookBack,
|
||||
metaFields,
|
||||
} = options;
|
||||
|
||||
const url = getUrl(['_fields_for_time_pattern'], {
|
||||
pattern,
|
||||
look_back: lookBack,
|
||||
meta_fields: metaFields,
|
||||
});
|
||||
|
||||
return notify.event(`getFieldsForTimePattern(${pattern})`, () => (
|
||||
request('GET', url).then(resp => resp.fields)
|
||||
));
|
||||
}
|
||||
|
||||
getFieldsForWildcard(options = {}) {
|
||||
const {
|
||||
pattern,
|
||||
metaFields,
|
||||
} = options;
|
||||
|
||||
const url = getUrl(['_fields_for_wildcard'], {
|
||||
pattern,
|
||||
meta_fields: metaFields,
|
||||
});
|
||||
|
||||
return notify.event(`getFieldsForWildcard(${pattern})`, () => (
|
||||
request('GET', url).then(resp => resp.fields)
|
||||
));
|
||||
}
|
||||
|
||||
testTimePattern(options = {}) {
|
||||
const {
|
||||
pattern
|
||||
} = options;
|
||||
|
||||
const url = getUrl(['_test_time_pattern'], {
|
||||
pattern,
|
||||
});
|
||||
|
||||
return notify.event(`testTimePattern(${pattern})`, () => (
|
||||
request('GET', url)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new IndexPatternsApiClient();
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import chrome from 'ui/chrome';
|
||||
import { createIndexPatternsApiClient } from './index_patterns_api_client';
|
||||
|
||||
export function IndexPatternsApiClientProvider($http) {
|
||||
return createIndexPatternsApiClient($http, chrome.getBasePath());
|
||||
}
|
|
@ -5,6 +5,7 @@ import { metadata } from 'ui/metadata';
|
|||
|
||||
const module = uiModules.get('kibana/notify');
|
||||
export const notify = new Notifier();
|
||||
export { Notifier } from 'ui/notify/notifier';
|
||||
|
||||
module.factory('createNotifier', function () {
|
||||
return function (opts) {
|
||||
|
@ -59,4 +60,3 @@ if (window.addEventListener) {
|
|||
notifier.log(`Detected an unhandled Promise rejection.\n${e.reason}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -210,10 +210,7 @@ module.exports = function MapsRenderbotFactory(Private, $injector, serviceSettin
|
|||
return _.assign(
|
||||
{},
|
||||
this.vis.type.params.defaults,
|
||||
{
|
||||
type: this.vis.type.name,
|
||||
hasTimeField: this.vis.indexPattern && this.vis.indexPattern.hasTimeField()// Add attribute which determines whether an index is time based or not.
|
||||
},
|
||||
{ type: this.vis.type.name },
|
||||
this.vis.params
|
||||
);
|
||||
}
|
||||
|
|
|
@ -90,7 +90,6 @@ describe('Vislib _chart Test Suite', function () {
|
|||
type: 'histogram',
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
hasTimeField: true,
|
||||
zeroFill: true
|
||||
};
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ dataTypesArray.forEach(function (dataType) {
|
|||
let persistedState;
|
||||
const visLibParams = {
|
||||
type: 'histogram',
|
||||
hasTimeField: true,
|
||||
addLegend: true,
|
||||
addTooltip: true,
|
||||
mode: mode,
|
||||
|
|
|
@ -38,11 +38,7 @@ module.exports = function VislibRenderbotFactory(Private, $injector) {
|
|||
return _.assign(
|
||||
{},
|
||||
self.vis.type.params.defaults,
|
||||
{
|
||||
type: self.vis.type.name,
|
||||
// Add attribute which determines whether an index is time based or not.
|
||||
hasTimeField: self.vis.indexPattern && self.vis.indexPattern.hasTimeField()
|
||||
},
|
||||
{ type: self.vis.type.name },
|
||||
self.vis.params
|
||||
);
|
||||
};
|
||||
|
|
|
@ -62,6 +62,27 @@ module.exports = function (grunt) {
|
|||
]
|
||||
},
|
||||
|
||||
devApiTestServer: {
|
||||
options: {
|
||||
wait: false,
|
||||
ready: /Server running/,
|
||||
quiet: false,
|
||||
failOnError: false
|
||||
},
|
||||
cmd: binScript,
|
||||
args: [
|
||||
...stdDevArgs,
|
||||
'--dev',
|
||||
'--no-base-path',
|
||||
'--no-ssl',
|
||||
'--optimize.enabled=false',
|
||||
'--elasticsearch.url=' + format(esTestServerUrlParts),
|
||||
'--server.port=' + kibanaTestServerUrlParts.port,
|
||||
'--server.xsrf.disableProtection=true',
|
||||
...kbnServerFlags,
|
||||
]
|
||||
},
|
||||
|
||||
testUIServer: {
|
||||
options: {
|
||||
wait: false,
|
||||
|
|
|
@ -88,7 +88,7 @@ module.exports = function (grunt) {
|
|||
|
||||
grunt.registerTask('test:api:server', [
|
||||
'esvm:ui',
|
||||
'run:apiTestServer:keepalive'
|
||||
'run:devApiTestServer:keepalive'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test:api:runner', () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('apis', () => {
|
||||
loadTestFile(require.resolve('./index_patterns'));
|
||||
loadTestFile(require.resolve('./scripts'));
|
||||
loadTestFile(require.resolve('./search'));
|
||||
});
|
||||
|
|
101
test/api_integration/apis/index_patterns/es_errors/errors.js
Normal file
101
test/api_integration/apis/index_patterns/es_errors/errors.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import {
|
||||
isEsIndexNotFoundError,
|
||||
createNoMatchingIndicesError,
|
||||
isNoMatchingIndicesError,
|
||||
convertEsIndexNotFoundError
|
||||
} from '../../../../../src/server/index_patterns/service/lib/errors';
|
||||
|
||||
import {
|
||||
getIndexNotFoundError,
|
||||
getDocNotFoundError
|
||||
} from './lib';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('index_patterns/* error handler', () => {
|
||||
let indexNotFoundError;
|
||||
let docNotFoundError;
|
||||
before(async () => {
|
||||
await esArchiver.load('index_patterns/basic_index');
|
||||
indexNotFoundError = await getIndexNotFoundError(es);
|
||||
docNotFoundError = await getDocNotFoundError(es);
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('index_patterns/basic_index');
|
||||
});
|
||||
|
||||
describe('isEsIndexNotFoundError()', () => {
|
||||
it('identifies index not found errors', () => {
|
||||
if (!isEsIndexNotFoundError(indexNotFoundError)) {
|
||||
throw new Error(`Expected isEsIndexNotFoundError(indexNotFoundError) to be true`);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects doc not found errors', () => {
|
||||
if (isEsIndexNotFoundError(docNotFoundError)) {
|
||||
throw new Error(`Expected isEsIndexNotFoundError(docNotFoundError) to be true`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNoMatchingIndicesError()', () => {
|
||||
it('returns a boom error', () => {
|
||||
const error = createNoMatchingIndicesError();
|
||||
if (!error || !error.isBoom) {
|
||||
throw new Error(`expected ${error} to be a Boom error`);
|
||||
}
|
||||
});
|
||||
|
||||
it('sets output code to "no_matching_indices"', () => {
|
||||
const error = createNoMatchingIndicesError();
|
||||
expect(error.output.payload).to.have.property('code', 'no_matching_indices');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNoMatchingIndicesError()', () => {
|
||||
it('returns true for errors from createNoMatchingIndicesError()', () => {
|
||||
if (!isNoMatchingIndicesError(createNoMatchingIndicesError())) {
|
||||
throw new Error('Expected isNoMatchingIndicesError(createNoMatchingIndicesError()) to be true');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false for indexNotFoundError', () => {
|
||||
if (isNoMatchingIndicesError(indexNotFoundError)) {
|
||||
throw new Error('expected isNoMatchingIndicesError(indexNotFoundError) to be false');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false for docNotFoundError', async () => {
|
||||
if (isNoMatchingIndicesError(docNotFoundError)) {
|
||||
throw new Error('expected isNoMatchingIndicesError(docNotFoundError) to be false');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertEsIndexNotFoundError()', () => {
|
||||
const indices = ['foo', 'bar'];
|
||||
|
||||
it('converts indexNotFoundErrors into NoMatchingIndices errors', async () => {
|
||||
const converted = convertEsIndexNotFoundError(indices, indexNotFoundError);
|
||||
if (!isNoMatchingIndicesError(converted)) {
|
||||
throw new Error('expected convertEsIndexNotFoundError(indexNotFoundError) to return NoMatchingIndices error');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns other errors', async () => {
|
||||
const originals = [docNotFoundError, '', 1, /foo/, new Date(), new Error(), function () {}];
|
||||
|
||||
originals.forEach(orig => {
|
||||
const converted = convertEsIndexNotFoundError(indices, orig);
|
||||
if (converted !== orig) {
|
||||
throw new Error(`expected convertEsIndexNotFoundError(${orig}) to return original error`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('index_patterns/service/lib', () => {
|
||||
loadTestFile(require.resolve('./errors'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export async function getIndexNotFoundError(es) {
|
||||
try {
|
||||
await es.indices.get({
|
||||
index: 'SHOULD NOT EXIST'
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.have.property('status', 404); // sanity check
|
||||
return err;
|
||||
}
|
||||
|
||||
throw new Error('Expected es.indices.get() call to fail');
|
||||
}
|
||||
|
||||
export async function getDocNotFoundError(es) {
|
||||
try {
|
||||
await es.get({
|
||||
index: 'basic_index',
|
||||
type: 'type',
|
||||
id: '1234'
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.have.property('status', 404); // sanity check
|
||||
return err;
|
||||
}
|
||||
|
||||
throw new Error('Expected es.get() call to fail');
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
getIndexNotFoundError,
|
||||
getDocNotFoundError,
|
||||
} from './get_es_errors';
|
|
@ -0,0 +1,15 @@
|
|||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('errors', () => {
|
||||
it('returns 404 when no indices match', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[not-really-an-index-]YYYY.MM.DD',
|
||||
look_back: 1
|
||||
})
|
||||
.expect(404)
|
||||
));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('index_patterns/_fields_for_time_pattern', () => {
|
||||
loadTestFile(require.resolve('./errors'));
|
||||
loadTestFile(require.resolve('./pattern'));
|
||||
loadTestFile(require.resolve('./query_params'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('pattern', () => {
|
||||
before(() => esArchiver.load('index_patterns/daily_index'));
|
||||
after(() => esArchiver.unload('index_patterns/daily_index'));
|
||||
|
||||
it('matches indices with compatible patterns', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[logs-]YYYY.MM.DD',
|
||||
look_back: 2,
|
||||
})
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'Jan01',
|
||||
type: 'boolean',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'Jan02',
|
||||
type: 'boolean',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('respects look_back parameter', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[logs-]YYYY.MM.DD',
|
||||
look_back: 1,
|
||||
})
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'Jan02',
|
||||
type: 'boolean',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('includes a field for each of `meta_fields` names', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[logs-]YYYY.MM.DD',
|
||||
look_back: 1,
|
||||
meta_fields: JSON.stringify(['meta1', 'meta2'])
|
||||
})
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'Jan02',
|
||||
type: 'boolean',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'meta1',
|
||||
type: 'string',
|
||||
aggregatable: false,
|
||||
searchable: false,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
{
|
||||
name: 'meta2',
|
||||
type: 'string',
|
||||
aggregatable: false,
|
||||
searchable: false,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('query params', () => {
|
||||
before(() => esArchiver.load('index_patterns/daily_index'));
|
||||
after(() => esArchiver.unload('index_patterns/daily_index'));
|
||||
|
||||
it('requires `pattern` and `look_back` query params', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({ pattern: null })
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body.validation).to.eql({
|
||||
keys: [
|
||||
'pattern',
|
||||
'look_back'
|
||||
],
|
||||
source: 'query'
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('supports `meta_fields` query param', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[logs-]YYYY.MM.DD',
|
||||
look_back: 1,
|
||||
meta_fields: JSON.stringify(['a'])
|
||||
})
|
||||
.expect(200)
|
||||
));
|
||||
|
||||
it('requires `look_back` to be a number', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[logs-]YYYY.MM.DD',
|
||||
look_back: 'foo',
|
||||
})
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body.message).to.contain('"look_back" must be a number');
|
||||
})
|
||||
));
|
||||
|
||||
it('requires `look_back` to be greater than one', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_time_pattern')
|
||||
.query({
|
||||
pattern: '[logs-]YYYY.MM.DD',
|
||||
look_back: 0,
|
||||
})
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body.message).to.contain('"look_back" must be larger than or equal to 1');
|
||||
})
|
||||
));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('conflicts', () => {
|
||||
before(() => esArchiver.load('index_patterns/conflicts'));
|
||||
after(() => esArchiver.unload('index_patterns/conflicts'));
|
||||
|
||||
it('flags fields with mismatched types as conflicting', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({ pattern: 'logs-*' })
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'success',
|
||||
type: 'conflict',
|
||||
aggregatable: false,
|
||||
searchable: false,
|
||||
readFromDocValues: false,
|
||||
conflictDescriptions: {
|
||||
boolean: [
|
||||
'logs-2017.01.02'
|
||||
],
|
||||
keyword: [
|
||||
'logs-2017.01.01'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('index_patterns/_fields_for_wildcard route', () => {
|
||||
loadTestFile(require.resolve('./params'));
|
||||
loadTestFile(require.resolve('./conflicts'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
export default function ({ getService }) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const chance = getService('chance');
|
||||
|
||||
describe('params', () => {
|
||||
before(() => esArchiver.load('index_patterns/basic_index'));
|
||||
after(() => esArchiver.unload('index_patterns/basic_index'));
|
||||
|
||||
it('requires a pattern query param', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({})
|
||||
.expect(400)
|
||||
));
|
||||
|
||||
it('accepts a JSON formatted meta_fields query param', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({
|
||||
pattern: '*',
|
||||
meta_fields: JSON.stringify(['meta'])
|
||||
})
|
||||
.expect(200)
|
||||
));
|
||||
|
||||
it('rejects a comma-separated list of meta_fields', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({
|
||||
pattern: '*',
|
||||
meta_fields: 'foo,bar'
|
||||
})
|
||||
.expect(400)
|
||||
));
|
||||
|
||||
it('rejects unexpected query params', () => (
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({
|
||||
pattern: chance.word(),
|
||||
[chance.word()]: chance.word(),
|
||||
})
|
||||
.expect(400)
|
||||
));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import expect from 'expect.js';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
const ensureFieldsAreSorted = resp => {
|
||||
expect(resp.body.fields)
|
||||
.to.eql(sortBy(resp.body.fields, 'name'));
|
||||
};
|
||||
|
||||
describe('response', () => {
|
||||
before(() => esArchiver.load('index_patterns/basic_index'));
|
||||
after(() => esArchiver.unload('index_patterns/basic_index'));
|
||||
|
||||
it('returns a flattened version of the fields in es', () =>
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({ pattern: 'basic_index' })
|
||||
.expect(200, {
|
||||
fields: [
|
||||
{
|
||||
type: 'boolean',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'bar',
|
||||
readFromDocValues: true
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
name: 'baz',
|
||||
readFromDocValues: false
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'baz.keyword',
|
||||
readFromDocValues: true
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'foo',
|
||||
readFromDocValues: true
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(ensureFieldsAreSorted)
|
||||
);
|
||||
|
||||
it('always returns a field for all passed meta fields', () =>
|
||||
supertest
|
||||
.get('/api/index_patterns/_fields_for_wildcard')
|
||||
.query({
|
||||
pattern: 'basic_index',
|
||||
meta_fields: JSON.stringify([
|
||||
'_id',
|
||||
'_source',
|
||||
'crazy_meta_field'
|
||||
])
|
||||
})
|
||||
.expect(200, {
|
||||
fields: [
|
||||
{
|
||||
aggregatable: false,
|
||||
name: '_id',
|
||||
readFromDocValues: false,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
aggregatable: false,
|
||||
name: '_source',
|
||||
readFromDocValues: false,
|
||||
searchable: false,
|
||||
type: '_source',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'bar',
|
||||
readFromDocValues: true
|
||||
},
|
||||
{
|
||||
aggregatable: false,
|
||||
name: 'baz',
|
||||
readFromDocValues: false,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'baz.keyword',
|
||||
readFromDocValues: true
|
||||
},
|
||||
{
|
||||
aggregatable: false,
|
||||
name: 'crazy_meta_field',
|
||||
readFromDocValues: false,
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
name: 'foo',
|
||||
readFromDocValues: true
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(ensureFieldsAreSorted)
|
||||
);
|
||||
});
|
||||
}
|
8
test/api_integration/apis/index_patterns/index.js
Normal file
8
test/api_integration/apis/index_patterns/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('index_patterns', () => {
|
||||
loadTestFile(require.resolve('./es_errors'));
|
||||
loadTestFile(require.resolve('./fields_for_time_pattern_route'));
|
||||
loadTestFile(require.resolve('./fields_for_wildcard_route'));
|
||||
loadTestFile(require.resolve('./test_time_pattern_route'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
export default function ({ getService }) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const chance = getService('chance');
|
||||
|
||||
describe('index_patterns/_test_time_pattern', () => {
|
||||
before(() => esArchiver.load('index_patterns/time_based_indices'));
|
||||
after(() => esArchiver.unload('index_patterns/time_based_indices'));
|
||||
|
||||
it('returns all and matching fields for tested pattern', () =>
|
||||
supertest
|
||||
.get('/api/index_patterns/_test_time_pattern')
|
||||
.query({ pattern: '[yearly-]YYYY' })
|
||||
.expect(200, {
|
||||
all: [
|
||||
'yearly-2018',
|
||||
'yearly-2017',
|
||||
'yearly-2016',
|
||||
],
|
||||
matches: [
|
||||
'yearly-2018',
|
||||
'yearly-2017',
|
||||
'yearly-2016',
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
it('returns all and matching fields for tested pattern', () =>
|
||||
supertest
|
||||
.get('/api/index_patterns/_test_time_pattern')
|
||||
.query({ pattern: '[monthly-]YYYY.MM' })
|
||||
.expect(200, {
|
||||
all: [
|
||||
'monthly-2017.01',
|
||||
'monthly-invalid',
|
||||
],
|
||||
matches: [
|
||||
'monthly-2017.01',
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
it('returns an empty response if it does not match any indexes', () =>
|
||||
supertest
|
||||
.get('/api/index_patterns/_test_time_pattern')
|
||||
.query({ pattern: `[${chance.word({ length: 12 })}]-YYYY` })
|
||||
.expect(200, {
|
||||
all: [],
|
||||
matches: []
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
SupertestProvider,
|
||||
ChanceProvider,
|
||||
} from './services';
|
||||
|
||||
export default async function ({ readConfigFile }) {
|
||||
|
@ -13,6 +14,7 @@ export default async function ({ readConfigFile }) {
|
|||
es: commonConfig.get('services.es'),
|
||||
esArchiver: commonConfig.get('services.esArchiver'),
|
||||
supertest: SupertestProvider,
|
||||
chance: ChanceProvider,
|
||||
},
|
||||
servers: commonConfig.get('servers'),
|
||||
};
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "basic_index",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"baz": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"foo": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "logs-2017.01.01",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"success": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "logs-2017.01.02",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "logs-2017.01.01",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"Jan01": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "logs-2017.01.02",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"Jan02": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "yearly-2017",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "long"
|
||||
},
|
||||
"time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "yearly-2016",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "long"
|
||||
},
|
||||
"time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "yearly-2018",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "long"
|
||||
},
|
||||
"time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "monthly-invalid",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "long"
|
||||
},
|
||||
"time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "monthly-2017.01",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"type": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "long"
|
||||
},
|
||||
"time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
test/api_integration/services/chance.js
Normal file
10
test/api_integration/services/chance.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import Chance from 'chance';
|
||||
|
||||
export function ChanceProvider({ getService }) {
|
||||
const log = getService('log');
|
||||
|
||||
const seed = Date.now();
|
||||
log.debug('randomness seed: %j', seed);
|
||||
|
||||
return new Chance(seed);
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export { SupertestProvider } from './supertest';
|
||||
export { ChanceProvider } from './chance';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue