index_patterns now can have patterned index names

This commit is contained in:
Spencer Alger 2014-05-21 20:22:23 -07:00
parent 45b0ab75aa
commit f6e1bafa31
48 changed files with 1129 additions and 719 deletions

View file

@ -14,6 +14,7 @@ define(function (require) {
require('directives/fixed_scroll');
require('filters/moment');
require('courier/courier');
require('index_patterns/index_patterns');
require('state_management/app_state');
require('services/timefilter');
@ -22,7 +23,8 @@ define(function (require) {
var app = require('modules').get('app/discover', [
'kibana/services',
'kibana/notify',
'kibana/courier'
'kibana/courier',
'kibana/index_patterns'
]);
require('routes')
@ -34,9 +36,9 @@ define(function (require) {
return savedSearches.get($route.current.params.id)
.catch(courier.redirectWhenMissing('/discover'));
},
indexPatternList: function (courier) {
return courier.indexPatterns.getIds()
.then(courier.indexPatterns.ensureSome());
indexPatternList: function (indexPatterns) {
return indexPatterns.getIds()
.then(indexPatterns.ensureSome());
}
}
});
@ -102,9 +104,9 @@ define(function (require) {
$scope.fields = null;
var init = _.once(function () {
return setFields()
return updateDataSource()
.then(function () {
updateDataSource();
setFields();
// state fields that shouldn't trigger a fetch when changed
var ignoreStateChanges = ['columns'];
@ -131,6 +133,10 @@ define(function (require) {
if (!angular.equals(sort, currentSort)) $scope.fetch();
});
$scope.$watch('opts.timefield', function (timefield) {
timefilter.enabled(!!timefield);
});
searchSource.onError().then(function searchError(err) {
console.log(err);
notify.error('An error occured with your request. Reset your inputs and try again.');
@ -164,27 +170,29 @@ define(function (require) {
});
$scope.opts.saveDataSource = function () {
updateDataSource();
savedSearch.id = savedSearch.title;
savedSearch.save()
return updateDataSource()
.then(function () {
notify.info('Saved Data Source "' + savedSearch.title + '"');
if (savedSearch.id !== $route.current.params.id) {
$location.url(globalState.writeToUrl('/discover/' + savedSearch.id));
}
}, notify.error);
savedSearch.id = savedSearch.title;
return savedSearch.save()
.then(function () {
notify.info('Saved Data Source "' + savedSearch.title + '"');
if (savedSearch.id !== $route.current.params.id) {
$location.url(globalState.writeToUrl('/discover/' + savedSearch.id));
}
});
})
.catch(notify.error);
};
$scope.fetch = function () {
setupVisualization().then(function () {
if ($scope.opts.timefield) timefilter.enabled(true);
updateDataSource();
setupVisualization()
.then(updateDataSource)
.then(function () {
$state.commit();
courier.fetch();
}, notify.error);
})
.catch(notify.error);
};
$scope.toggleConfig = function () {
@ -233,16 +241,29 @@ define(function (require) {
}
});
if ($scope.opts.index !== searchSource.get('index')) {
// set the index on the savedSearch
searchSource.index($scope.opts.index);
$state.index = $scope.opts.index;
delete $scope.fields;
delete $scope.columns;
setFields();
// get the current indexPattern
var indexPattern = searchSource.get('index');
// if indexPattern exists, but $scope.opts.index doesn't, or the opposite, or if indexPattern's id
// is not equal to the $scope.opts.index then either clean or
if (
Boolean($scope.opts.index) !== Boolean(indexPattern)
|| (indexPattern && indexPattern.id) !== $scope.opts.index
) {
$state.index = $scope.opts.index = $scope.opts.index || config.get('defaultIndex');
indexPattern = courier.indexPatterns.get($scope.opts.index);
}
return Promise.cast(indexPattern)
.then(function (indexPattern) {
if (indexPattern !== searchSource.get('index')) {
// set the index on the savedSearch
searchSource.index(indexPattern);
delete $scope.fields;
delete $scope.columns;
setFields();
}
});
}
// This is a hacky optimization for comparing the contents of a large array to a short one.
@ -255,46 +276,44 @@ define(function (require) {
}
function setFields() {
return courier.getFieldsFor($scope.opts.index)
.then(function (rawFields) {
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
display: field.display
};
}, {});
var indexPattern = searchSource.get('index');
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
display: field.display
};
}, {});
if (!rawFields) return;
if (!indexPattern) return;
var columnObjects = arrayToKeys($scope.state.columns);
var columnObjects = arrayToKeys($scope.state.columns);
$scope.fields = [];
$scope.fieldsByName = {};
$scope.formatsByName = {};
$scope.state.columns = $scope.state.columns || [];
$scope.fields = [];
$scope.fieldsByName = {};
$scope.formatsByName = {};
$scope.state.columns = $scope.state.columns || [];
// Inject source into list;
$scope.fields.push({name: '_source', type: 'source', display: false});
// Inject source into list;
$scope.fields.push({name: '_source', type: 'source', display: false});
_.sortBy(rawFields, 'name').forEach(function (field) {
_.defaults(field, currentState[field.name]);
// clone the field and add it's display prop
var clone = _.assign({}, field, { display: columnObjects[name] || false });
$scope.fields.push(clone);
$scope.fieldsByName[field.name] = clone;
$scope.formatsByName[field.name] = field.format;
});
_.sortBy(indexPattern.fields, 'name').forEach(function (field) {
_.defaults(field, currentState[field.name]);
// clone the field and add it's display prop
var clone = _.assign({}, field, { display: columnObjects[name] || false });
$scope.fields.push(clone);
$scope.fieldsByName[field.name] = clone;
$scope.formatsByName[field.name] = field.format;
});
// TODO: timefield should be associated with the index pattern, this is a hack
// to pick the first date field and use it.
var timefields = _.find($scope.fields, {type: 'date'});
if (!!timefields) {
$scope.opts.timefield = timefields.name;
} else {
delete $scope.opts.timefield;
}
// TODO: timefield should be associated with the index pattern, this is a hack
// to pick the first date field and use it.
var timefields = _.find($scope.fields, {type: 'date'});
if (!!timefields) {
$scope.opts.timefield = timefields.name;
} else {
delete $scope.opts.timefield;
}
refreshColumns();
}, notify.error);
refreshColumns();
}
// TODO: On array fields, negating does not negate the combination, rather all terms
@ -366,46 +385,59 @@ define(function (require) {
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};
var loadingVis;
var setupVisualization = function () {
return new Promise(function (resolve, reject) {
// we shouldn't have a vis, delete it
if (!$scope.opts.timefield && $scope.vis) delete $scope.vis;
// we shouldn't have one, or already do, return whatever we already have
if (!$scope.opts.timefield || $scope.vis) return resolve($scope.vis);
if (loadingVis) return loadingVis;
// set the scopes vis property to the AdhocVis so that we know not to re-init
$scope.vis = new AdhocVis({
searchSource: searchSource,
type: 'histogram',
listeners: {
onClick: function (e) {
console.log(e);
}
},
config: {
metric: {
configs: [{
agg: 'count',
}]
},
segment: {
configs: [{
agg: 'date_histogram',
field: $scope.opts.timefield,
min_doc_count: 0,
}]
},
group: { configs: [] },
split: { configs: [] },
// we shouldn't have a vis, delete it
if (!$scope.opts.timefield && $scope.vis) delete $scope.vis;
// we shouldn't have one, or already do, return whatever we already have
if (!$scope.opts.timefield || $scope.vis) return Promise.resolve($scope.vis);
var vis = new AdhocVis({
searchSource: searchSource,
type: 'histogram',
listeners: {
onClick: function (e) {
console.log(e);
}
});
// once the visualization is ready, resolve the promise with the vis
$scope.$on('ready:vis', function () {
// enable the source, but wait for the visualization to be ready before running
resolve($scope.vis);
});
},
config: {
metric: {
configs: [{
agg: 'count',
}]
},
segment: {
configs: [{
agg: 'date_histogram',
field: $scope.opts.timefield,
min_doc_count: 0,
}]
},
group: { configs: [] },
split: { configs: [] },
}
});
// stash this promise so that other calls to setupVisualization will have to wait
loadingVis = vis.init()
.then(function () {
// expose the vis so that the visualize directive can get started
$scope.vis = vis;
// wait for visualize directive to emit that it's ready before resolving
return new Promise(function (resolve) {
$scope.$on('ready:vis', resolve);
});
})
.then(function () {
// clear the loading flag
loadingVis = null;
return vis;
});
return loadingVis;
};
init();

View file

@ -14,6 +14,7 @@ define(function (require) {
restrict: 'E',
template: html,
replace: true,
require: '^discFieldChooser',
link: function ($scope, $elem) {
var detailsElem;
var detailScope = $scope.$new();
@ -24,6 +25,16 @@ define(function (require) {
}
};
$scope.toggleDisplay = function (field) {
// inheritted param to fieldChooser
$scope.toggle(field.name);
// we are now displaying the field, kill it's details
if (field.details) {
$scope.toggleDetails(field);
}
};
$scope.toggleDetails = function (field, recompute) {
if (_.isUndefined(field.details) || recompute) {
// This is inherited from fieldChooser

View file

@ -5,7 +5,7 @@
<div class="discover-field-details">
<a ng-click="toggle(field.name)" bo-class="displayButton(field)" class="pull-right btn btn-default btn-xs discover-field-toggle">
<a ng-click="toggleDisplay(field)" bo-class="displayButton(field)" class="pull-right btn btn-default btn-xs discover-field-toggle">
<i class="fa fa-table"></i>
</a>

View file

@ -9,7 +9,7 @@ define(function (require) {
'kibana/courier'
]);
module.factory('SavedSearch', function (configFile, courier, Promise) {
module.factory('SavedSearch', function (courier, indexPatterns) {
function SavedSearch(id) {
courier.SavedObject.call(this, {
type: 'search',
@ -33,7 +33,7 @@ define(function (require) {
afterESResp: function () {
var index = this.searchSource.get('index');
if (typeof index === 'string') {
this.searchSource.index(courier.indexPatterns.get(index));
this.searchSource.index(indexPatterns.get(index));
}
}
});

View file

@ -1,26 +1,141 @@
define(function (require) {
var _ = require('lodash');
var moment = require('moment');
var errors = require('errors');
require('routes').when('/settings/indices/', {
template: require('text!../../partials/indices/create.html')
});
require('modules').get('app/settings')
.controller('kbnSettingsIndicesCreate', function ($scope, courier, $location, Notifier, Private) {
.controller('kbnSettingsIndicesCreate', function ($scope, $location, Notifier, Private, indexPatterns, es) {
var notify = new Notifier();
var refreshKibanaIndex = Private(require('./_refresh_kibana_index'));
var MissingIndices = courier.indexPatterns.errors.MissingIndices;
var MissingIndices = errors.IndexPatternMissingIndices;
var intervals = indexPatterns.intervals;
$scope.create = function () {
// this and child scopes will write pattern vars here
var index = $scope.index = {
// set in updateDefaultIndexName watcher
name: null,
defaultName: null,
isTimeBased: false,
nameIsPattern: false,
sampleCount: 5,
nameIntervalOptions: intervals
};
index.nameInterval = _.find(index.nameIntervalOptions, { name: 'daily' });
index.timeField = _.find(index.nameIntervalOptions, { name: 'field' });
var updateSamples = function () {
index.samples = null;
index.existing = null;
index.patternErrors = [];
if (!index.nameInterval || !index.name) {
return;
}
// replace anything that is outside of brackets with a *
var wildcard = indexPatterns.patternToWildcard(index.name);
es.indices.getAliases({
index: wildcard
})
.then(function (resp) {
var all = Object.keys(resp);
var matches = all.filter(function (existingIndex) {
var parsed = moment(existingIndex, index.name);
return existingIndex === parsed.format(index.name);
});
if (all.length) {
index.existing = {
class: all.length === matches.length ? 'success' : 'warning',
all: all,
matches: matches,
matchPercent: Math.round((matches.length / all.length) * 100) + '%',
failures: _.difference(all, matches)
};
return;
}
index.patternErrors.push('Pattern does not match any existing indices');
var radius = Math.round(index.sampleCount / 2);
var samples = intervals.toIndexList(index.name, index.nameInterval, -radius, radius);
if (_.uniq(samples).length !== samples.length) {
index.patternErrors.push('Invalid pattern, interval does not create unique index names');
} else {
index.samples = samples;
}
})
.catch(notify.error);
};
$scope.refreshFieldList = function () {
index.dateFields = index.timeField = index.fetchFieldsError = null;
var useIndexList = index.isTimeBased && index.nameIsPattern;
// we don't have enough info to continue
if (!index.name) {
index.fetchFieldsError = 'Set an index name first';
return;
}
if (useIndexList && !index.nameInterval) {
index.fetchFieldsError = 'Select the interval at which your indices are populated.';
return;
}
indexPatterns.mapper.clearCache(index.name)
.then(function () {
var pattern = index.name;
if (useIndexList) {
// trick the mapper into thinking this is an indexPattern
pattern = {
id: index.name,
toIndexList: function (to, from) {
return intervals.toIndexList(index.name, index.nameInterval, to, from);
}
};
}
return indexPatterns.mapper.getFieldsForIndexPattern(pattern, true)
.catch(function (err) {
// TODO: we should probably display a message of some kind
if (err instanceof MissingIndices) return [];
throw err;
});
})
.then(function (fields) {
index.dateFields = fields.filter(function (field) {
return field.type === 'date';
});
}, notify.fatal);
};
$scope.createIndexPattern = function () {
// get an empty indexPattern to start
courier.indexPatterns.get()
indexPatterns.get()
.then(function (indexPattern) {
// set both the id and title to the index pattern
indexPattern.id = indexPattern.title = $scope.newIndexPattern;
// set both the id and title to the index index
indexPattern.id = indexPattern.title = index.name;
if (index.isTimeBased) {
indexPattern.timeFieldName = index.timeField.name;
if (index.nameIsPattern) {
indexPattern.intervalName = index.nameInterval.name;
}
}
// fetch the fields
return indexPattern.refreshFields()
.then(refreshKibanaIndex)
.then(function () {
courier.indexPatterns.cache.clear(indexPattern.id);
indexPatterns.cache.clear(indexPattern.id);
$location.url('/settings/indices/' + indexPattern.id);
});
@ -34,5 +149,30 @@ define(function (require) {
else notify.fatal(err);
});
};
var updateDefaultIndexName = function () {
var newDefault = index.nameIsPattern
? '[logstash-]YYYY.MM.DD'
: 'logstash-*';
if (index.name === index.defaultName) {
index.name = index.defaultName = newDefault;
} else {
index.defaultName = newDefault;
}
};
$scope.moreSamples = function (andUpdate) {
index.sampleCount += 5;
if (andUpdate) updateSamples();
};
$scope.$watch('index.name', updateSamples);
$scope.$watch('index.name', $scope.refreshFieldList);
$scope.$watch('index.isTimeBased', $scope.refreshFieldList);
$scope.$watch('index.nameIsPattern', updateDefaultIndexName);
$scope.$watch('index.nameIsPattern', $scope.refreshFieldList);
$scope.$watch('index.nameInterval', updateSamples);
$scope.$watch('index.nameInterval', $scope.refreshFieldList);
});
});

View file

@ -47,4 +47,4 @@ define(function (require) {
});
});
});

View file

@ -11,7 +11,7 @@
<tr>
<th>Name</th>
<th>Value</th>
<th>Actions <kbn-info info="'Edit or restore the default value.'"></kbn-info></th>
<th>Actions <kbn-info info="Edit or restore the default value."></kbn-info></th>
</tr>
</thead>
<tbody>

View file

@ -1,21 +1,124 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<div ng-controller="kbnSettingsIndicesCreate">
<div ng-controller="kbnSettingsIndicesCreate" class="kbn-settings-indices-create">
<div class="page-header">
<h1>Configure an index pattern</h1>
In order to use Kibana you must configure at least one index pattern. Index patterns are
used to identify the Elasticsearch index to run search and analytics against. They are also
used to configure fields.
</div>
<form name="form" role="form" class="well" ng-submit="create()">
<div class="form-group">
<label>Index Name <small>or pattern</small></label>
<input type="text" class="form-control" placeholder="Enter index name"
ng-model="newIndexPattern" required>
</div>
<button type="submit" class="btn btn-primary"
ng-disabled="form.$invalid">Create</button>
</form>
<div class="container">
<form name="form" role="form" class="well" ng-submit="createIndexPattern()">
<div class="form-group time-and-pattern">
<label>
<input
ng-model="index.isTimeBased"
type="checkbox">
Index contains time-based events
</label>
<br>
<label ng-if="index.isTimeBased">
<input ng-model="index.nameIsPattern" type="checkbox">
Use event times to create index names
</label>
</div>
<div class="form-group">
<label>
Index Name
or Pattern &mdash;
<small><a href="http://momentjs.com/docs/#/displaying/format/">docs</a></small>
</label>
<input
ng-model="index.name"
ng-attr-placeholder="{{index.defaultName}}"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 2500, 'blur': 0} }"
required
type="text"
class="form-control">
</div>
<section ng-if="index.isTimeBased && index.nameIsPattern">
<div class="form-group">
<label>
Index Pattern Interval&nbsp;
<kbn-info info="The interval at which index names rotate."></kbn-info>
</label>
<select
required
ng-options="opt.display for opt in index.nameIntervalOptions"
ng-model="index.nameInterval"
class="form-control">
</select>
</div>
<div class="alert alert-danger" ng-repeat="err in index.patternErrors">{{err}}</div>
<div class="alert alert-info" ng-if="index.samples">
Sample Index Names
<ul>
<li ng-repeat="sample in index.samples">{{sample}}</li>
</ul>
<a ng-click="moreSamples(true)" class="alert-link">more</a>
</div>
<div class="alert alert-{{index.existing.class}}" ng-if="index.existing">
Pattern matches {{index.existing.matchPercent}} of similar indices
<ul>
<li ng-repeat="match in index.existing.matches | limitTo: index.sampleCount">{{match}}</li>
</ul>
<a
ng-if="index.sampleCount < index.existing.matches.length"
ng-click="moreSamples()"
class="alert-link">
more
</a>
</div>
<div class="alert alert-danger" ng-if="index.existing.failures.length">
Indices that did not match:
<ul>
<li ng-repeat="match in index.existing.failures | limitTo: index.sampleCount">{{match}}</li>
</ul>
<a
ng-if="index.sampleCount < index.existing.matches.length"
ng-click="moreSamples()"
class="alert-link">
more
</a>
</section>
</section>
<div class="form-group" ng-if="index.isTimeBased">
<label>
Time-Field Name
&nbsp;
<kbn-info info="This field will be use to filter events with the global time filter"></kbn-info>
&nbsp;
<small>
<a ng-click="refreshFieldList();"> refresh fields</a>
</small>
</label>
<select
required
ng-if="!index.fetchFieldsError"
ng-init="refreshFieldList();"
ng-options="field.name for field in index.dateFields"
ng-model="index.timeField"
class="form-control">
</select>
<select ng-if="index.fetchFieldsError" class="form-control" disabled >
<option>{{index.fetchFieldsError}}</option>
</select>
</div>
<button
ng-disabled="form.$invalid"
type="submit"
class="btn btn-success">
Create
</button>
</form>
</div>
</div>
</kbn-settings-indices>
</kbn-settings-app>

View file

@ -41,4 +41,8 @@ kbn-settings-advanced {
color: @btn-success-bg;
}
}
}
.kbn-settings-indices-create {
.time-and-pattern > div {}
}

View file

@ -53,7 +53,8 @@ define(function (require) {
vis.searchSource.onResults(function onResults(resp) {
courier.indexPatterns.get(vis.searchSource.get('index'))
.then(function (indexPattern) {
chart.render(vis.buildChartDataFromResponse(indexPattern, resp));
var chartData = vis.buildChartDataFromResponse(indexPattern, resp);
chart.render(chartData);
})
.catch(notify.fatal);
}).catch(notify.fatal);

View file

@ -17,7 +17,7 @@
name="custom interval">
</td>
<td ng-if="config.interval === 'customInterval'">
<kbn-info info="'interval bewteen timestamp measurements, in milliseconds'"></kbn-info>
<kbn-info info="interval bewteen timestamp measurements, in milliseconds"></kbn-info>
</td> -->
</tr>
</table>

View file

@ -10,7 +10,7 @@
<div class="form-group" ng-if="config.agg && config.agg !== 'count'">
<label for="field">
Field to {{config.agg}}&nbsp;
<kbn-info placement="right" info="'Field to use for the ' + metric.label"></kbn-info>
<kbn-info placement="right" info="Field to use for the {{metric.label}}"></kbn-info>
</label>
<select
class="form-control"

View file

@ -5,10 +5,9 @@ define(function (require) {
var configCats = require('./_config_categories');
module.factory('AdhocVis', function (courier, Private) {
module.factory('AdhocVis', function (courier, Private, Promise) {
var aggs = Private(require('./_aggs'));
/**
opts params:
{
@ -24,48 +23,66 @@ define(function (require) {
*/
function AdhocVis(opts) {
opts = opts || {};
if (!_.isObject(opts)) throw new TypeError('options must be an object');
var vis = this;
var params;
// Must get an object for this one
if (!_.isObject(opts)) return;
vis.init = _.once(function () {
vis.typeName = opts.type || 'histogram';
vis.params = _.cloneDeep(opts.params);
vis.typeName = opts.type || 'histogram';
vis.params = _.cloneDeep(opts.params);
vis.searchSource = opts.searchSource || courier.SavedObject.rootSearch();
// give vis the properties of config
_.assign(vis, opts.config);
// give this the properties of config
_.merge(vis, opts.config);
// also give it the on* interaction functions, if any
_.assign(vis, opts.listeners);
// also give it the on* interaction functions, if any
_.merge(vis, opts.listeners);
vis._fillConfigsToMinimum();
// TODO: Should we abtract out the agg building stuff?
vis.searchSource
// reads the vis' config and write the agg to the searchSource
.aggs(function () {
// stores the config objects in queryDsl
var dsl = {};
// counter to ensure unique agg names
var i = 0;
// start at the root, but the current will move
var current = dsl;
// resolve the search source for this AdhocVis
return Promise.cast((function () {
if (opts.searchSource) return opts.searchSource;
// continue to nest the aggs under each other
// writes to the dsl object
vis.getConfig().forEach(function (config) {
current.aggs = {};
var key = '_agg_' + (i++);
return courier.getRootSearch()
.then(function (rootSearch) {
var searchSource = courier.createSource('search');
searchSource.inherits(rootSearch);
return searchSource;
});
}()))
.then(function (searchSource) {
// TODO: Should we abtract out the agg building stuff?
searchSource.aggs(function () {
// stores the config objects in queryDsl
var dsl = {};
// counter to ensure unique agg names
var i = 0;
// start at the root, but the current will move
var current = dsl;
var aggDsl = {};
aggDsl[config.agg] = config.aggParams;
// continue to nest the aggs under each other
// writes to the dsl object
vis.getConfig().forEach(function (config) {
current.aggs = {};
var key = '_agg_' + (i++);
current = current.aggs[key] = aggDsl;
var aggDsl = {};
aggDsl[config.agg] = config.aggParams;
current = current.aggs[key] = aggDsl;
});
// set the dsl to the searchSource
return dsl.aggs || {};
});
// set the dsl to the searchSource
return dsl.aggs || {};
vis.searchSource = searchSource;
return vis;
});
});
// TODO: Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js
vis._fillConfigsToMinimum = function () {
@ -81,8 +98,6 @@ define(function (require) {
});
};
vis._fillConfigsToMinimum();
// Need these, but we have nothing to destroy for now;
vis.destroy = function () {};

View file

@ -21,7 +21,7 @@ define(function (require) {
}
var typeDef;
var rootSearch = opts.parentSearchSource || courier.SavedObject.rootSearch();
var defaultParent = opts.parentSearchSource;
courier.SavedObject.call(vis, {
type: 'visualization',
@ -74,17 +74,25 @@ define(function (require) {
return savedSearches.get(vis.savedSearchId);
}
if (relatedPattern) {
// create a new search source that inherits from the parent and uses the indexPattern
return Promise.resolve({
searchSource: rootSearch.extend().index(relatedPattern)
});
}
return courier.getRootSearch()
.then(function (rootSearch) {
if (relatedPattern) {
return courier.indexPatterns.get(relatedPattern)
.then(function (indexPattern) {
// create a new search source that inherits from the parent and uses the indexPattern
return {
searchSource: rootSearch.extend().index(indexPattern)
};
});
}
// default parent is the rootSearch, can be overridden (like in discover)
// but we mimic the searchSource prop from saved objects here
return {
searchSource: rootSearch
};
// default parent is the rootSearch, can be overridden (like in discover)
// but we mimic the searchSource prop from saved objects here
return Promise.resolve({
searchSource: rootSearch
});
}());
@ -122,11 +130,6 @@ define(function (require) {
vis._fillConfigsToMinimum();
// get and cache the field list
var index = vis.searchSource.get('index');
if (index) return courier.getFieldsFor(index);
})
.then(function () {
return vis;
});
}

View file

@ -1,163 +0,0 @@
define(function (require) {
var _ = require('lodash');
var inherits = require('utils/inherits');
var canStack = (function () {
var err = new Error();
return !!err.stack;
}());
return function () {
var errors = this;
// abstract error class
function CourierError(msg, constructor) {
this.message = msg;
Error.call(this, this.message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, constructor || CourierError);
} else if (canStack) {
this.stack = (new Error()).stack;
} else {
this.stack = '';
}
}
errors.CourierError = CourierError;
inherits(CourierError, Error);
/**
* HastyRefresh error class
* @param {String} [msg] - An error message that will probably end up in a log.
*/
errors.HastyRefresh = function HastyRefresh() {
CourierError.call(this,
'Courier attempted to start a query before the previous had finished.',
errors.HastyRefresh);
};
inherits(errors.HastyRefresh, CourierError);
/**
* Request Failure - When an entire mutli request fails
* @param {Error} err - the Error that came back
* @param {Object} resp - optional HTTP response
*/
errors.RequestFailure = function RequestFailure(err, resp) {
CourierError.call(this,
'Request to Elasticsearch failed: ' + JSON.stringify(resp || err.message),
errors.RequestFailure);
this.origError = err;
this.resp = resp;
};
inherits(errors.RequestFailure, CourierError);
/**
* FetchFailure Error - when there is an error getting a doc or search within
* a multi-response response body
* @param {String} [msg] - An error message that will probably end up in a log.
*/
errors.FetchFailure = function FetchFailure(resp) {
CourierError.call(this,
'Failed to get the doc: ' + JSON.stringify(resp),
errors.FetchFailure);
this.resp = resp;
};
inherits(errors.FetchFailure, CourierError);
/**
* A doc was re-indexed but it was out of date.
* @param {Object} resp - The response from es (one of the multi-response responses).
*/
errors.VersionConflict = function VersionConflict(resp) {
CourierError.call(this,
'Failed to store document changes do to a version conflict.',
errors.VersionConflict);
this.resp = resp;
};
inherits(errors.VersionConflict, CourierError);
/**
* there was a conflict storing a doc
* @param {String} field - the fields which contains the conflict
*/
errors.MappingConflict = function MappingConflict(field) {
CourierError.call(this,
'Field "' + field + '" is defined with at least two different types in indices matching the pattern',
errors.MappingConflict);
};
inherits(errors.MappingConflict, CourierError);
/**
* a field mapping was using a restricted fields name
* @param {String} field - the fields which contains the conflict
*/
errors.RestrictedMapping = function RestrictedMapping(field, index) {
var msg = field + ' is a restricted field name';
if (index) msg += ', found it while attempting to fetch mapping for index pattern: ' + index;
CourierError.call(this,
msg,
errors.RestrictedMapping);
};
inherits(errors.RestrictedMapping, CourierError);
/**
* a non-critical cache write to elasticseach failed
*/
errors.CacheWriteFailure = function CacheWriteFailure() {
CourierError.call(this,
'A Elasticsearch cache write has failed.',
errors.CacheWriteFailure);
};
inherits(errors.CacheWriteFailure, CourierError);
/**
* when a field mapping is requested for an unknown field
* @param {String} name - the field name
*/
errors.FieldNotFoundInCache = function FieldNotFoundInCache(name) {
CourierError.call(this,
'The ' + name + ' field was not found in the cached mappings',
errors.FieldNotFoundInCache);
};
inherits(errors.FieldNotFoundInCache, CourierError);
/**
* A saved object was not found
* @param {String} field - the fields which contains the conflict
*/
errors.SavedObjectNotFound = function SavedObjectNotFound(type) {
this.savedObjectType = type;
CourierError.call(this,
'Could not locate that ' + type,
errors.SavedObjectNotFound);
};
inherits(errors.SavedObjectNotFound, CourierError);
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
errors.IndexPatternMissingIndices = function IndexPatternMissingIndices(type) {
CourierError.call(this,
'IndexPattern\'s configured pattern does not match any indices',
errors.IndexPatternMissingIndices);
};
inherits(errors.IndexPatternMissingIndices, CourierError);
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
errors.NoDefinedIndexPatterns = function NoDefinedIndexPatterns(type) {
CourierError.call(this,
'Define at least one index pattern to continue',
errors.NoDefinedIndexPatterns);
};
inherits(errors.NoDefinedIndexPatterns, CourierError);
};
});

View file

@ -0,0 +1,33 @@
define(function (require) {
return function RootSearchFactory(Private, config, $rootScope, timefilter, indexPatterns, Promise) {
var SearchSource = Private(require('./data_source/search_source'));
var source; // the actual search source
var prom; // promise that must be resolved before the source is acurrate (updated by loadDefaultPattern)
var loadDefaultPattern = function () {
var defId = config.get('defaultIndex');
return prom = Promise.cast(defId && indexPatterns.get(defId)).then(function (pattern) {
source.set('index', pattern);
return source;
});
};
var init = function () {
source = new SearchSource();
source.filter(function (source) {
// dynamic time filter will be called in the _flatten phase of things
return timefilter.get(source.get('index'));
});
$rootScope.$on('change:config.defaultIndex', loadDefaultPattern);
$rootScope.$on('init:config', loadDefaultPattern);
return loadDefaultPattern();
};
return function () {
return prom || init();
};
};
});

View file

@ -1,6 +1,8 @@
define(function (require) {
return function RedirectWhenMissingFn($location, $route, globalState, Notifier, Private) {
var SavedObjectNotFound = Private(require('./_errors')).SavedObjectNotFound;
var errors = require('errors');
return function RedirectWhenMissingFn($location, $route, globalState, Notifier) {
var SavedObjectNotFound = errors.SavedObjectNotFound;
var notify = new Notifier();

View file

@ -1,9 +1,12 @@
define(function (require) {
var errors = require('errors');
require('services/es');
require('services/promises');
require('index_patterns/index_patterns');
require('modules').get('kibana/courier')
.service('courier', function ($rootScope, Private, Promise) {
.service('courier', function ($rootScope, Private, Promise, indexPatterns) {
function Courier() {
var courier = this;
@ -15,13 +18,13 @@ define(function (require) {
var docLooper = Private(require('./looper/doc'));
// expose some internal modules
courier.errors = Private(require('./_errors'));
courier.getRootSearch = Private(require('./_get_root_search'));
courier.SavedObject = Private(require('./saved_object/saved_object'));
courier.indexPatterns = Private(require('./index_patterns/index_patterns'));
courier.indexPatterns = indexPatterns;
courier.redirectWhenMissing = Private(require('./_redirect_when_missing'));
var HastyRefresh = courier.errors.HastyRefresh;
var Abort = courier.errors.Abort;
var HastyRefresh = errors.HastyRefresh;
var Abort = errors.Abort;
/**
* update the time between automatic search requests

View file

@ -9,6 +9,8 @@ define(function (require) {
var fetch = Private(require('../fetch/fetch'));
function SourceAbstract(initialState) {
this._instanceid = _.uniqueId('data_source');
this._state = (function () {
// state can be serialized as JSON, and passed back in to restore
if (initialState) {
@ -62,9 +64,10 @@ define(function (require) {
*/
SourceAbstract.prototype.set = function (state, val) {
if (typeof state === 'string') {
return this[state](val);
this[state](val);
} else {
this._state = state;
}
this._state = state;
return this;
};

View file

@ -1,8 +1,8 @@
define(function (require) {
var _ = require('lodash');
var errors = require('errors');
return function (Promise, Private, es) {
var errors = Private(require('../_errors'));
var pendingRequests = Private(require('../_pending_requests'));
/**

View file

@ -1,10 +1,10 @@
define(function (require) {
var _ = require('lodash');
var errors = require('errors');
var inherits = require('utils/inherits');
return function DocSourceFactory(Private, Promise, es) {
var errors = Private(require('../_errors'));
var sendToEs = Private(require('./_doc_send_to_es'));
var SourceAbstract = Private(require('./_abstract'));

View file

@ -1,9 +1,9 @@
define(function (require) {
var inherits = require('utils/inherits');
var _ = require('lodash');
var errors = require('errors');
return function SearchSourceFactory(Promise, Private) {
var errors = Private(require('../_errors'));
var SourceAbstract = Private(require('./_abstract'));
var FetchFailure = errors.FetchFailure;
@ -25,8 +25,6 @@ define(function (require) {
* @type {Array}
*/
SearchSource.prototype._methods = [
'index',
'indexInterval', // monthly, daily, etc
'type',
'query',
'filter',
@ -38,6 +36,16 @@ define(function (require) {
'source'
];
SearchSource.prototype.index = function (indexPattern) {
if (indexPattern === void 0) return this._state.index;
if (!indexPattern || typeof indexPattern.toIndexList !== 'function') {
throw new TypeError('expected indexPattern to be an IndexPattern duck.');
}
this._state.index = indexPattern;
return this;
};
SearchSource.prototype.extend = function () {
return (new SearchSource()).inherits(this);
};

View file

@ -14,7 +14,7 @@ define(function (require) {
var handlerCount = 0;
errorHandlers.splice(0).forEach(function (handler) {
if (handler.source !== req.source) return pendingRequests.push(handler);
if (handler.source !== req.source) return errorHandlers.push(handler);
handler.defer.resolve(error);
handlerCount++;
});

View file

@ -1,11 +1,11 @@
define(function (require) {
return function fetchService(Private, es, Promise, Notifier, $q) {
return function fetchService(Private, es, Promise, Notifier) {
var _ = require('lodash');
var errors = require('errors');
var docStrategy = Private(require('./strategy/doc'));
var searchStrategy = Private(require('./strategy/search'));
var errors = Private(require('../_errors'));
var RequestErrorHandler = Private(require('./_request_error_handler'));
var pendingRequests = Private(require('../_pending_requests'));
@ -16,7 +16,27 @@ define(function (require) {
var fetchThese = function (strategy, requests, reqErrHandler) {
var all, body;
all = requests.splice(0);
// dedupe requests
var uniqs = {};
all = requests.splice(0).filter(function (req) {
var iid = req.source._instanceid;
if (!uniqs[iid]) {
// this request is unique so far
uniqs[iid] = req;
// keep the request
return true;
}
// the source was requested at least twice
var uniq = uniqs[iid];
if (uniq._merged) {
// already setup the multi-responder
uniq._merged.push(req);
} else {
// put all requests into this array and itterate them all
uniq._merged = [uniq, req];
}
});
return Promise.map(all, function (req) {
return req.source._flatten();
@ -31,18 +51,32 @@ define(function (require) {
body: body
})
.then(function (resp) {
strategy.getResponses(resp).forEach(function (resp) {
var req = all.shift();
var sendResponse = function (req, resp) {
if (resp.error) return reqErrHandler.handle(req, new errors.FetchFailure(resp));
else strategy.resolveRequest(req, resp);
};
strategy.getResponses(resp).forEach(function (resp) {
var req = all.shift();
if (!req._merged) sendResponse(req, resp);
else {
req._merged.forEach(function (mergedReq) {
sendResponse(mergedReq, _.cloneDeep(resp));
});
}
});
// pass the response along to the next promise
return resp;
})
.catch(function (err) {
all.forEach(function (req) {
var sendFailure = function (req) {
reqErrHandler.handle(req, err);
};
all.forEach(function (req) {
if (!req._merged) sendFailure(req);
else req._merged.forEach(sendFailure);
});
throw err;
});

View file

@ -1,5 +1,5 @@
define(function (require) {
return function FetchStrategyForSearch(Private, Promise, Notifier) {
return function FetchStrategyForSearch(Private, Promise, Notifier, timefilter) {
var _ = require('lodash');
var notify = new Notifier();
@ -14,9 +14,13 @@ define(function (require) {
*/
requestStatesToBody: function (states) {
return states.map(function (state) {
var timeBounds = timefilter.getBounds();
var indexList = state.index.toIndexList(timeBounds.min, timeBounds.max);
return JSON.stringify({
index: state.index,
type: state.type
index: indexList,
type: state.type,
ignore_unavailable: true
})
+ '\n'
+ JSON.stringify(state.body);

View file

@ -1,83 +0,0 @@
define(function (require) {
return function IndexPatternFactory(Private) {
var inherits = require('utils/inherits');
var mapper = Private(require('./_mapper'));
var SavedObject = Private(require('../saved_object/saved_object'));
var fieldFormats = Private(require('./_field_formats'));
var patternCache = Private(require('./_pattern_cache'));
function IndexPattern(id) {
var pattern = this;
SavedObject.call(pattern, {
type: 'index-pattern',
id: id,
mapping: {
title: 'string',
timeFieldName: 'string',
customFormats: 'json',
fields: 'json'
},
defaults: {
title: id,
customFormats: {}
},
afterESResp: function () {
if (pattern.id) {
if (!pattern.fields) return pattern.fetchFields();
afterFieldsSet();
}
},
searchSource: false
});
function afterFieldsSet() {
pattern.fieldsByName = {};
pattern.fields.forEach(function (field) {
pattern.fieldsByName[field.name] = field;
// non-enumerable type so that it does not get included in the JSON
Object.defineProperty(field, 'format', {
enumerable: false,
get: function () {
var formatName = pattern.customFormats && pattern.customFormats[field.name];
return formatName ? fieldFormats.byName[formatName] : fieldFormats.defaultByType[field.type];
}
});
});
}
pattern.refreshFields = function () {
return mapper.clearCache(pattern.id)
.then(function () {
return pattern.fetchFields();
});
};
pattern.fetchFields = function () {
return mapper.getFieldsForIndexPattern(pattern.id, true)
.then(function (fields) {
pattern.fields = fields;
afterFieldsSet();
return pattern.save();
});
};
pattern.toJSON = function () {
return pattern.id;
};
pattern.toString = function () {
return '' + this.toJSON();
};
}
inherits(IndexPattern, SavedObject);
return IndexPattern;
};
});

View file

@ -1,8 +1,8 @@
define(function (require) {
return function SearchLooperService(Private, Promise) {
var errors = require('errors');
var fetch = Private(require('../fetch/fetch'));
var Looper = Private(require('./_looper'));
var errors = Private(require('../_errors'));
// track the currently executing search resquest
var _activeAutoSearch = null;

View file

@ -1,24 +0,0 @@
define(function (require) {
return function RootSearchFactory(Private, config, $rootScope, timefilter) {
return function makeRootSearch() {
var SearchSource = Private(require('../data_source/search_source'));
var indexPatterns = Private(require('../index_patterns/index_patterns'));
var source = (new SearchSource())
.index(config.get('defaultIndex'))
.filter(function (source) {
return indexPatterns.get(source.get('index'))
.then(function (indexPattern) {
// dynamic time filter will be called in the _flatten phase of things
return timefilter.get(indexPattern);
});
});
$rootScope.$on('change:config.defaultIndex', function () {
source.index(config.get('defaultIndex'));
});
return source;
};
};
});

View file

@ -1,22 +1,13 @@
define(function (require) {
return function SavedObjectFactory(configFile, Promise, Private, Notifier) {
var errors = require('errors');
var angular = require('angular');
var _ = require('lodash');
var DocSource = Private(require('../data_source/doc_source'));
var SearchSource = Private(require('../data_source/search_source'));
var errors = Private(require('../_errors'));
var mappingSetup = Private(require('./_mapping_setup'));
var json = {
_serialize: function (val) {
if (val != null) return angular.toJson(val);
},
_deserialize: function (val) {
if (val != null) return JSON.parse(val);
}
};
var mappingSetup = Private(require('utils/mapping_setup'));
var getRootSearch = Private(require('../_get_root_search'));
function SavedObject(config) {
if (!_.isObject(config)) config = {};
@ -39,19 +30,7 @@ define(function (require) {
});
// mapping definition for the fields that this object will expose
var mapping = _.mapValues(config.mapping || {}, function (val, prop) {
// allow shortcuts for the field types, by just setting the value
// to the type name
if (typeof val === 'string') val = { type: val };
if (val.type === 'json') {
val.type = 'string';
val._serialize = json._serialize;
val._deserialize = json._deserialize;
}
return val;
});
var mapping = mappingSetup.expandShorthand(config.mapping);
// default field values, assigned when the source is loaded
var defaults = config.defaults || {};
@ -83,12 +62,13 @@ define(function (require) {
.id(obj.id);
// by default, the search source should inherit from the rootSearch
if (obj.searchSource) {
obj.searchSource.inherits(SavedObject.rootSearch());
}
return Promise.cast(obj.searchSource && getRootSearch())
.then(function (rootSearch) {
if (rootSearch) obj.searchSource.inherits(rootSearch);
// check that the mapping for this type is defined
return mappingSetup.isDefined(type)
// check that the mapping for this type is defined
return mappingSetup.isDefined(type);
})
.then(function (defined) {
// if it is already defined skip this step
if (defined) return true;
@ -208,12 +188,6 @@ define(function (require) {
}
SavedObject.rootSearch = function () {
var rootSearch = Private(require('./_root_search'))();
SavedObject.rootSearch = function () { return rootSearch; };
return rootSearch;
};
return SavedObject;
};
});

View file

@ -0,0 +1,160 @@
define(function (require) {
var _ = require('lodash');
var inherits = require('utils/inherits');
var canStack = (function () {
var err = new Error();
return !!err.stack;
}());
var errors = {};
// abstract error class
function KbnError(msg, constructor) {
this.message = msg;
Error.call(this, this.message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, constructor || KbnError);
} else if (canStack) {
this.stack = (new Error()).stack;
} else {
this.stack = '';
}
}
errors.KbnError = KbnError;
inherits(KbnError, Error);
/**
* HastyRefresh error class
* @param {String} [msg] - An error message that will probably end up in a log.
*/
errors.HastyRefresh = function HastyRefresh() {
KbnError.call(this,
'Courier attempted to start a query before the previous had finished.',
errors.HastyRefresh);
};
inherits(errors.HastyRefresh, KbnError);
/**
* Request Failure - When an entire mutli request fails
* @param {Error} err - the Error that came back
* @param {Object} resp - optional HTTP response
*/
errors.RequestFailure = function RequestFailure(err, resp) {
KbnError.call(this,
'Request to Elasticsearch failed: ' + JSON.stringify(resp || err.message),
errors.RequestFailure);
this.origError = err;
this.resp = resp;
};
inherits(errors.RequestFailure, KbnError);
/**
* FetchFailure Error - when there is an error getting a doc or search within
* a multi-response response body
* @param {String} [msg] - An error message that will probably end up in a log.
*/
errors.FetchFailure = function FetchFailure(resp) {
KbnError.call(this,
'Failed to get the doc: ' + JSON.stringify(resp),
errors.FetchFailure);
this.resp = resp;
};
inherits(errors.FetchFailure, KbnError);
/**
* A doc was re-indexed but it was out of date.
* @param {Object} resp - The response from es (one of the multi-response responses).
*/
errors.VersionConflict = function VersionConflict(resp) {
KbnError.call(this,
'Failed to store document changes do to a version conflict.',
errors.VersionConflict);
this.resp = resp;
};
inherits(errors.VersionConflict, KbnError);
/**
* there was a conflict storing a doc
* @param {String} field - the fields which contains the conflict
*/
errors.MappingConflict = function MappingConflict(field) {
KbnError.call(this,
'Field "' + field + '" is defined with at least two different types in indices matching the pattern',
errors.MappingConflict);
};
inherits(errors.MappingConflict, KbnError);
/**
* a field mapping was using a restricted fields name
* @param {String} field - the fields which contains the conflict
*/
errors.RestrictedMapping = function RestrictedMapping(field, index) {
var msg = field + ' is a restricted field name';
if (index) msg += ', found it while attempting to fetch mapping for index pattern: ' + index;
KbnError.call(this, msg, errors.RestrictedMapping);
};
inherits(errors.RestrictedMapping, KbnError);
/**
* a non-critical cache write to elasticseach failed
*/
errors.CacheWriteFailure = function CacheWriteFailure() {
KbnError.call(this,
'A Elasticsearch cache write has failed.',
errors.CacheWriteFailure);
};
inherits(errors.CacheWriteFailure, KbnError);
/**
* when a field mapping is requested for an unknown field
* @param {String} name - the field name
*/
errors.FieldNotFoundInCache = function FieldNotFoundInCache(name) {
KbnError.call(this,
'The ' + name + ' field was not found in the cached mappings',
errors.FieldNotFoundInCache);
};
inherits(errors.FieldNotFoundInCache, KbnError);
/**
* A saved object was not found
* @param {String} field - the fields which contains the conflict
*/
errors.SavedObjectNotFound = function SavedObjectNotFound(type) {
this.savedObjectType = type;
KbnError.call(this,
'Could not locate that ' + type,
errors.SavedObjectNotFound);
};
inherits(errors.SavedObjectNotFound, KbnError);
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
errors.IndexPatternMissingIndices = function IndexPatternMissingIndices(type) {
KbnError.call(this,
'IndexPattern\'s configured pattern does not match any indices',
errors.IndexPatternMissingIndices);
};
inherits(errors.IndexPatternMissingIndices, KbnError);
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
errors.NoDefinedIndexPatterns = function NoDefinedIndexPatterns(type) {
KbnError.call(this,
'Define at least one index pattern to continue',
errors.NoDefinedIndexPatterns);
};
inherits(errors.NoDefinedIndexPatterns, KbnError);
return errors;
});

View file

@ -1,6 +1,6 @@
define(function (require) {
return function EnsureSomeIndexPatternsFn(Private, Notifier, $location, $route) {
var errors = Private(require('../_errors'));
var errors = require('errors');
var notify = new Notifier();
return function ensureSomeIndexPatterns() {

View file

@ -0,0 +1,157 @@
define(function (require) {
return function IndexPatternFactory(Private, timefilter, configFile, Notifier) {
var _ = require('lodash');
var angular = require('angular');
var errors = require('errors');
var mapper = Private(require('./_mapper'));
var fieldFormats = Private(require('./_field_formats'));
var patternCache = Private(require('./_pattern_cache'));
var intervals = Private(require('./_intervals'));
var mappingSetup = Private(require('utils/mapping_setup'));
var DocSource = Private(require('courier/data_source/doc_source'));
var type = 'index-pattern';
var notify = new Notifier();
var mapping = mappingSetup.expandShorthand({
title: 'string',
timeFieldName: 'string',
intervalName: 'string',
customFormats: 'json',
fields: 'json'
});
function IndexPattern(id) {
var pattern = this;
// set defaults
pattern.id = id;
pattern.title = id;
pattern.customFormats = {};
var docSource = new DocSource();
pattern.init = function () {
// tell the docSource where to find the doc
docSource
.index(configFile.kibanaIndex)
.type(type)
.id(pattern.id);
// check that the mapping for this type is defined
return mappingSetup.isDefined(type)
.then(function (defined) {
if (defined) return true;
return mappingSetup.setup(type, mapping);
})
.then(function () {
// If there is no id, then there is no document to fetch from elasticsearch
if (!pattern.id) return;
// fetch the object from ES
return docSource.fetch()
.then(function applyESResp(resp) {
if (!resp.found) throw new errors.SavedObjectNotFound(type);
// deserialize any json fields
_.forOwn(mapping, function ittr(fieldMapping, name) {
if (fieldMapping._deserialize) {
resp._source[name] = fieldMapping._deserialize(resp._source[name], resp, name, fieldMapping);
}
});
// Give obj all of the values in _source.fields
_.assign(pattern, resp._source);
if (pattern.id) {
if (!pattern.fields) return pattern.fetchFields();
afterFieldsSet();
}
// Any time obj is updated, re-call applyESResp
docSource.onUpdate().then(applyESResp, notify.fatal);
});
})
.then(function () {
// return our obj as the result of init()
return pattern;
});
};
function afterFieldsSet() {
pattern.fieldsByName = {};
pattern.fields.forEach(function (field) {
pattern.fieldsByName[field.name] = field;
// non-enumerable type so that it does not get included in the JSON
Object.defineProperty(field, 'format', {
enumerable: false,
get: function () {
var formatName = pattern.customFormats && pattern.customFormats[field.name];
return formatName ? fieldFormats.byName[formatName] : fieldFormats.defaultByType[field.type];
}
});
});
}
pattern.toIndexList = function (start, stop) {
var interval = this.intervalName && _.find(intervals, { name: this.intervalName });
if (interval) {
return intervals.toIndexList(pattern.id, interval, start, stop);
} else {
return pattern.id;
}
};
pattern.save = function () {
var body = {};
// serialize json fields
_.forOwn(mapping, function (fieldMapping, fieldName) {
if (pattern[fieldName] != null) {
body[fieldName] = (fieldMapping._serialize)
? fieldMapping._serialize(pattern[fieldName])
: pattern[fieldName];
}
});
// ensure that the docSource has the current pattern.id
docSource.id(pattern.id);
// index the document
return docSource.doIndex(body)
.then(function (id) {
pattern.id = id;
return pattern.id;
});
};
pattern.refreshFields = function () {
return mapper.clearCache(pattern)
.then(function () {
return pattern.fetchFields();
});
};
pattern.fetchFields = function () {
return mapper.getFieldsForIndexPattern(pattern, true)
.then(function (fields) {
pattern.fields = fields;
afterFieldsSet();
return pattern.save();
});
};
pattern.toJSON = function () {
return pattern.id;
};
pattern.toString = function () {
return '' + pattern.toJSON();
};
}
return IndexPattern;
};
});

View file

@ -0,0 +1,71 @@
define(function (require) {
return function IndexNameIntervalsService(timefilter) {
var _ = require('lodash');
var moment = require('moment');
var intervals = [
{
name: 'hours',
display: 'Hourly'
},
{
name: 'days',
display: 'Daily'
},
{
name: 'weeks',
display: 'Weekly'
},
{
name: 'months',
display: 'Monthly'
},
{
name: 'years',
display: 'Yearly'
}
];
intervals.toIndexList = function (format, interval, a, b) {
var bounds;
// setup the range that the list will span, return two moment objects that
// are in proper order. a and b can be numbers to specify to go before or after now (respectively)
// a certain number of times, based on the interval
var range = [ [a, 'min'], [b, 'max'] ].map(function (v) {
var val = v[0];
var bound = v[1];
// grab a bound from the time filter
if (val == null) {
bounds = bounds || timefilter.getBounds();
return bounds[bound];
}
if (_.isNumeric(val)) return moment().add(interval.name, val);
if (moment.isMoment(val)) return val;
return moment(val);
}).sort(function (a, b) {
return a - b;
});
if (typeof interval === 'string') {
interval = _.find(intervals, { name: interval });
if (!interval) throw new Error('Interval must be one of ' + _.pluck(intervals, 'name'));
}
var indexList = [];
var start = range.shift();
// turn stop into milliseconds to that it's not constantly converted by the while condition
var stop = range.shift().valueOf();
while (start <= stop) {
start.add(interval.name, 1);
indexList.push(start.format(format));
}
return indexList;
};
return intervals;
};
});

View file

@ -1,9 +1,10 @@
define(function (require) {
var _ = require('lodash');
return function MapperService(Private, Promise, es, configFile) {
var IndexPatternMissingIndices = Private(require('../_errors')).IndexPatternMissingIndices;
var _ = require('lodash');
var IndexPatternMissingIndices = require('errors').IndexPatternMissingIndices;
var transformMappingIntoFields = Private(require('./_transform_mapping_into_fields'));
var intervals = Private(require('./_intervals'));
var LocalCache = Private(require('./_local_cache'));
@ -23,19 +24,22 @@ define(function (require) {
* @async
*/
mapper.getFieldsForIndexPattern = function (indexPattern, skipIndexPatternCache) {
var cache;
if (cache = fieldCache.get(indexPattern)) return Promise.resolve(cache);
var id = indexPattern.id;
var indexList = indexPattern.toIndexList(0, 5);
var cache = fieldCache.get(id);
if (cache) return Promise.resolve(cache);
if (!skipIndexPatternCache) {
return es.get({
index: configFile.kibanaIndex,
type: 'index-pattern',
id: indexPattern,
id: id,
_sourceInclude: ['fields']
})
.then(function (resp) {
if (resp.found && resp._source.fields) {
fieldCache.set(indexPattern, JSON.parse(resp._source.fields));
fieldCache.set(id, JSON.parse(resp._source.fields));
}
return mapper.getFieldsForIndexPattern(indexPattern, true);
});
@ -43,8 +47,10 @@ define(function (require) {
return es.indices.getFieldMapping({
// TODO: Change index to be the resolved in some way, last three months, last hour, last year, whatever
index: indexPattern,
index: indexList,
field: '*',
ignoreUnavailable: _.isArray(indexList),
allowNoIndices: false
})
.catch(function (err) {
if (err.status >= 400) {
@ -57,8 +63,8 @@ define(function (require) {
})
.then(transformMappingIntoFields)
.then(function (fields) {
fieldCache.set(indexPattern, fields);
return fieldCache.get(indexPattern);
fieldCache.set(id, fields);
return fieldCache.get(id);
});
};

View file

@ -0,0 +1,31 @@
define(function (require) {
return function PatternToWildcardFn() {
return function (format) {
var wildcard = '';
var inEscape = false;
var inPattern = false;
for (var i = 0; i < format.length; i++) {
var ch = format.charAt(i);
switch (ch) {
case '[':
if (!inEscape) inEscape = true;
else wildcard += ch;
break;
case ']':
if (inEscape) inEscape = false;
else wildcard += ch;
break;
default:
if (inEscape) wildcard += ch;
else if (!inPattern) {
wildcard += '*';
inPattern = true;
}
}
}
return wildcard;
};
};
});

View file

@ -1,7 +1,7 @@
define(function (require) {
return function transformMappingIntoFields(Private, configFile) {
var _ = require('lodash');
var MappingConflict = Private(require('../_errors')).MappingConflict;
var MappingConflict = require('errors').MappingConflict;
var castMappingType = Private(require('./_cast_mapping_type'));
var reservedFields = {

View file

@ -1,13 +1,13 @@
define(function (require) {
return function IndexPatternsService(configFile, es, Notifier, Private, Promise) {
var module = require('modules').get('kibana/index_patterns');
module.service('indexPatterns', function (configFile, es, Notifier, Private, Promise) {
var indexPatterns = this;
var _ = require('lodash');
var errors = require('errors');
var IndexPattern = Private(require('./_index_pattern'));
var patternCache = Private(require('./_pattern_cache'));
var SourceAbstract = Private(require('../data_source/_abstract'));
var errors = Private(require('../_errors'));
var notify = new Notifier({ location: 'IndexPatterns Service'});
@ -39,23 +39,14 @@ define(function (require) {
});
};
indexPatterns.getFieldsFor = function (indexish) {
// pull the index string out of Source objects
if (indexish instanceof SourceAbstract) {
indexish = indexish.get('index');
}
return Promise.cast(_.isObject(indexish) ? indexish : indexPatterns.get(indexish))
.then(function (indexPattern) {
return indexPattern.fields;
});
};
indexPatterns.errors = {
MissingIndices: errors.IndexPatternMissingIndices
};
indexPatterns.ensureSome = Private(require('./_ensure_some'));
indexPatterns.cache = patternCache;
};
indexPatterns.intervals = Private(require('./_intervals'));
indexPatterns.mapper = Private(require('./_mapper'));
indexPatterns.patternToWildcard = Private(require('./_pattern_to_wildcard'));
});
});

View file

@ -7,7 +7,7 @@ define(function (require) {
return {
restrict: 'E',
scope: {
info: '=',
info: '@',
placement: '@'
},
template: html,

View file

@ -2,11 +2,13 @@ require.config({
baseUrl: './kibana',
paths: {
// components
kibana: './index',
courier: './components/courier',
config: './components/config',
setup: './components/setup',
kibana: './index',
config: './components/config',
errors: './components/errors',
notify: './components/notify',
courier: './components/courier',
index_patterns: './components/index_patterns',
state_management: './components/state_management',
// special utils

View file

@ -7209,3 +7209,24 @@ saved-object-finder .finder-options > li.active a {
color: #b4bcc2;
padding: 5px 15px;
}
.form-control[disabled],
.form-control[readonly],
fieldset[disabled] .form-control {
cursor: default;
opacity: .8;
}
input[type="radio"][disabled],
input[type="checkbox"][disabled],
.radio[disabled],
.radio-inline[disabled],
.checkbox[disabled],
.checkbox-inline[disabled],
fieldset[disabled] input[type="radio"],
fieldset[disabled] input[type="checkbox"],
fieldset[disabled] .radio,
fieldset[disabled] .radio-inline,
fieldset[disabled] .checkbox,
fieldset[disabled] .checkbox-inline {
cursor: default;
opacity: .8;
}

View file

@ -216,3 +216,27 @@ saved-object-finder {
color: @text-muted;
padding: @padding-base-vertical @padding-base-horizontal;
}
//== override the disabled cursor that doesn't work everywhere
.form-control{
&[disabled],
&[readonly],
fieldset[disabled] & {
cursor: default;
opacity: .8;
}
}
input[type="radio"],
input[type="checkbox"],
.radio,
.radio-inline,
.checkbox,
.checkbox-inline {
&[disabled],
fieldset[disabled] & {
cursor: default;
opacity: .8;
}
}

View file

@ -1,8 +1,18 @@
define(function () {
return function MappingSetupService(configFile, es) {
var angular = require('angular');
var _ = require('lodash');
var mappingSetup = this;
var json = {
_serialize: function (val) {
if (val != null) return angular.toJson(val);
},
_deserialize: function (val) {
if (val != null) return JSON.parse(val);
}
};
/**
* Use to create the mappings, but that should only happen one at a time
*/
@ -26,6 +36,22 @@ define(function () {
});
});
mappingSetup.expandShorthand = function (sh) {
return _.mapValues(sh || {}, function (val, prop) {
// allow shortcuts for the field types, by just setting the value
// to the type name
if (typeof val === 'string') val = { type: val };
if (val.type === 'json') {
val.type = 'string';
val._serialize = json._serialize;
val._deserialize = json._deserialize;
}
return val;
});
};
mappingSetup.isDefined = function (type) {
return getKnownKibanaTypes()
.then(function (knownTypes) {

View file

@ -55,6 +55,9 @@ define(function (require) {
return memo.apply(this, arguments);
}
};
},
isNumeric: function (v) {
return !isNaN(v) && _.isNumber(v);
}
});

View file

@ -1,182 +0,0 @@
/*
define(function (require) {
var sinon = require('test_utils/auto_release_sinon');
var _ = require('lodash');
require('angular-mocks');
return function extendCourierSuite() {
var courier, es, $httpBackend;
beforeEach(inject(function ($injector) {
courier = $injector.get('courier');
es = $injector.get('es');
$httpBackend = $injector.get('$httpBackend');
}));
describe('DocSource class', function () {
it('tracks the version of the document', function (done) {
var version = 51;
var source = courier
.createSource('doc').index('fake').type('fake').id('fake')
.on('results', function (doc) {
expect(source._getVersion()).to.eql(version);
expect(courier._getRefFor(source).version).to.eql(version);
done();
});
courier.start();
});
it('updates to a doc will propogate to other docs with the same index/type/id', function (done) {
var client = (function () {
// fake server state
var version = 0;
var doc = { hi: 'fallacy' };
return stubClient(es, {
update: function (params, cb) {
_.assign(doc, params.body.doc);
version++;
cb(void 0, { ok: true });
},
default: function (method, params, cb) {
cb(void 0, stubClient.doc({ _source: doc, _version: version }));
}
});
}());
var update = { hi: 'truth' };
// updating this
var pitcher = courier.createSource('doc').index('fake').type('fake').id('fake')
.doUpdate(update);
// should update this
var catcher = courier.createSource('doc').index('fake').type('fake').id('fake')
.on('results', function (doc) {
expect(doc._source).to.eql(update);
done();
});
});
it('clears the stored version when a document has been deleted', function (done) {
var client = (function () {
// fake server state
var doc = { hi: 'fallacy' };
return stubbedClient({
delete: function (params, cb) {
doc = null;
cb(void 0, { ok: true });
},
default: function (method, params, cb) {
if (doc) {
cb(void 0, stubbedClient.doc({ _source: doc }));
} else {
cb(void 0, stubbedClient.doc({ found: false, _source: null, _version: null }));
}
}
});
}());
var courier = createCourier(client);
var source = courier.createSource('doc')
.index('fake')
.type('fake')
.id('fake')
.on('results', function (doc) {
if (doc.found) {
client.delete({}, function () {
source.fetch();
});
} else {
expect(courier._getRefFor(source).version).to.be(void 0);
expect(source._getVersion()).to.be(void 0);
done();
}
});
courier.start();
});
it('checks localStorage for changes to the stored version, which will trigger the doc to be refetched', function (done) {
var version = 11234;
var courier = createCourier(stubbedClient(function (method, params, cb) {
cb(void 0, stubbedClient.doc({ _version: version }));
}));
var count = 0;
courier.docInterval(10);
var source = courier.createSource('doc')
.index('fake')
.type('fake')
.id('fake')
.on('results', function (doc) {
switch (count++) {
case 0:
// simulate removing the version in another tab
localStorage.removeItem(source._versionKey());
// get version should now be returning undefined
expect(source._getVersion()).to.eql(void 0);
// tell the courier to check docs 1 ms now
courier.docInterval(1);
break;
case 1:
// doc version should now be populated
expect(source._getVersion()).to.eql(version);
done();
}
});
courier.start();
});
describe('#doIndex', function () {
it('reindexes the doc using the hash passed in', function (done) {
var client = (function () {
var version = 1;
var doc = { initial: true };
return stubbedClient({
index: function (params, cb) {
doc = _.clone(params.body);
version ++;
cb(void 0, { ok: true });
},
mget: function (params, cb) {
cb(void 0, stubbedClient.doc({ _version: version, _source: doc }));
}
});
}());
var courier = createCourier(client);
var count = 0;
courier.docInterval(10);
var source = courier.createSource('doc')
.index('fake')
.type('fake')
.id('fake')
.on('results', function (doc) {
switch (count ++) {
case 0:
expect(doc._source).to.have.property('initial', true);
source.doIndex({ second: true });
break;
case 1:
expect(doc._source).to.not.have.property('initial');
expect(doc._source).to.have.property('second', true);
done();
break;
}
});
courier.start();
});
});
});
};
});
*/