switched to indexPatters as saved objects

This commit is contained in:
Spencer Alger 2014-05-01 16:31:25 -07:00
parent 554e224d5c
commit 30851d68c9
56 changed files with 888 additions and 953 deletions

View file

@ -1,21 +1,17 @@
define(function (require) {
var module = require('modules').get('app/dashboard', [
'kibana/saved_object'
]);
var module = require('modules').get('app/dashboard');
var _ = require('lodash');
var inherits = require('utils/inherits');
require('saved_object/saved_object');
// Used only by the savedDashboards service, usually no reason to change this
module.factory('SavedDashboard', function (configFile, courier, Promise, createNotifier, SavedObject) {
module.factory('SavedDashboard', function (courier) {
// SavedDashboard constructor. Usually you'd interact with an instance of this.
// ID is option, without it one will be generated on save.
function SavedDashboard(id) {
// Gives our SavedDashboard the properties of a SavedObject
SavedObject.call(this, {
courier.SavedObject.call(this, {
// this object will be saved at {{configFile.kibanaIndex}}/dashboard/{{id}}
type: 'dashboard',
@ -41,7 +37,7 @@ define(function (require) {
}
// Sets savedDashboard.prototype to an instance of SavedObject
inherits(SavedDashboard, SavedObject);
inherits(SavedDashboard, courier.SavedObject);
return SavedDashboard;
});

View file

@ -13,7 +13,7 @@ define(function (require) {
require('directives/timepicker');
require('directives/fixed_scroll');
require('filters/moment');
require('apps/settings/services/index_patterns');
require('courier/courier');
require('state_management/app_state');
require('services/timefilter');
@ -40,8 +40,8 @@ define(function (require) {
}
});
},
indexPatternList: function (indexPatterns) {
return indexPatterns.getIds();
indexPatternList: function (courier) {
return courier.indexPatterns.getIds();
}
}
});
@ -261,15 +261,15 @@ define(function (require) {
}
function setFields() {
return searchSource.getFields($scope.opts.index)
.then(function (fields) {
return courier.getFieldsFor($scope.opts.index)
.then(function (rawFields) {
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
display: field.display
};
}, {});
if (!fields) return;
if (!rawFields) return;
var columnObjects = arrayToKeys($scope.state.columns);
@ -279,16 +279,10 @@ define(function (require) {
// Inject source into list;
$scope.fields.push({name: '_source', type: 'source', display: false});
_(fields)
.keys()
.sort()
.each(function (name) {
var field = fields[name];
field.name = name;
_.defaults(field, currentState[name]);
$scope.fields.push(_.defaults(field, {display: columnObjects[name] || false}));
});
_.sortBy(rawFields, 'name').forEach(function (field) {
_.defaults(field, currentState[field.name]);
$scope.fields.push(_.defaults(field, {display: columnObjects[name] || false}));
});
// TODO: timefield should be associated with the index pattern, this is a hack
// to pick the first date field and use it.

View file

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

View file

@ -5,11 +5,10 @@ define(function (require) {
require('notify/notify');
var module = require('modules').get('discover/saved_searches', [
'kibana/notify',
'kibana/courier'
'kibana/notify'
]);
module.service('savedSearches', function (courier, configFile, es, createNotifier, SavedSearch) {
module.service('savedSearches', function (configFile, es, createNotifier, SavedSearch) {
var notify = createNotifier({
location: 'Saved Searches'
});

View file

@ -3,6 +3,19 @@ define(function (require) {
var _ = require('lodash');
var configDefaults = require('config/defaults');
require('../sections').push({
order: 2,
name: 'advanced',
display: 'Advanced',
url: '#/settings/advanced',
template: require('text!../partials/advanced.html'),
resolve: {
noId: function ($route, $location) {
if ($route.current.params.id) $location.url('/settings/advanced');
}
}
});
module.controller('advancedSettings', function ($scope, config, Notifier) {
var notify = new Notifier();
var configVals = config._vals();

View file

@ -1,102 +1,100 @@
define(function (require) {
var module = require('modules').get('settings/controllers');
var _ = require('lodash');
module.controller('indexSettings', function ($scope, courier, Notifier, $route, $location, es, config, indexPatterns) {
require('courier/courier');
require('../sections').push({
order: 1,
name: 'indices',
display: 'Indices',
url: '#/settings/indices',
template: require('text!../partials/indices.html'),
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.id);
},
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
}
}
});
module.controller('indexSettings', function ($scope, courier, Notifier, $route, $location, es, config) {
var notify = new Notifier({
location: 'Index Settings'
});
var init = function () {
$scope.getPatterns();
$scope.indices = {
id: $route.current.params.id,
table: {
by: 'field',
reverse: false,
page: 0,
max: 20
},
default: config.get('defaultIndex')
};
$scope.indexPattern = $route.current.locals.indexPattern;
$scope.indexPatternIds = $route.current.locals.indexPatternIds;
if (!!$scope.indices.id) {
loadPattern($scope.indices.id);
}
$scope.defaultIndex = config.get('defaultIndex');
$scope.$on('change:config.defaultIndex', function () {
$scope.defaultIndex = config.get('defaultIndex');
});
$scope.table = {
by: 'name',
reverse: false,
page: 0,
max: 20
};
var loadPattern = function (pattern) {
return indexPatterns.getFields(pattern)
.then(function (fields) {
$scope.indices.mapping = fields;
$scope.refreshFields = function () {
$scope.indexPattern.refreshFields();
};
$scope.removePattern = function () {
courier.indexPatterns.delete($scope.indexPattern)
.then(refreshKibanaIndex)
.then(function () {
$location.path('/settings/indices');
})
.catch(function () {
$location.path('/settings/indices');
});
.catch(notify.fatal);
};
// TODO: Resolve this in the routes, otherwise the warning will show for a moment;
$scope.getPatterns = function (pattern) {
return indexPatterns.getIds()
.then(function (ids) {
$scope.indices.patterns = ids;
});
};
$scope.create = function () {
$scope.indexPattern.id = $scope.indexPattern.title;
$scope.refreshFields = function (pattern) {
return indexPatterns.delete(pattern)
// refresh the fields, to verify that the
$scope.indexPattern.refreshFields()
.then(refreshKibanaIndex)
.then(function () {
return indexPatterns.create(pattern);
});
};
$scope.removePattern = function (pattern) {
indexPatterns.delete(pattern)
.then(function () {
$location.path('/settings/indices');
$location.path('/settings/indices/' + $scope.indexPattern.id);
})
.catch(function (err) {
$location.path('/settings/indices');
if (err.status >= 400) notify.error('Could not locate any indices matching that pattern. Please add the index to Elasticsearch');
else notify.fatal(err);
});
};
$scope.addPattern = function (pattern) {
indexPatterns.create(pattern)
.then(function () {
$location.path('/settings/indices/' + pattern);
})
.catch(function (err) {
if (err.status >= 400) {
notify.error('Could not locate any indices matching that pattern. Please add the index to Elasticsearch');
}
});
};
$scope.setDefaultPattern = function (pattern) {
config.set('defaultIndex', pattern)
.then(function () {
$scope.indices.default = pattern;
});
$scope.setDefaultPattern = function () {
config.set('defaultIndex', $scope.indexPattern.id);
};
$scope.setFieldSort = function (by) {
if ($scope.indices.table.by === by) {
$scope.indices.table.reverse = !$scope.indices.table.reverse;
if ($scope.table.by === by) {
$scope.table.reverse = !$scope.table.reverse;
} else {
$scope.indices.table.by = by;
$scope.table.by = by;
}
};
$scope.sortClass = function (column) {
if ($scope.indices.table.by !== column) return;
return $scope.indices.table.reverse ? ['fa', 'fa-sort-asc'] : ['fa', 'fa-sort-desc'];
if ($scope.table.by !== column) return;
return $scope.table.reverse ? ['fa', 'fa-sort-asc'] : ['fa', 'fa-sort-desc'];
};
$scope.tablePages = function () {
if (!$scope.indices.mapping) return 0;
return Math.ceil($scope.indices.mapping.length / $scope.indices.table.max);
if (!$scope.indexPattern.fields) return 0;
return Math.ceil($scope.indexPattern.fields.length / $scope.table.max);
};
init();
var refreshKibanaIndex = function () {
return es.indices.refresh({
index: config.file.kibanaIndex
});
};
$scope.$emit('application.load');
});

View file

@ -6,7 +6,10 @@ define(function (require, module, exports) {
require('css!./styles/main.css');
require('filters/start_from');
require('./services/index_patterns');
var sections = require('./sections');
// each controller will write it's section config to the sections module
require('./controllers/advanced');
require('./controllers/indices');
@ -16,31 +19,28 @@ define(function (require, module, exports) {
})
.when('/settings/:section/:id?', {
template: require('text!./index.html'),
reloadOnSearch: false
});
reloadOnSearch: false,
resolve: {
sectionLocals: function ($route, $location, $injector, Promise) {
var section = _.find(sections, { name: $route.current.params.section });
if (!section) return $location.url('/settings/');
var sections = [
{
name: 'indices',
display: 'Indices',
url: '#/settings/indices',
template: require('text!./partials/indices.html')
},
{
name: 'advanced',
display: 'Advanced',
url: '#/settings/advanced',
template: require('text!./partials/advanced.html')
return Promise.props(_.mapValues(section.resolve || {}, function (fn) {
return $injector.invoke(fn);
}));
}
}
];
});
app.directive('settingsApp', function ($route, $location, $compile) {
return {
restrict: 'E',
link: function ($scope, $el) {
$scope.sections = sections;
$scope.sections = _.sortBy(sections, 'order');
$scope.section = _.find($scope.sections, { name: $route.current.params.section });
if (!$scope.section) return $location.url('/settings');
// reassign the sectionLocals to the locals object, so that the section's controller can use their own logic
_.unwrapProp($route.current.locals, 'sectionLocals');
$scope.sections.forEach(function (section) {
section.class = (section === $scope.section) ? 'active' : void 0;

View file

@ -2,12 +2,12 @@
<div class="col-md-2 sidebar-container">
<div class="sidebar-list">
<div class="sidebar-list-header">
<h5>Index Patterns <a ng-show="indices.id" href="#/settings/indices" class="btn btn-primary btn-xs"><i class="fa fa-plus"></i> Add New</a></h5>
<h5>Index Patterns <a ng-show="indexPattern.id" href="#/settings/indices" class="btn btn-primary btn-xs"><i class="fa fa-plus"></i> Add New</a></h5>
</div>
<ul class="list-unstyled">
<li class="sidebar-item" ng-repeat="pattern in defaultPattern = (indices.patterns | filter:indices.default)">
<li class="sidebar-item" ng-repeat="pattern in defaultPattern = (indexPatternIds | filter:defaultIndex)">
<a ng-href="#/settings/indices/{{pattern}}">
<div class="sidebar-item-title" ng-class="{'active': pattern == indices.id}">
<div class="sidebar-item-title" ng-class="{'active': pattern == indexPattern.id}">
<i class="fa fa-star"></i> <span bo-text="pattern"></span>
</div>
</a>
@ -17,9 +17,9 @@
<strong>Warning:</strong> No default index pattern. You must select or create one to continue.
</div>
</li>
<li class="sidebar-item" ng-repeat="pattern in indices.patterns | filter:'!'+indices.default">
<li class="sidebar-item" ng-repeat="pattern in indexPatternIds | filter:'!'+defaultIndex">
<a ng-href="#/settings/indices/{{pattern}}">
<div class="sidebar-item-title" ng-class="{'active': pattern == indices.id}">
<div class="sidebar-item-title" ng-class="{'active': pattern == indexPattern.id}">
<span bo-text="pattern"></span>
</div>
</a>
@ -31,7 +31,7 @@
<div class="container-fluid">
<div ng-show="!indices.id">
<div ng-if="!indexPattern.id">
<div class="col-md-12">
<div class="page-header">
<h1>Configure an index pattern</h1>
@ -39,11 +39,11 @@
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="addPattern(indices.newIndex.pattern)">
<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="indices.newIndex.pattern" required>
ng-model="indexPattern.title" required>
</div>
<button type="submit" class="btn btn-primary"
ng-disabled="form.$invalid">Create</button>
@ -51,33 +51,43 @@
</div>
</div>
<div ng-show="indices.id">
<div ng-if="indexPattern.id">
<div class="col-md-12">
<div class="page-header">
<h1>
{{indices.id}}
<button class="btn btn-success" tooltip="Set as default index" ng-click="setDefaultPattern(indices.id)">
{{indexPattern.id}}
<button
ng-click="setDefaultPattern()"
tooltip="Set as default index"
class="btn btn-success"
ng-disabled="indexPattern.id === defaultIndex">
<i class="fa fa-star"></i>
</button>
<button class="btn btn-warning" tooltip="Refresh field list" ng-click="refreshFields(indices.id)">
<button
ng-click="indexPattern.refreshFields()"
tooltip="Refresh field list"
class="btn btn-warning">
<i class="fa fa-refresh"></i>
</button>
<button class="btn btn-danger" tooltip="Remove index pattern" ng-click="removePattern(indices.id)">
<button
ng-click="removePattern()"
tooltip="Remove index pattern"
class="btn btn-danger">
<i class="fa fa-ban"></i>
</button>
</h1>
This page lists every field in the <strong>{{indices.id}}</strong> index and the field's
This page lists every field in the <strong>{{indexPattern.id}}</strong> index and the field's
associated core type as recorded by Elasticsearch
</div>
<div ng-hide="tablePages() == 1">
<center>
<button class="btn btn-sm" ng-disabled="indices.table.page == 0" ng-click="indices.table.page=indices.table.page-1">
<button class="btn btn-sm" ng-disabled="table.page == 0" ng-click="table.page=table.page-1">
Previous
</button>
{{indices.table.page+1}}/{{tablePages()}}
<button class="btn btn-sm" ng-disabled="indices.table.page >= indices.mapping.length/indices.table.max - 1" ng-click="indices.table.page=indices.table.page+1">
{{table.page+1}}/{{tablePages()}}
<button class="btn btn-sm" ng-disabled="table.page >= indexPattern.fields.length/table.max - 1" ng-click="table.page=table.page+1">
Next
</button>
</center>
@ -85,12 +95,12 @@
<table class="table">
<thead>
<th ng-click="setFieldSort('field')">field <i ng-class="sortClass('field')"></i></th>
<th ng-click="setFieldSort('mapping.type')">type <i ng-class="sortClass('mapping.type')"></i></th>
<th ng-click="setFieldSort('name')">name <i ng-class="sortClass('name')"></i></th>
<th ng-click="setFieldSort('type')">type <i ng-class="sortClass('type')"></i></th>
</thead>
<tr ng-repeat="field in indices.mapping | orderBy:indices.table.by:indices.table.reverse | startFrom:indices.table.page*indices.table.max | limitTo:indices.table.max">
<td bo-text="field.field"></td>
<td bo-text="field.mapping.type"></td>
<tr ng-repeat="field in indexPattern.fields | orderBy:table.by:table.reverse | startFrom:table.page*table.max | limitTo:table.max">
<td bo-text="field.name"></td>
<td bo-text="field.type"></td>
</tr>
</table>
</div>

View file

@ -0,0 +1,5 @@
define(function (require) {
// sections are defined by the controllers required by the index, and are defined by
// the controller, but written here so that they can easily be shared
return [];
});

View file

@ -1,91 +0,0 @@
define(function (require) {
var module = require('modules').get('app/settings');
var _ = require('lodash');
module.service('indexPatterns', function (config, es, courier) {
this.getFields = function (id) {
if (typeof id === 'object' && typeof id.get === 'function') {
id = id.get('index');
}
return es.get({
index: config.file.kibanaIndex,
type: 'mapping',
id: id
})
.then(function (resp) {
return _.map(resp._source, function (v, k) {
return {field: k, mapping: v};
});
});
};
this.getIds = function () {
return es.search({
index: config.file.kibanaIndex,
type: 'mapping',
fields: [],
body: {
query: { match_all: {} }
}
})
.then(function (resp) {
return _.pluck(resp.hits.hits, '_id');
});
};
this.find = function (searchString) {
return es.search({
index: config.file.kibanaIndex,
type: 'mapping',
fields: [],
body: {
query: {
multi_match: {
query: searchString || '',
type: 'phrase_prefix',
fields: ['_id'],
zero_terms_query: 'all'
}
}
}
})
.then(function (resp) {
return resp.hits.hits.map(function (hit) {
return {
id: hit._id,
title: hit._id,
url: '/settings/indices/' + hit._id
};
});
});
};
this.delete = function (pattern) {
var source = courier.createSource('search').index(pattern);
return source.clearFieldCache()
.then(function () {
return es.indices.refresh({
index: config.file.kibanaIndex
});
})
.finally(function () {
source.destroy();
});
};
this.create = function (pattern) {
var source = courier.createSource('search').index(pattern);
return source.getFields()
.then(function (fields) {
return es.indices.refresh({
index: config.file.kibanaIndex
});
})
.finally(function () {
source.destroy();
});
};
});
});

View file

@ -14,12 +14,12 @@ define(function (require) {
var aggs = require('../saved_visualizations/_aggs');
var visConfigCategories = require('../saved_visualizations/_config_categories');
var getVisAndFieldsHash = function (savedVisualizations, courier, Notifier, $location, $route) {
var getVisAndFields = function (savedVisualizations, courier, Notifier, $location, $route) {
return function (using) {
return savedVisualizations.get(using)
.then(function (vis) {
// get the fields before we render, but return the vis
return vis.searchSource.getFields()
return courier.getFieldsFor(vis.searchSource)
.then(function (fields) {
return [vis, fields];
});
@ -40,16 +40,16 @@ define(function (require) {
.when('/visualize/create', {
template: require('text!../editor.html'),
resolve: {
visAndFieldsHash: function ($injector, $route) {
return $injector.invoke(getVisAndFieldsHash)($route.current.params);
visAndFields: function ($injector, $route) {
return $injector.invoke(getVisAndFields)($route.current.params);
}
}
})
.when('/visualize/edit/:id', {
template: require('text!../editor.html'),
resolve: {
visAndFieldsHash: function ($injector, $route) {
return $injector.invoke(getVisAndFieldsHash)($route.current.params.id);
visAndFields: function ($injector, $route) {
return $injector.invoke(getVisAndFields)($route.current.params.id);
}
}
});
@ -60,26 +60,14 @@ define(function (require) {
});
// get the vis loaded in from the routes
var vis = $route.current.locals.visAndFieldsHash[0];
var vis = $route.current.locals.visAndFields[0];
// vis.destroy called by visualize directive
var fieldsHash = $route.current.locals.visAndFieldsHash[1];
$scope.fields = _.sortBy($route.current.locals.visAndFields[1], 'name');
$scope.fields.byName = _.indexBy($scope.fields, 'name');
var $state = new AppState(vis.getState());
// get the current field list
// create a sorted list of the fields for display purposes
$scope.fields = _(fieldsHash)
.keys()
.sort()
.transform(function (fields, name) {
var field = fieldsHash[name];
field.name = name;
fields.push(field);
})
.value();
$scope.fields.byName = fieldsHash;
$scope.vis = vis;
$scope.aggs = aggs;
$scope.visConfigCategories = visConfigCategories;

View file

@ -3,7 +3,7 @@ define(function (require) {
var typeDefs = require('../saved_visualizations/_type_defs');
require('../saved_visualizations/saved_visualizations');
require('saved_object/finder.directive');
require('directives/saved_object_finder');
require('apps/discover/saved_searches/saved_searches');
var app = require('modules').get('app/visualize', [
@ -22,13 +22,13 @@ define(function (require) {
routes.when('/visualize/step/1', {
template: templateStep(1, require('text!../partials/wizard/step_1.html')),
resolve: {
indexPatternIds: function (indexPatterns) {
return indexPatterns.getIds();
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
}
}
});
app.controller('VisualizeWizardStep1', function ($route, $scope, courier, config, $location, indexPatterns, timefilter) {
app.controller('VisualizeWizardStep1', function ($route, $scope, $location, timefilter) {
$scope.step2WithSearchUrl = function (hit) {
return '#/visualize/step/2?savedSearchId=' + encodeURIComponent(hit.id);
};

View file

@ -7,14 +7,14 @@ define(function (require) {
var module = require('modules').get('kibana/directive');
var chart; // set in "vis" watcher
module.directive('visualize', function (createNotifier) {
module.directive('visualize', function (createNotifier, SavedVis) {
return {
restrict: 'E',
link: function ($scope, $el) {
$scope.$watch('vis', function (vis, prevVis) {
if (prevVis) prevVis.destroy();
if (prevVis && prevVis.destroy) prevVis.destroy();
if (chart) chart.destroy();
if (!vis) return;
if (!(vis instanceof SavedVis)) return;
var notify = createNotifier({
location: vis.typeName + ' visualization'

View file

@ -6,11 +6,9 @@ define(function (require) {
var aggs = require('./_aggs');
var typeDefs = require('./_type_defs');
require('services/root_search');
var module = require('modules').get('kibana/services');
module.factory('SavedVis', function (config, $injector, SavedObject, rootSearch, Promise, savedSearches) {
module.factory('SavedVis', function (config, $injector, courier, Promise, savedSearches) {
function SavedVis(opts) {
var vis = this;
opts = opts || {};
@ -21,10 +19,10 @@ define(function (require) {
};
}
var typeDef = typeDefs.byName[opts.type];
var parentSearch = opts.parentSearchSource || rootSearch;
var typeDef;
var parentSearch = opts.parentSearchSource || courier.SavedObject.rootSearch();
SavedObject.call(vis, {
courier.SavedObject.call(vis, {
type: 'visualization',
id: opts.id,
@ -47,12 +45,6 @@ define(function (require) {
searchSource: true,
init: function () {
configCats.forEach(function (category) {
vis._initConfigCategory(category);
});
},
afterESResp: function setVisState() {
if (!typeDef || vis.typeName !== typeDef.name) {
// refresh the typeDef
@ -96,7 +88,7 @@ define(function (require) {
vis._fillConfigsToMinimum();
// get and cache the field list
return vis.searchSource.getFields();
return courier.getFieldsFor(vis.searchSource.get('index'));
})
.then(function () {
return vis;
@ -212,7 +204,7 @@ define(function (require) {
*/
vis.buildChartDataFromResponse = $injector.invoke(require('./_build_chart_data'));
}
inherits(SavedVis, SavedObject);
inherits(SavedVis, courier.SavedObject);
return SavedVis;
});

View file

@ -4,7 +4,7 @@ define(function (require) {
require('./_saved_vis');
app.service('savedVisualizations', function (es, config, courier, $q, $timeout, SavedVis) {
app.service('savedVisualizations', function (es, config, SavedVis) {
this.get = function (id) {
return (new SavedVis(id)).init();
};

View file

@ -1,4 +1,5 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var nextTick = require('utils/next_tick');
var configFile = require('../../../config');
@ -7,8 +8,7 @@ define(function (require) {
require('notify/notify');
var module = require('modules').get('kibana/config', [
'kibana/notify',
'kibana/courier'
'kibana/notify'
]);
// guid within this window
@ -23,13 +23,15 @@ define(function (require) {
module.constant('configFile', configFile);
// service for delivering config variables to everywhere else
module.service('config', function ($rootScope, createNotifier, courier, kbnVersion, configFile, Promise, setup) {
module.service('config', function (Private, $rootScope, Notifier, kbnVersion, Promise, setup) {
var config = this;
var notify = createNotifier({
var notify = new Notifier({
location: 'Config'
});
var doc = courier.createSource('doc')
var DocSource = Private(require('courier/data_source/doc_source'));
var doc = (new DocSource())
.index(configFile.kibanaIndex)
.type('config')
.id(kbnVersion);
@ -51,13 +53,20 @@ define(function (require) {
config.init = _.once(function () {
notify.lifecycle('config init');
return setup.bootstrap().then(function getDoc() {
return doc.fetch()
.then(function (resp) {
if (!resp.found) {
return doc.doIndex({}).then(getDoc);
}
return doc.fetch().then(function initDoc(resp) {
if (!resp.found) return doc.doIndex({}).then(initDoc);
else {
vals = _.cloneDeep(resp._source || {});
vals = _.cloneDeep(resp._source);
doc.onUpdate(function applyMassUpdate(resp) {
var allKeys = [].concat(_.keys(vals), _.keys(resp._source));
_.uniq(allKeys).forEach(function (key) {
if (!angular.equals(vals[key], resp._source[key])) {
change(key, resp._source[key]);
}
});
});
}
});
})
.finally(function () {

View file

@ -0,0 +1,10 @@
define(function (require) {
return function ErrorHandlerList() {
/**
* Queue of pending error handlers, they are removed as
* they are resolved.
* @type {Array}
*/
return [];
};
});

View file

@ -1,14 +1,13 @@
define(function (require) {
var _ = require('lodash');
var module = require('modules').get('kibana/courier');
var inherits = require('utils/inherits');
var canStack = (function () {
var err = new Error();
return !!err.stack;
}());
var err = new Error();
return !!err.stack;
}());
module.service('couriersErrors', function () {
return function () {
var errors = this;
// abstract error class
@ -139,5 +138,15 @@ define(function (require) {
};
inherits(errors.SavedObjectNotFound, CourierError);
});
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
errors.MissingIndexPattern = function MissingIndexPattern(type) {
CourierError.call(this,
'SearchSource expects index to be an indexPattern',
errors.MissingIndexPattern);
};
inherits(errors.MissingIndexPattern, CourierError);
};
});

View file

@ -0,0 +1,10 @@
define(function (require) {
return function PendingRequestList() {
/**
* Queue of pending requests, requests are removed as
* they are processed by fetch.[sourceType]().
* @type {Array}
*/
return [];
};
});

View file

@ -1,23 +0,0 @@
define(function (require) {
return function (start, end, interval, pattern) {
if (end.isBefore(start)) {
throw new Error('Start must begin before end.');
}
if (!~['hour', 'day', 'week', 'year'].indexOf(interval)) {
throw new Error('Interval must be hour, day, week, or year.');
}
if (!pattern) {
throw new Error('Pattern can not be empty.');
}
var data = [];
while (start.isBefore(end)) {
start.add(interval, '1');
data.push(start.format(pattern));
}
return data;
};
});

View file

@ -1,171 +1,122 @@
define(function (require) {
var _ = require('lodash');
require('./data_source/doc');
require('./data_source/search');
require('./fetch/fetch');
require('./errors');
require('./looper');
require('services/es');
require('services/promises');
var module = require('modules').get('kibana/courier');
module.service('courier', [
'es',
'$rootScope',
'couriersFetch',
'Promise',
'Looper',
'couriersErrors',
'CouriersMapper',
'CouriersDocSource',
'CouriersSearchSource',
function (client, $rootScope, fetch, Promise, Looper, errors, Mapper, DocSource, SearchSource) {
var HastyRefresh = errors.HastyRefresh;
module.service('courier', function ($rootScope, Private, Promise) {
var DocSource = Private(require('./data_source/doc_source'));
var SearchSource = Private(require('./data_source/search_source'));
function Courier() {
var courier = this;
courier.errors = errors;
var SavedObject = Private(require('./saved_object/saved_object'));
/**
* Queue of pending requests, requests are removed as
* they are processed by fetch.[sourceType]().
* @type {Array}
*/
courier._pendingRequests = [];
var errors = Private(require('./_errors'));
var pendingRequests = Private(require('./_pending_requests'));
var searchLooper = Private(require('./looper/search'));
var docLooper = Private(require('./looper/doc'));
var indexPatterns = Private(require('./index_patterns/index_patterns'));
/**
* Queue of pending error handlers, they are removed as
* they are resolved.
* @type {Array}
*/
courier._errorHandlers = [];
var HastyRefresh = errors.HastyRefresh;
/**
* Fetch the docs
* @type {function}
*/
var processDocRequests = _.partial(fetch.docs, courier);
function Courier() {
var courier = this;
/**
* Fetch the search requests
* @type {function}
*/
var processSearchRequests = (function () {
// track the currently executing search resquest
var _activeAutoSearch = null;
// expose some internal modules
courier.errors = errors;
courier.SavedObject = SavedObject;
courier.indexPatterns = indexPatterns;
return function () {
// fatal if refreshes take longer then the refresh interval
if (_activeAutoSearch) Promise.rejected(new HastyRefresh());
return _activeAutoSearch = fetch.searches(courier).finally(function (res) {
_activeAutoSearch = null;
});
};
}());
/**
* update the time between automatic search requests
*
* @chainable
*/
courier.fetchInterval = function (ms) {
searchLooper.ms(ms);
return this;
};
/**
* The Looper which will manage the doc fetch interval
* @type {Looper}
*/
var docInterval = new Looper().fn(processDocRequests).start();
/**
* Start fetching search requests on an interval
* @chainable
*/
courier.start = function () {
searchLooper.start();
return this;
};
/**
* The Looper which will manage the doc fetch interval
* @type {Looper}
*/
var searchInterval = new Looper().fn(processSearchRequests);
/**
* Instance of Mapper, helps dataSources figure out their fields
* @type {Mapper}
*/
courier._mapper = new Mapper(courier);
/**
* update the time between automatic search requests
*
* @chainable
*/
courier.fetchInterval = function (ms) {
searchInterval.ms(ms);
return this;
};
/**
* Start fetching search requests on an interval
* @chainable
*/
courier.start = function () {
searchInterval.start();
return this;
};
/**
* Process the pending request queue right now, returns
* a promise that resembles the success of the fetch completing,
* individual errors are routed to their respectiv requests.
*/
courier.fetch = function () {
return processSearchRequests();
};
/**
* Process the pending request queue right now, returns
* a promise that resembles the success of the fetch completing,
* individual errors are routed to their respectiv requests.
*/
courier.fetch = function () {
return searchLooper.run();
};
/**
* is the currior currently fetching search
* results automatically?
*
* @return {boolean}
*/
courier.started = function () {
return searchInterval.started();
};
/**
* is the currior currently fetching search
* results automatically?
*
* @return {boolean}
*/
courier.started = function () {
return searchLooper.started();
};
/**
* stop the courier from fetching more search
* results, does not stop vaidating docs.
*
* @chainable
*/
courier.stop = function () {
searchInterval.stop();
return this;
};
/**
* stop the courier from fetching more search
* results, does not stop vaidating docs.
*
* @chainable
*/
courier.stop = function () {
searchLooper.stop();
return this;
};
/**
* create a source object that is a child of this courier
*
* @param {string} type - the type of Source to create
*/
courier.createSource = function (type) {
switch (type) {
case 'doc':
return new DocSource(courier);
case 'search':
return new SearchSource(courier);
}
};
/**
* create a source object that is a child of this courier
*
* @param {string} type - the type of Source to create
*/
courier.createSource = function (type) {
switch (type) {
case 'doc':
return new DocSource();
case 'search':
return new SearchSource();
}
};
/**
* Abort all pending requests
* @return {[type]} [description]
*/
courier.close = function () {
[].concat(this._pendingRequests.splice(0), this._errorHandlers.splice(0))
.forEach(function (req) {
req.defer.reject(new errors.Abort());
});
courier.getFieldsFor = function (indexish) {
return indexPatterns.getFieldsFor(indexish);
};
if (this._pendingRequests.length) {
throw new Error('Aborting all pending requests failed.');
}
};
}
/**
* Abort all pending requests
* @return {[type]} [description]
*/
courier.close = function () {
searchLooper.stop();
docLooper.stop();
return new Courier();
[].concat(pendingRequests.splice(0), this._errorHandlers.splice(0))
.forEach(function (req) {
req.defer.reject(new errors.Abort());
});
if (pendingRequests.length) {
throw new Error('Aborting all pending requests failed.');
}
};
}
]);
return new Courier();
});
});

View file

@ -1,14 +1,14 @@
define(function (require) {
var inherits = require('utils/inherits');
var _ = require('lodash');
var Mapper = require('courier/mapper');
var nextTick = require('utils/next_tick');
var module = require('modules').get('kibana/courier');
return function SourceAbstractFactory(Private, Promise, timefilter) {
var pendingRequests = Private(require('../_pending_requests'));
var errorHandlers = Private(require('../_error_handlers'));
var fetch = Private(require('../fetch/fetch'));
module.factory('CouriersSourceAbstract', function (couriersFetch, Promise, timefilter) {
function SourceAbstract(courier, initialState) {
function SourceAbstract(initialState) {
this._state = (function () {
// state can be serialized as JSON, and passed back in to restore
if (initialState) {
@ -23,7 +23,6 @@ define(function (require) {
}());
this._dynamicState = this._dynamicState || {};
this._courier = courier;
// get/set internal state values
this._methods.forEach(function (name) {
@ -69,43 +68,13 @@ define(function (require) {
return this;
};
/**
* Set the courier for this dataSource
* @chainable
* @param {Courier} newCourier
*/
SourceAbstract.prototype.courier = function (newCourier) {
this._courier = newCourier;
return this;
};
/**
* Create a new dataSource object of the same type
* as this, which inherits this dataSource's properties
* @return {SourceAbstract}
*/
SourceAbstract.prototype.extend = function () {
return this._courier
.createSource(this._getType())
.inherits(this);
};
/**
* fetch the field names for this SourceAbstract
* @param {Function} cb
* @callback {Error, Array} - calls cb with a possible error or an array of field names
*/
SourceAbstract.prototype.getFields = function () {
return this._courier._mapper.getFields(this);
};
/**
* clear the field list cache
* @param {Function} cb
* @callback {Error, Array} - calls cb with a possible error
*/
SourceAbstract.prototype.clearFieldCache = function () {
return this._courier._mapper.clearCache(this);
return (new this.Class()).inherits(this);
};
/**
@ -132,7 +101,7 @@ define(function (require) {
SourceAbstract.prototype.onResults = function (handler) {
var source = this;
return new Promise.emitter(function (resolve, reject, defer) {
source._courier._pendingRequests.push({
pendingRequests.push({
source: source,
defer: defer
});
@ -147,7 +116,7 @@ define(function (require) {
*/
SourceAbstract.prototype.onError = function () {
var defer = Promise.defer();
this._courier._errorHandlers.push({
errorHandlers.push({
source: this,
defer: defer
});
@ -159,15 +128,14 @@ define(function (require) {
* @param {Function} cb - callback
*/
SourceAbstract.prototype.fetch = function () {
var courier = this._courier;
var source = this;
return couriersFetch[this._getType()](this)
return fetch[this._getType()](this)
.then(function (res) {
courier._pendingRequests.splice(0).forEach(function (req) {
pendingRequests.splice(0).forEach(function (req) {
if (req.source === source) {
req.defer.resolve(_.cloneDeep(res));
} else {
courier._pendingRequests.push(req);
pendingRequests.push(req);
}
});
return res;
@ -179,8 +147,8 @@ define(function (require) {
* @return {undefined}
*/
SourceAbstract.prototype.cancelPending = function () {
var pending = _.where(this._courier._pendingRequests, { source: this});
_.pull.apply(_, [this._courier._pendingRequests].concat(pending));
var pending = _.where(pendingRequests, { source: this});
_.pull.apply(_, [pendingRequests].concat(pending));
};
/**
@ -216,9 +184,17 @@ define(function (require) {
// call the ittr and return it's promise
return (function ittr(resolve, reject) {
// itterate the _state object (not array) and
// pass each key:value pair to collect prop.
return Promise.all(_.map(current._state, function (value, key) {
return root._mergeProp(flatState, value, key);
// pass each key:value pair to source._mergeProp. if _mergeProp
// returns a promise, then wait for it to complete and call _mergeProp again
return Promise.all(_.map(current._state, function ittr(value, key) {
if (Promise.is(value)) {
return value.then(function (value) {
return ittr(value, key);
});
}
var prom = root._mergeProp(flatState, value, key);
return Promise.is(prom) ? prom : null;
}))
.then(function () {
// move to this sources parent
@ -261,5 +237,5 @@ define(function (require) {
return SourceAbstract;
});
};
});

View file

@ -1,7 +1,9 @@
define(function (require) {
var _ = require('lodash');
return function (Promise, es) {
return function (Promise, Private, es) {
var errors = Private(require('../_errors'));
var pendingRequests = Private(require('../_pending_requests'));
/**
* Backend for doUpdate and doIndex
@ -10,7 +12,7 @@ define(function (require) {
* of the the docs current version be sent to es?
* @param {String} body - HTTP request body
*/
return function (courier, method, validateVersion, body) {
return function (method, validateVersion, body) {
var doc = this;
// straight assignment will causes undefined values
var params = _.pick(this._state, ['id', 'type', 'index']);
@ -23,7 +25,7 @@ define(function (require) {
return es[method](params)
.then(function (resp) {
if (resp.status === 409) throw new courier.errors.VersionConflict(resp);
if (resp.status === 409) throw new errors.VersionConflict(resp);
doc._storeVersion(resp._version);
doc.id(resp._id);
@ -40,12 +42,9 @@ define(function (require) {
// use the key to compair sources
var key = doc._versionKey();
// filter out the matching requests from the _pendingRequests queue
var pending = courier._pendingRequests;
// clear the queue and filter out the removed items, pushing the
// unmatched ones back in.
pending.splice(0).filter(function (req) {
pendingRequests.splice(0).filter(function (req) {
var isDoc = req.source._getType() === 'doc';
var keyMatches = isDoc && req.source._versionKey() === key;
@ -56,7 +55,7 @@ define(function (require) {
}
// otherwise, put the request back into the queue
pending.push(req);
pendingRequests.push(req);
});
});
@ -64,7 +63,7 @@ define(function (require) {
})
.catch(function (err) {
// cast the error
throw new courier.errors.RequestFailure(err);
throw new errors.RequestFailure(err);
});
};
};

View file

@ -3,25 +3,22 @@ define(function (require) {
var inherits = require('utils/inherits');
require('./abstract');
return function DocSourceFactory(Private, Promise, es) {
var errors = Private(require('../_errors'));
var sendToEs = Private(require('./_doc_send_to_es'));
var SourceAbstract = Private(require('./_abstract'));
var module = require('modules').get('kibana/courier');
var VersionConflict = errors.VersionConflict;
var RequestFailure = errors.RequestFailure;
module.factory('CouriersDocSource', function (couriersErrors, CouriersSourceAbstract, Promise, es, $injector) {
var VersionConflict = couriersErrors.VersionConflict;
var RequestFailure = couriersErrors.RequestFailure;
var sendToEs = $injector.invoke(require('./_doc_send_to_es'));
function DocSource(courier, initialState) {
CouriersSourceAbstract.call(this, courier, initialState);
function DocSource(initialState) {
SourceAbstract.call(this, initialState);
// move onResults over to onUpdate, because that makes more sense
this.onUpdate = this.onResults;
this.onResults = void 0;
this._sendToEs = sendToEs;
}
inherits(DocSource, CouriersSourceAbstract);
inherits(DocSource, SourceAbstract);
/*****
* PUBLIC API
@ -46,7 +43,7 @@ define(function (require) {
*/
DocSource.prototype.doUpdate = function (fields) {
if (!this._state.id) return this.doIndex(fields);
return this._sendToEs(this._courier, 'update', false, { doc: fields });
return sendToEs.call(this, 'update', false, { doc: fields });
};
/**
@ -55,7 +52,7 @@ define(function (require) {
* @return {[type]} [description]
*/
DocSource.prototype.doIndex = function (body) {
return this._sendToEs(this._courier, 'index', false, body);
return sendToEs.call(this, 'index', false, body);
};
/*****
@ -154,5 +151,5 @@ define(function (require) {
};
return DocSource;
});
};
});

View file

@ -2,19 +2,18 @@ define(function (require) {
var inherits = require('utils/inherits');
var _ = require('lodash');
require('./abstract');
return function SearchSourceFactory(Promise, Private) {
var errors = Private(require('../_errors'));
var SourceAbstract = Private(require('./_abstract'));
var module = require('modules').get('kibana/courier');
var FetchFailure = errors.FetchFailure;
var RequestFailure = errors.RequestFailure;
var MissingIndexPattern = errors.MissingIndexPattern;
module.factory('CouriersSearchSource', function (couriersErrors, CouriersSourceAbstract, Promise) {
var FetchFailure = couriersErrors.FetchFailure;
var RequestFailure = couriersErrors.RequestFailure;
function SearchSource(courier, initialState) {
CouriersSourceAbstract.call(this, courier, initialState);
function SearchSource(initialState) {
SourceAbstract.call(this, initialState);
}
inherits(SearchSource, CouriersSourceAbstract);
inherits(SearchSource, SourceAbstract);
/*****
* PUBLIC API
@ -40,6 +39,10 @@ define(function (require) {
'source'
];
SearchSource.prototype.extend = function () {
return (new SearchSource()).inherits(this);
};
/**
* Set a searchSource that this source should inherit from
* @param {SearchSource} searchSource - the parent searchSource
@ -58,15 +61,6 @@ define(function (require) {
return this._parent;
};
/**
* Get the mapping for the index of this SearchSource
* @return {Promise}
*/
SearchSource.prototype.getFields = function () {
return this._courier._mapper.getFields(this);
};
/**
* Temporarily prevent this Search from being fetched... not a fan but it's easy
*/
@ -105,8 +99,9 @@ define(function (require) {
SearchSource.prototype._mergeProp = function (state, val, key) {
if (typeof val === 'function') {
var source = this;
return Promise.cast(val(source)).then(function (res) {
return source._mergeProp(state, res, key);
return Promise.cast(val(this))
.then(function (newVal) {
return source._mergeProp(state, newVal, key);
});
}
@ -136,5 +131,5 @@ define(function (require) {
};
return SearchSource;
});
};
});

View file

@ -0,0 +1,29 @@
define(function (require) {
return function RequestErrorHandlerFactory(Private, Notifier) {
var pendingRequests = Private(require('../_pending_requests'));
var errorHandlers = Private(require('../_error_handlers'));
var notify = new Notifier({
location: 'Courier Fetch Error'
});
function RequestErrorHandler() {}
RequestErrorHandler.prototype.handle = function (req, error) {
pendingRequests.push(req);
var handlerCount = 0;
errorHandlers.splice(0).forEach(function (handler) {
if (handler.source !== req.source) return pendingRequests.push(handler);
handler.defer.resolve(error);
handlerCount++;
});
if (!handlerCount) {
notify.fatal(new Error('unhandled error ' + (error.stack || error.message)));
}
};
return RequestErrorHandler;
};
});

View file

@ -1,32 +1,18 @@
define(function (require) {
return function fetchService(Private, es, Promise, Notifier, $q) {
var _ = require('lodash');
var module = require('modules').get('kibana/courier');
var _ = require('lodash');
var docStrategy = require('./strategy/doc');
var searchStrategy = require('./strategy/search');
var docStrategy = require('./strategy/doc');
var searchStrategy = require('./strategy/search');
var errors = Private(require('../_errors'));
var RequestErrorHandler = Private(require('./_request_error_handler'));
var pendingRequests = Private(require('../_pending_requests'));
module.service('couriersFetch', function (es, Promise, couriersErrors, createNotifier, $q) {
var notify = createNotifier({
var notify = new Notifier({
location: 'Courier Fetch'
});
function RequestErrorHandler(courier) { this._courier = courier; }
RequestErrorHandler.prototype.handle = function (req, error) {
if (!this._courier) return req.defer.reject(error);
this._courier._pendingRequests.push(req);
var handlerCount = 0;
this._courier._errorHandlers.splice(0).forEach(function (handler) {
if (handler.source !== req.source) return this._courier._pendingRequests.push(handler);
handler.defer.resolve(error);
handlerCount++;
});
if (!handlerCount) {
notify.fatal(new Error('unhandled error ' + (error.stack || error.message)));
this._courier.stop();
}
};
var fetchThese = function (strategy, requests, reqErrHandler) {
var all, body;
@ -47,7 +33,7 @@ define(function (require) {
.then(function (resp) {
strategy.getResponses(resp).forEach(function (resp) {
var req = all.shift();
if (resp.error) return reqErrHandler.handle(req, new couriersErrors.FetchFailure(resp));
if (resp.error) return reqErrHandler.handle(req, new errors.FetchFailure(resp));
else strategy.resolveRequest(req, resp);
});
@ -63,10 +49,10 @@ define(function (require) {
}, notify.fatal);
};
var fetchPending = function (strategy, courier) {
var requests = strategy.getPendingRequests(courier._pendingRequests);
var fetchPending = function (strategy) {
var requests = strategy.getPendingRequests(pendingRequests);
if (!requests.length) return Promise.resolved();
else return fetchThese(strategy, requests, new RequestErrorHandler(courier));
else return fetchThese(strategy, requests, new RequestErrorHandler());
};
var fetchASource = function (strategy, source) {
@ -109,5 +95,5 @@ define(function (require) {
* @async
*/
this.search = _.partial(fetchASource, searchStrategy);
});
};
});

View file

@ -0,0 +1,30 @@
define(function (require) {
return function CastMappingTypeFn() {
/**
* Accepts a mapping type, and converts it into it's js equivilent
* @param {String} type - the type from the mapping's 'type' field
* @return {String} - the most specific type that we care for
*/
return function castMappingType(type) {
switch (type) {
case 'float':
case 'double':
case 'integer':
case 'long':
case 'short':
case 'byte':
case 'token_count':
return 'number';
case 'date':
case 'boolean':
case 'ip':
case 'attachment':
case 'geo_point':
case 'geo_shape':
return type;
default: // including 'string'
return 'string';
}
};
};
});

View file

@ -0,0 +1,55 @@
define(function (require) {
var inherits = require('utils/inherits');
return function IndexPatternFactory(Private) {
var SavedObject = Private(require('../saved_object/saved_object'));
var mapper = Private(require('./_mapper'));
function IndexPattern(id) {
var pattern = this;
SavedObject.call(pattern, {
type: 'index-pattern',
id: id,
mapping: {
title: 'string',
timeFieldName: 'string',
fields: 'json'
},
defaults: {
title: id
},
afterESResp: function () {
if (pattern.id && !pattern.fields) {
return pattern.refreshFields();
}
},
searchSource: false
});
pattern.refreshFields = function () {
return mapper.clearCache(pattern.id)
.then(function () {
return mapper.getFieldsForIndexPattern(pattern.id);
})
.then(function (fields) {
pattern.fields = fields;
return pattern.save();
});
};
pattern.toJSON = function () {
return pattern.id;
};
pattern.toString = function () {
return '' + this.toJSON();
};
}
return IndexPattern;
};
});

View file

@ -1,11 +1,10 @@
define(function (require) {
var module = require('modules').get('kibana/courier');
var _ = require('lodash');
module.factory('LocalCache', function () {
return function LocalCacheFactory() {
function LocalCache(opts) {
var _id = opts.id || _.identity;
opts = opts || {};
var _id = opts.id || function (o) { return '' + o; };
var _cache = {};
this.get = function (obj) {
@ -30,6 +29,8 @@ define(function (require) {
delete _cache[id];
};
}
return LocalCache;
});
};
});

View file

@ -0,0 +1,61 @@
define(function (require) {
var _ = require('lodash');
return function MapperService(Private, Promise, es, configFile) {
var errors = Private(require('../_errors'));
var CacheWriteFailure = errors.CacheWriteFailure;
var MappingConflict = errors.MappingConflict;
var RestrictedMapping = errors.RestrictedMapping;
var FieldNotFoundInCache = errors.FieldNotFoundInCache;
// private function
var transformMappingIntoFields = Private(require('./_transform_mapping_into_fields'));
// private factory
var LocalCache = Private(require('./_local_cache'));
function Mapper() {
// Save a reference to mapper
var mapper = this;
// proper-ish cache, keeps a clean copy of the object, only returns copies of it's copy
var fieldCache = new LocalCache();
/**
* Gets an object containing all fields with their mappings
* @param {dataSource} dataSource
* @returns {Promise}
* @async
*/
mapper.getFieldsForIndexPattern = function (indexPattern) {
var cache;
if (cache = fieldCache.get(indexPattern)) return Promise.resolved(cache);
return es.indices.getFieldMapping({
// TODO: Change index to be the resolved in some way, last three months, last hour, last year, whatever
index: indexPattern,
field: '*',
})
.then(transformMappingIntoFields)
.then(function (fields) {
fieldCache.set(indexPattern, fields);
return fieldCache.get(indexPattern);
});
};
/**
* Clears mapping caches from elasticsearch and from local object
* @param {dataSource} dataSource
* @returns {Promise}
* @async
*/
mapper.clearCache = function (indexPattern) {
fieldCache.clear(indexPattern);
return Promise.resolved();
};
}
return new Mapper();
};
});

View file

@ -0,0 +1,52 @@
define(function (require) {
return function transformMappingIntoFields(Private, configFile) {
var _ = require('lodash');
var MappingConflict = Private(require('../_errors')).MappingConflict;
var castMappingType = Private(require('./_cast_mapping_type'));
var reservedFields = {
_id: { type: 'string'},
_type: { type: 'string' },
_index: { type: 'string' }
};
/**
* 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) {
var fields = _.cloneDeep(reservedFields);
_.each(response, function (index, indexName) {
if (indexName === configFile.kibanaIndex) return;
_.each(index.mappings, function (mappings, typeName) {
_.each(mappings, function (field, name) {
var keys = Object.keys(field.mapping);
if (keys.length === 0 || name[0] === '_') return;
var mapping = _.cloneDeep(field.mapping[keys.shift()]);
mapping.type = castMappingType(mapping.type);
if (fields[name]) {
if (fields[name].type !== mapping.type) {
throw new MappingConflict(name);
}
return;
}
fields[name] = mapping;
});
});
});
return _.map(fields, function (mapping, name) {
mapping.name = name;
return mapping;
});
};
};
});

View file

@ -0,0 +1,55 @@
define(function (require) {
return function IndexPatternsService(configFile, es, Notifier, Private, Promise) {
var indexPatterns = this;
var _ = require('lodash');
var IndexPattern = Private(require('./_index_pattern'));
var SourceAbstract = Private(require('../data_source/_abstract'));
var mapper = Private(require('./_mapper'));
var notify = new Notifier({ location: 'IndexPatterns Service'});
indexPatterns.get = _.memoize(function (id) {
return (new IndexPattern(id)).init();
});
indexPatterns.getIds = function () {
return es.search({
index: configFile.kibanaIndex,
type: 'index-pattern',
fields: [],
body: {
query: { match_all: {} }
}
})
.then(function (resp) {
return _.pluck(resp.hits.hits, '_id');
});
};
indexPatterns.delete = function (pattern) {
return es.delete({
index: configFile.kibanaIndex,
type: 'index-pattern',
id: pattern.id
});
};
indexPatterns.getFieldsFor = function (indexish) {
// pull the index string out of Source objects
if (indexish instanceof SourceAbstract) {
indexish = indexish.get('index');
}
return Promise.cast(
(typeof indexish === 'object')
? indexish
: indexPatterns.get(indexish)
)
.then(function (indexPattern) {
return indexPattern.fields;
});
};
};
});

View file

@ -1,9 +1,7 @@
define(function (require) {
var _ = require('lodash');
var module = require('modules').get('kibana/courier');
module.factory('Looper', function ($timeout) {
return function LooperFactory($timeout, Notifier) {
var _ = require('lodash');
var notify = new Notifier();
function Looper(ms, fn) {
var _ms = ms === void 0 ? 1500 : ms;
@ -108,8 +106,15 @@ define(function (require) {
_timerId = _ms ? $timeout(looper._looperOver, _ms) : null;
};
/**
* execute the _fn, and restart the timer
*/
looper.run = function () {
looper.start();
};
}
return Looper;
});
};
});

View file

@ -0,0 +1,14 @@
define(function (require) {
return function DocLooperService(Private) {
var fetch = Private(require('../fetch/fetch'));
var Looper = Private(require('./_looper'));
/**
* The Looper which will manage the doc fetch interval
* @type {Looper}
*/
var docLooper = new Looper(1500, fetch.docs).start();
return docLooper;
};
});

View file

@ -0,0 +1,24 @@
define(function (require) {
return function SearchLooperService(Private, Promise) {
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;
/**
* The Looper which will manage the doc fetch interval
* @type {Looper}
*/
var searchLooper = new Looper(null, function () {
// fatal if refreshes take longer then the refresh interval
if (_activeAutoSearch) Promise.rejected(new errors.HastyRefresh());
return _activeAutoSearch = fetch.searches().finally(function (res) {
_activeAutoSearch = null;
});
}).start();
return searchLooper;
};
});

View file

@ -1,187 +0,0 @@
define(function (require) {
var _ = require('lodash');
require('./local_cache');
// these fields are the only _ prefixed fields that will
// be found in the field list. All others are filtered
var reservedFields = {
_id: { type: 'string' },
_type: { type: 'string' },
_index: { type: 'string' }
};
var module = require('modules').get('kibana/courier');
module.factory('CouriersMapper', function (Promise, es, configFile, LocalCache, couriersErrors) {
var CacheWriteFailure = couriersErrors.CacheWriteFailure;
var MappingConflict = couriersErrors.MappingConflict;
var RestrictedMapping = couriersErrors.RestrictedMapping;
var FieldNotFoundInCache = couriersErrors.FieldNotFoundInCache;
function CouriersMapper(courier) {
// Save a reference to mapper
var mapper = this;
var fieldCache = new LocalCache({
id: function (dataSource) {
return dataSource.get('index');
}
});
/**
* Gets an object containing all fields with their mappings
* @param {dataSource} dataSource
* @async
*/
mapper.getFields = function (dataSource) {
if (!dataSource.get('index')) {
// always callback async
return Promise.rejected(new TypeError('dataSource must have an index before it\'s fields can be fetched'));
}
return mapper.getCachedFieldsFor(dataSource)
.catch(function () {
// If we are unable to get the fields from cache, get them from mapping instead
return mapper.getFieldsFromEsFor(dataSource);
});
};
/**
* Gets an object containing all fields with their mappings from kibana's cache in Elasticsearch
* @param {dataSource} dataSource
* @param {Function} callback A function to be executed with the results.
*/
mapper.getCachedFieldsFor = function (dataSource) {
// If we already have the fields in the local cache, use those
var cached = fieldCache.get(dataSource);
if (cached) return Promise.resolved(cached);
return es.getSource({
index: configFile.kibanaIndex,
type: 'mapping',
id: dataSource.get('index'),
}).then(function (fields) {
fieldCache.set(dataSource, fields);
return fieldCache.get(dataSource);
});
};
/**
* Gets an object containing all fields with their mappings directly from Elasticsearch _mapping API
* @param {dataSource} dataSource
* @param {Function} callback A function to be executed with the results.
*/
mapper.getFieldsFromEsFor = function (dataSource, callback) {
return es.indices.getFieldMapping({
// TODO: Change index to be the resolved in some way, last three months, last hour, last year, whatever
index: dataSource.get('index'),
field: '*',
})
.then(transformFieldMappingResponse)
.then(function (fields) {
return mapper.writeFieldsToCaches(dataSource, fields);
})
.then(function () {
return fieldCache.get(dataSource);
});
};
/**
* Stores processed mappings in Elasticsearch, and in local cache
* @param {dataSource} dataSource
* @param {Function} callback A function to be executed with the results.
* @async
*/
mapper.writeFieldsToCaches = function (dataSource, fields) {
return es.index({
index: configFile.kibanaIndex,
type: 'mapping',
id: dataSource.get('index'),
body: fields
})
.then(function () {
fieldCache.set(dataSource, fields);
});
};
/**
* Clears mapping caches from elasticsearch and from local object
* @param {dataSource} dataSource
* @param {Function} callback A function to be executed with the results.
*/
mapper.clearCache = function (dataSource) {
fieldCache.clear(dataSource);
return es.delete({
index: configFile.kibanaIndex,
type: 'mapping',
id: dataSource.get('index')
});
};
/**
* 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
*/
var transformFieldMappingResponse = function (response) {
var fields = _.cloneDeep(reservedFields);
_.each(response, function (index, indexName) {
if (indexName === configFile.kibanaIndex) return;
_.each(index.mappings, function (mappings, typeName) {
_.each(mappings, function (field, name) {
if (_.size(field.mapping) === 0 || name[0] === '_') return;
var mapping = field.mapping[_.keys(field.mapping)[0]];
mapping.type = castMappingType(mapping.type);
if (fields[name]) {
if (fields[name].type !== mapping.type) {
throw new MappingConflict(name);
}
return;
}
fields[name] = mapping;
});
});
});
return fields;
};
/**
* Accepts a mapping type, and converts it into it's js equivilent
* @param {String} type - the type from the mapping's 'type' field
* @return {String} - the most specific type that we care for
*/
var castMappingType = function (type) {
switch (type) {
case 'float':
case 'double':
case 'integer':
case 'long':
case 'short':
case 'byte':
case 'token_count':
return 'number';
case 'date':
case 'boolean':
case 'ip':
case 'attachment':
case 'geo_point':
case 'geo_shape':
return type;
default: // including 'string'
return 'string';
}
};
}
return CouriersMapper;
});
});

View file

@ -1,11 +1,7 @@
define(function () {
var _ = require('lodash');
/**
* Create the mappingSetup module by passing in it's dependencies.
*/
return function (configFile, es, courier) {
var mappingSetup = {};
return function MappingSetupService(configFile, es) {
var _ = require('lodash');
var mappingSetup = this;
/**
* Use to create the mappings, but that should only happen one at a time
@ -81,8 +77,6 @@ define(function () {
activeTypeCreations[type] = prom;
return prom;
};
return mappingSetup;
};
});

View file

@ -0,0 +1,24 @@
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,12 +1,21 @@
define(function (require) {
var module = require('modules').get('kibana/saved_object');
var _ = require('lodash');
require('services/root_search');
return function SavedObjectFactory(configFile, Promise, Private, Notifier) {
var DocSource = Private(require('../data_source/doc_source'));
var SearchSource = Private(require('../data_source/search_source'));
module.factory('SavedObject', function (courier, configFile, rootSearch, Promise, createNotifier, $injector) {
var errors = Private(require('../_errors'));
var mappingSetup = Private(require('./_mapping_setup'));
var mappingSetup = $injector.invoke(require('./_mapping_setup'));
var json = {
_serialize: function (val) {
if (val != null) return JSON.stringify(val);
},
_deserialize: function (val) {
if (val != null) return JSON.parse(val);
}
};
function SavedObject(config) {
if (!_.isObject(config)) config = {};
@ -18,18 +27,30 @@ define(function (require) {
* Initialize config vars
************/
// the doc which is used to store this object
var docSource = courier.createSource('doc');
var docSource = new DocSource();
// type name for this object, used as the ES-type
var type = config.type;
// Create a notifier for sending alerts
var notify = createNotifier({
var notify = new Notifier({
location: 'Saved ' + type
});
// mapping definition for the fields that this object will expose
var mapping = config.mapping || {};
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;
});
// default field values, assigned when the source is loaded
var defaults = config.defaults || {};
@ -38,7 +59,7 @@ define(function (require) {
var customInit = config.init || _.noop;
// optional search source which this object configures
obj.searchSource = config.searchSource && courier.createSource('search');
obj.searchSource = config.searchSource && new SearchSource();
// the id of the document
obj.id = config.id || void 0;
@ -51,8 +72,6 @@ define(function (require) {
* @resolved {SavedObject}
*/
obj.init = _.once(function () {
customInit();
// ensure that the type is defined
if (!type) throw new Error('You must define a type name to use SavedObject objects.');
@ -64,7 +83,7 @@ define(function (require) {
// by default, the search source should inherit from the rootSearch
if (obj.searchSource) {
obj.searchSource.inherits(rootSearch);
obj.searchSource.inherits(SavedObject.rootSearch());
}
// check that the mapping for this type is defined
@ -73,16 +92,6 @@ define(function (require) {
// if it is already defined skip this step
if (defined) return true;
// we need to setup the mapping, flesh it out first
var mapping = _.mapValues(mapping, function (val, prop) {
// allow shortcuts for the field types, by just setting the value
// to the type name
if (typeof val !== 'string') return val;
return {
type: val
};
});
mapping.kibanaSavedObjectMeta = {
properties: {
// setup the searchSource mapping, even if it is not used but this type yet
@ -106,7 +115,7 @@ define(function (require) {
// fetch the object from ES
return docSource.fetch()
.then(function applyESResp(resp) {
if (!resp.found) throw new courier.errors.SavedObjectNotFound(type);
if (!resp.found) throw new errors.SavedObjectNotFound(type);
var meta = resp._source.kibanaSavedObjectMeta || {};
delete resp._source.kibanaSavedObjectMeta;
@ -114,6 +123,13 @@ define(function (require) {
// assign the defaults to the response
_.defaults(resp._source, defaults);
// transform the source using _deserializers
_.forOwn(mapping, function ittr(fieldMapping, fieldName) {
if (fieldMapping._deserialize) {
resp._source[fieldName] = fieldMapping._deserialize(resp._source[fieldName], resp, fieldName, fieldMapping);
}
});
// Give obj all of the values in _source.fields
_.assign(obj, resp._source);
@ -133,6 +149,9 @@ define(function (require) {
});
});
})
.then(function () {
return customInit.call(obj);
})
.then(function () {
// return our obj as the result of init()
return obj;
@ -151,7 +170,9 @@ define(function (require) {
_.forOwn(mapping, function (fieldMapping, fieldName) {
if (obj[fieldName] != null) {
body[fieldName] = obj[fieldName];
body[fieldName] = (fieldMapping._serialize)
? fieldMapping._serialize(obj[fieldName])
: obj[fieldName];
}
});
@ -166,9 +187,11 @@ define(function (require) {
// index the document
return docSource.doIndex(body).then(function (id) {
// ensure that the object has the potentially new id
obj.id = id;
return id;
})
.then(function () {
// ensure that the object has the potentially new id
return obj.id;
});
};
@ -184,6 +207,12 @@ define(function (require) {
}
SavedObject.rootSearch = function () {
var rootSearch = Private(require('./_root_search'))();
SavedObject.rootSearch = function () { return rootSearch; };
return rootSearch;
};
return SavedObject;
});
};
});

View file

@ -4,7 +4,7 @@ define(function (require) {
var $ = require('jquery');
var modules = require('modules');
var module = modules.get('kibana/notify');
var errors = require('./errors');
var errors = require('./_errors');
var Notifier = require('./_notifier');
var rootNotifier = new Notifier();

View file

@ -11,6 +11,7 @@ define(function (require) {
require('notify/notify');
require('directives/info');
require('angular-bootstrap');
require('utils/private');
var modules = require('modules').get('kibana/controllers', ['ui.bootstrap']);

View file

@ -1,8 +1,8 @@
define(function (require) {
var module = require('modules').get('kibana/saved_object');
var module = require('modules').get('kibana/directives');
var _ = require('lodash');
module.directive('savedObjectFinder', function (savedSearches, savedVisualizations, savedDashboards, indexPatterns, $parse) {
module.directive('savedObjectFinder', function (savedSearches, savedVisualizations, savedDashboards) {
var vars = {
searches: {
service: savedSearches,
@ -16,11 +16,7 @@ define(function (require) {
dashboards: {
service: savedDashboards,
noun: 'Dashboard'
},
indexPatterns: {
service: indexPatterns,
noun: 'Index Pattern'
},
}
};
return {
@ -31,7 +27,7 @@ define(function (require) {
makeUrl: '=?',
onChoose: '=?'
},
template: require('text!./_finder.html'),
template: require('text!../partials/saved_object_finder.html'),
link: function ($scope, $el) {
// the text input element
var $input = $el.find('input[ng-model=filter]');
@ -158,7 +154,9 @@ define(function (require) {
};
}()));
$scope.$on('$destroy', _.bindKey($input, 'off', 'keydown'));
$scope.$on('$destroy', function () {
$input.off('keydown');
});
function filterResults() {
if (!service) return;

View file

@ -6,7 +6,6 @@ require.config({
courier: './components/courier',
config: './components/config',
notify: './components/notify',
saved_object: './components/saved_object',
state_management: './components/state_management',
// special utils

View file

@ -49,7 +49,7 @@ define(function (require) {
return defer.promise;
}
Promise.all = $q.all;
Promise.all = Promise.props = $q.all;
Promise.resolved = function (val) {
var defer = $q.defer();
defer.resolve(val);
@ -70,6 +70,11 @@ define(function (require) {
Promise.map = function (arr, fn) {
return Promise.all(arr.map(fn));
};
Promise.is = function (obj) {
// $q doesn't create instances of any constructor, promises are just objects with a then function
// https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335
return obj && typeof obj.then === 'function';
};
/**
* Create a promise that uses our "event" like pattern.

View file

@ -1,15 +0,0 @@
define(function (require) {
var module = require('modules').get('kibana/services');
module.service('rootSearch', function (courier, config, timefilter, indexPatterns) {
return courier.createSource('search')
.index(config.get('defaultIndex'))
.filter(function (source) {
return source.getFields()
.then(function (fields) {
// dynamic time filter will be called in the _flatten phase of things
return timefilter.get(fields);
});
});
});
});

View file

@ -26,16 +26,16 @@ define(function (require) {
return enable;
};
this.get = function (fieldHash) {
this.get = function (indexPattern) {
var timefield, filter;
// TODO: time field should be stored in the pattern meta data. For now we just use the first date field we find
timefield = _.findKey(fieldHash, {type: 'date'});
timefield = _.find(indexPattern.fields, {type: 'date'});
var bounds = this.getBounds();
if (!!timefield) {
filter = {range : {}};
filter.range[timefield] = {
filter.range[timefield.name] = {
gte: bounds.min,
lte: bounds.max
};

View file

@ -1,4 +1,3 @@
@import "../../bower_components/K4D3/k4.d3.css";
@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic");
@import "icon_font.css";
/*!

View file

@ -1,5 +1,4 @@
@import "../../bower_components/font-awesome/less/font-awesome.less";
@import "../../bower_components/K4D3/k4.d3.css";
// Custom variables
@import "./_variables.less";

View file

@ -38,6 +38,13 @@ define(function (require) {
});
}(nestedObj));
return flatObj;
},
// assign the properties of an object's subObject to the parent object.
// obj = { prop: { a: 1} } ===> obj = { a: 1 }
unwrapProp: function (obj, prop) {
var wrapped = obj[prop];
delete obj[prop];
_.assign(obj, wrapped);
}
});

View file

@ -0,0 +1,35 @@
define(function (require) {
/**
* create private services and factories that can still use angular deps
* @type {[type]}
*/
var privPath = [];
var pathToString = function () {
return privPath.map(function (construct) {
return construct.name;
}).join(' -> ');
};
var module = require('modules').get('kibana/utils');
module.service('Private', function ($injector) {
return function Private(construct) {
if (typeof construct !== 'function') {
throw new TypeError('Expected private module "' + construct + '" to be a function');
}
var circular = !!(~privPath.indexOf(construct));
privPath.push(construct);
if (circular) throw new Error('Circluar private deps found: ' + pathToString());
if (!construct.$$instance) {
var instance = {};
construct.$$instance = $injector.invoke(construct, instance);
construct.$$instance = construct.$$instance || instance;
}
privPath.pop();
return construct.$$instance;
};
});
});

View file

@ -1,6 +1,6 @@
define(function (require) {
var SearchSource = require('courier/data_source/search');
var DocSource = require('courier/data_source/doc');
var SearchSource = require('courier/data_source/search_source');
var DocSource = require('courier/data_source/doc_source');
return function extendCourierSuite() {
inject(function (courier) {

View file

@ -1,33 +0,0 @@
define(function (require) {
require('angular-mocks');
describe('Courier Module', function () {
var HastyRefresh;
var courier;
before(function () {
inject(function (couriersErrors, _courier_) {
HastyRefresh = couriersErrors.HastyRefresh;
courier = _courier_;
});
});
afterEach(function () {
courier.close();
});
// describe('sync API', function () {
// require('./create_source')();
// require('./start_stop')();
// require('./calculate_indices')();
// require('./create_source')();
// require('./abort')();
// require('./on_fetch')();
// require('./source_merging')();
// });
// require('./data_source')();
// require('./doc_source')();
// require('./mapper')();
});
});

View file

@ -1,76 +0,0 @@
/*
define(function (require) {
var SearchSource = require('courier/data_source/search');
var DocSource = require('courier/data_source/doc');
var nextTick = require('utils/next_tick');
var sinon = require('test_utils/auto_release_sinon');
return function extendCourierSuite() {
describe('onFetch()', function () {
it('defers to the "fetch" method on the SearchSource class to do the fetch', function () {
sinon.stub(SearchSource, 'fetch');
courier.fetch('search');
expect(SearchSource.fetch.callCount).to.equal(1);
});
it('defers to the "validate" method on the DocSource class to determine which docs need fetching', function () {
sinon.stub(DocSource, 'validate');
var courier = createCourier();
courier.fetch('doc');
expect(DocSource.validate.callCount).to.equal(1);
});
it('when it receives refs from DocSource.validate, passes them back to DocSource.fetch', function (done) {
sinon.stub(DocSource, 'validate', function (courier, refs, cb) {
// just pass back the refs we receive
nextTick(cb, null, refs);
});
sinon.spy(DocSource, 'fetch');
var courier = createCourier({
client: stubbedClient(function (method, params, cb) {
cb(null, {
docs: [
{
found: true,
_version: 1,
_source: {}
}
]
});
})
});
courier
.createSource('doc')
.index('foo').type('bar').id('bax')
.on('results', function () {
done();
});
courier.fetch('doc');
expect(DocSource.validate.callCount).to.equal(1);
});
it('calls it\'s own fetch method when the interval is up and immediately schedules another fetch', function () {
var courier = createCourier();
var clock = sinon.useFakeTimers();
var count = 0;
sinon.stub(courier, 'fetch', function () {
count++;
});
courier.fetchInterval(10);
courier.start();
expect(count).to.eql(1);
clock.tick(10);
expect(count).to.eql(2);
});
});
};
});
*/