[5.x] [IndexPatterns] Support cross cluster search (#11114) (#12155)

* [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:
Spencer 2017-06-02 15:44:40 -07:00 committed by GitHub
parent 4edae39733
commit 60db93758b
98 changed files with 2692 additions and 938 deletions

View file

@ -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);

View file

@ -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);
});
});
});

View file

@ -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>

View file

@ -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)

View file

@ -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');
});
});

View file

@ -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;
};

View file

@ -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()
});

View file

@ -1,6 +0,0 @@
import Joi from 'joi';
import indexPatternSchema from './index_pattern_schema';
module.exports = Joi.object({
index_pattern: indexPatternSchema.required()
});

View file

@ -1,5 +0,0 @@
import { registerFieldCapabilities } from './register_field_capabilities';
export default function (server) {
registerFieldCapabilities(server);
}

View file

@ -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));
}
);
}
});
}

View file

@ -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 } ],

View file

@ -40,8 +40,7 @@ module.exports = function VislibFixtures(Private) {
defaultYExtents: false,
setYExtents: false,
yAxis: {},
type: 'histogram',
hasTimeField: true
type: 'histogram'
}));
};
};

View file

@ -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();

View file

@ -64,6 +64,7 @@ export function ConsoleReporterProvider({ getService }) {
onPending = test => {
log.write('-> ' + colors.pending(test.title));
log.indent(2);
}
onPass = test => {

View file

@ -0,0 +1 @@
export { indexPatternsMixin } from './mixin';

View 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));
}

View file

@ -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 }))
);
}
}
});

View file

@ -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 }))
);
}
}
});

View 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';

View 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,
}));
}
}
});

View file

@ -0,0 +1 @@
export { IndexPatternsService } from './index_patterns_service';

View 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);
}
}

View file

@ -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

View 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);
}
});
});
});

View file

@ -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'
]);
});
});
});
});

View file

@ -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}`);
}
});
});
});

View 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;
}

View 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);
}
}

View file

@ -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 }
]);
});
});
});

View file

@ -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'
]
}
}
]);
});
});
});
});

View file

@ -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
}
}
}
}

View file

@ -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');
}

View file

@ -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),
};
});
}

View file

@ -0,0 +1 @@
export { getFieldCapabilities } from './field_capabilities';

View file

@ -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;
}
}

View file

@ -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('_');
}

View file

@ -0,0 +1,3 @@
export { getFieldCapabilities } from './field_capabilities';
export { resolveTimePattern } from './resolve_time_pattern';
export { createNoMatchingIndicesError, isNoMatchingIndicesError } from './errors';

View file

@ -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),
};
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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;

View file

@ -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;
}

View file

@ -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 () {

View file

@ -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;
});
}

View file

@ -46,6 +46,7 @@ describe('Validate index name directive', function () {
'foo',
'foo.bar',
'[foo-]YYYY-MM-DD',
'foo:bar',
];
const wildcardPatterns = [

View file

@ -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>

View file

@ -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(

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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*');
});
});

View file

@ -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 () {

View file

@ -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();
}
);
}

View file

@ -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;
});
}
}

View file

@ -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]);
});
});
};
}

View file

@ -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;
});
}

View file

@ -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;
}

View file

@ -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;
};
}

View file

@ -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();
}

View file

@ -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;
};
}

View file

@ -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;
});
};
}

View 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();
}

View 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);
}

View file

@ -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));

View 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();
}

View file

@ -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());
}

View file

@ -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}`);
});
}

View file

@ -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
);
}

View file

@ -90,7 +90,6 @@ describe('Vislib _chart Test Suite', function () {
type: 'histogram',
addTooltip: true,
addLegend: true,
hasTimeField: true,
zeroFill: true
};

View file

@ -34,7 +34,6 @@ dataTypesArray.forEach(function (dataType) {
let persistedState;
const visLibParams = {
type: 'histogram',
hasTimeField: true,
addLegend: true,
addTooltip: true,
mode: mode,

View file

@ -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
);
};

View file

@ -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,

View file

@ -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', () => {

View file

@ -1,5 +1,6 @@
export default function ({ loadTestFile }) {
describe('apis', () => {
loadTestFile(require.resolve('./index_patterns'));
loadTestFile(require.resolve('./scripts'));
loadTestFile(require.resolve('./search'));
});

View 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`);
}
});
});
});
});
}

View file

@ -0,0 +1,5 @@
export default function ({ loadTestFile }) {
describe('index_patterns/service/lib', () => {
loadTestFile(require.resolve('./errors'));
});
}

View file

@ -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');
}

View file

@ -0,0 +1,4 @@
export {
getIndexNotFoundError,
getDocNotFoundError,
} from './get_es_errors';

View file

@ -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)
));
});
}

View file

@ -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'));
});
}

View file

@ -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,
},
]
});
})
));
});
}

View file

@ -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');
})
));
});
}

View file

@ -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'
]
}
}
]
});
})
));
});
}

View file

@ -0,0 +1,6 @@
export default function ({ loadTestFile }) {
describe('index_patterns/_fields_for_wildcard route', () => {
loadTestFile(require.resolve('./params'));
loadTestFile(require.resolve('./conflicts'));
});
}

View file

@ -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)
));
});
}

View file

@ -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)
);
});
}

View 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'));
});
}

View file

@ -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: []
})
);
});
}

View file

@ -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'),
};

View file

@ -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"
}
}
}
}
}
}

View file

@ -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"
}
}
}
}
}
}

View file

@ -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"
}
}
}
}
}
}

View file

@ -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"
}
}
}
}
}
}

View 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);
}

View file

@ -1 +1,2 @@
export { SupertestProvider } from './supertest';
export { ChanceProvider } from './chance';