diff --git a/src/courier/data_source/data_source.js b/src/courier/data_source/data_source.js
index 1d20e5cc4cd75..8763d94626a41 100644
--- a/src/courier/data_source/data_source.js
+++ b/src/courier/data_source/data_source.js
@@ -24,6 +24,12 @@ define(function (require) {
this._state = state;
this._courier = courier;
+ // before newListener to prevent unnecessary "emit" when added
+ this.on('removeListener', function onRemoveListener() {
+ if (EventEmitter.listenerCount(this, 'results') > 0) return;
+ courier._closeDataSource(this);
+ });
+
this.on('newListener', function (name, listener) {
if (name !== 'results') return;
@@ -43,11 +49,6 @@ define(function (require) {
}
});
- this.on('removeListener', function onRemoveListener() {
- if (EventEmitter.listenerCount(this, 'results') > 0) return;
- courier._closeDataSource(this);
- });
-
this.extend = function () {
return courier
.createSource(this._getType())
diff --git a/src/courier/mapper.js b/src/courier/mapper.js
index 67d647648e505..87e15e4c1bf54 100644
--- a/src/courier/mapper.js
+++ b/src/courier/mapper.js
@@ -54,11 +54,11 @@ define(function (require) {
});
cacheFieldsToObject(dataSource, fields);
- callback(err, fields);
+ callback(err, self.getFieldsFromObject(dataSource));
});
} else {
cacheFieldsToObject(dataSource, fields);
- callback(err, fields);
+ callback(err, self.getFieldsFromObject(dataSource));
}
});
}
@@ -141,8 +141,15 @@ define(function (require) {
_.each(index.mappings, function (type) {
_.each(type, function (field, name) {
if (_.size(field.mapping) === 0 || name[0] === '_') return;
- if (!_.isUndefined(fields[name]) && fields[name].type !== field.mapping[_.keys(field.mapping)[0]].type)
+
+ var mapping = field.mapping[_.keys(field.mapping)[0]];
+ mapping.type = castMappingType(mapping.type);
+
+ if (fields[name]) {
+ if (fields[name].type === mapping.type) return;
return courier._error(new Error.MappingConflict(name));
+ }
+
fields[name] = field.mapping[_.keys(field.mapping)[0]];
});
});
@@ -181,6 +188,33 @@ define(function (require) {
return !_.isUndefined(mappings[dataSource._state.index]) ? true : false;
};
+ /**
+ * 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';
+ }
+ };
+
/**
* Clears mapping caches from elasticsearch and from local object
* @param {dataSource} dataSource
diff --git a/src/kibana/apps/discover/field_chooser.html b/src/kibana/apps/discover/field_chooser.html
new file mode 100644
index 0000000000000..ab27846749338
--- /dev/null
+++ b/src/kibana/apps/discover/field_chooser.html
@@ -0,0 +1,15 @@
+
-
Discover
-
\ No newline at end of file
diff --git a/src/kibana/apps/discover/index.js b/src/kibana/apps/discover/index.js
index 3e96fea96dd08..5fbc223b1dfac 100644
--- a/src/kibana/apps/discover/index.js
+++ b/src/kibana/apps/discover/index.js
@@ -1,11 +1,12 @@
-define(function (require) {
+define(function (require, module, exports) {
var angular = require('angular');
var _ = require('lodash');
require('directives/table');
- require('css!./styles/main.css');
+ require('./field_chooser');
+ require('services/saved_searches');
- var app = angular.module('app/discover', []);
+ var app = angular.module('app/discover');
var sizeOptions = [
{ display: '30', val: 30 },
@@ -13,7 +14,7 @@ define(function (require) {
{ display: '80', val: 80 },
{ display: '125', val: 125 },
{ display: '250', val: 250 },
- { display: 'Unlimited', val: null },
+ { display: '500', val: 500 }
];
var intervalOptions = [
@@ -25,20 +26,19 @@ define(function (require) {
{ display: 'Yearly', val: 'yearly' }
];
- app.controller('discover', function ($scope, courier, config) {
- var source = courier.rootSearchSource.extend()
- .size(30)
- .$scope($scope)
- .on('results', function (res) {
- if (!$scope.fields) getFields();
- $scope.rows = res.hits.hits;
- });
+ app.controller('discover', function ($scope, config, $q, $routeParams, savedSearches, courier) {
+ var source;
+ if ($routeParams.id) {
+ source = savedSearches.get($routeParams.id);
+ } else {
+ source = savedSearches.create();
+ }
// stores the complete list of fields
- $scope.fields = [];
+ $scope.fields = null;
// stores the fields we want to fetch
- $scope.columns = [];
+ $scope.columns = null;
// At what interval are your index patterns
$scope.intervalOptions = intervalOptions;
@@ -48,56 +48,128 @@ define(function (require) {
$scope.sizeOptions = sizeOptions;
$scope.size = $scope.sizeOptions[0];
- // watch the discover.defaultIndex config value for changes
+ // the index that will be
config.$watch('discover.defaultIndex', function (val) {
- if (!val) {
- config.set('discover.defaultIndex', '_all');
- return;
+ if (!val) return config.set('discover.defaultIndex', '_all');
+ if (!$scope.index) {
+ $scope.index = val;
+ $scope.fetch();
}
- // only set if datasource doesn't have an index
- if (!source.get('index')) $scope.index = val;
});
- $scope.$watch('index', function (val) {
- // set the index on the data source
- source.index(val);
- // clear the columns and fields, then refetch when we so a search
- $scope.columns = $scope.fields = null;
- });
+ source
+ .size(30)
+ .$scope($scope)
+ .inherits(courier.rootSearchSource)
+ .on('results', function (res) {
+ if (!$scope.fields) getFields();
+ $scope.rows = res.hits.hits;
+ });
- $scope.$watch('query', function (query) {
- if (query) {
- source.query({
+ $scope.fetch = function () {
+ if (!$scope.fields) getFields();
+ source
+ .size($scope.size.val)
+ .query(!$scope.query ? null : {
query_string: {
- query: query
+ query: $scope.query
}
+ })
+ .source(!$scope.columns ? null : {
+ include: $scope.columns
});
- } else {
- // clear the query
- source.query(null);
+
+ if ($scope.sort) {
+ var sort = {};
+ sort[$scope.sort.name] = 'asc';
+ source.sort(sort);
}
- });
- $scope.$watch('size', function (selectedSize) {
- source.size(selectedSize.val);
- });
+ if ($scope.index !== source.get('index')) {
+ // set the index on the data source
+ source.index($scope.index);
+ // clear the columns and fields, then refetch when we so a search
+ $scope.columns = $scope.fields = null;
+ }
- $scope.reset = function () {
- // the check happens only when the results come in; prevents a race condition
- // if (!$scope.fields) getFields();
- courier.abort();
- courier.fetch();
+ // fetch just this datasource
+ source.fetch();
};
+ var activeGetFields;
function getFields() {
- source.getFields(function (err, fields) {
- $scope.fields = fields;
- $scope.columns = _.keys(fields);
- source.source({
- include: $scope.columns
+ var defer = $q.defer();
+
+ if (!source.get('index')) {
+ // Without an index there is nothing to do here.
+ defer.resolve();
+ return defer.promise;
+ }
+
+ if (activeGetFields) {
+ activeGetFields.then(function () {
+ defer.resolve();
});
+ return;
+ }
+
+ var currentState = _.transform($scope.fields || [], function (current, field) {
+ current[field.name] = {
+ hidden: field.hidden
+ };
+ }, {});
+
+ source
+ .getFields()
+ .then(function (fields) {
+ $scope.fields = [];
+ $scope.columns = [];
+
+ _(fields)
+ .keys()
+ .sort()
+ .each(function (name) {
+ var field = fields[name];
+ field.name = name;
+ _.defaults(field, currentState[name]);
+
+ if (!field.hidden) $scope.columns.push(name);
+ $scope.fields.push(field);
+ });
+
+ defer.resolve();
+ }, defer.reject);
+
+ return defer.promise.then(function () {
+ activeGetFields = null;
});
}
+ $scope.toggleField = function (name) {
+ var field = _.find($scope.fields, { name: name });
+
+ // toggle the hidden property
+ field.hidden = !field.hidden;
+
+ // collect column names for non-hidden fields and sort
+ $scope.columns = _.transform($scope.fields, function (cols, field) {
+ if (!field.hidden) cols.push(field.name);
+ }, []).sort();
+
+ // if we are just removing a field, no reason to refetch
+ if (!field.hidden) {
+ $scope.fetch();
+ }
+ };
+
+ $scope.refreshFieldList = function () {
+ source.clearFieldCache(function () {
+ getFields(function () {
+ $scope.fetch();
+ });
+ });
+ };
+
+ $scope.$emit('application.load');
});
});
\ No newline at end of file
diff --git a/src/kibana/apps/discover/styles/main.css b/src/kibana/apps/discover/styles/main.css
deleted file mode 100644
index e69de29bb2d1d..0000000000000
diff --git a/src/kibana/apps/discover/styles/main.html b/src/kibana/apps/discover/styles/main.html
deleted file mode 100644
index e69de29bb2d1d..0000000000000
diff --git a/src/kibana/apps/discover/styles/main.less b/src/kibana/apps/discover/styles/main.less
deleted file mode 100644
index e69de29bb2d1d..0000000000000
diff --git a/src/kibana/controllers/kibana.js b/src/kibana/controllers/kibana.js
index 69d5ea43edf25..4a5ace67ffcc6 100644
--- a/src/kibana/controllers/kibana.js
+++ b/src/kibana/controllers/kibana.js
@@ -9,15 +9,11 @@ define(function (require) {
angular
.module('kibana/controllers')
.controller('kibana', function ($scope, courier, configFile) {
- setTimeout(function () {
- courier.start();
- }, 15);
-
$scope.apps = configFile.apps;
$scope.activeApp = '';
- $scope.$on('$routeChangeSuccess', function () {
- if (courier.running()) courier.fetch();
+ $scope.$on('application.load', function () {
+ courier.start();
});
});
});
\ No newline at end of file
diff --git a/src/kibana/directives/table.js b/src/kibana/directives/table.js
index 91590d5727153..4e54b0d0b1f23 100644
--- a/src/kibana/directives/table.js
+++ b/src/kibana/directives/table.js
@@ -14,12 +14,6 @@ define(function (require) {
*
* ```
*/
-
- var defaults = {
- columns: [],
- rows: []
- };
-
module.directive('kbnTable', function () {
return {
restrict: 'E',
@@ -28,23 +22,28 @@ define(function (require) {
columns: '=',
rows: '='
},
- controller: function ($scope) {
- _.defaults($scope, defaults);
+ link: function (scope, element, attrs) {
+ scope.$watch('rows', render);
+ scope.$watch('columns', render);
+
+ function render() {
+ var $body = element.find('tbody').empty();
+
+ if (!scope.rows || scope.rows.length === 0) return;
+ if (!scope.columns || scope.columns.length === 0) return;
+
+ _.each(scope.rows, function (row) {
+ var tr = document.createElement('tr');
+
+ _.each(scope.columns, function (name) {
+ var td = document.createElement('td');
+ td.innerText = row._source[name] || row[name] || '';
+ tr.appendChild(td);
+ });
- $scope.makeRowHtml = function (row) {
- var html = '
';
- _.each($scope.columns, function (col) {
- html += '';
- if (row[col] !== void 0) {
- html += row[col];
- } else {
- html += row._source[col];
- }
- html += ' | ';
+ $body.append(tr);
});
- html += '
';
- return html;
- };
+ }
}
};
});
diff --git a/src/kibana/index.js b/src/kibana/index.js
index 0e3a7172a82c5..d8d72525a76f5 100644
--- a/src/kibana/index.js
+++ b/src/kibana/index.js
@@ -29,8 +29,9 @@ define(function (require) {
return 'app/' + app.id;
}));
+ // create empty modules for all of the kibana and app modules
requiredAgularModules.forEach(function (name) {
- if (name.indexOf('kibana/') === 0) angular.module(name, []);
+ if (/^(kibana|app)\//.test(name)) angular.module(name, []);
});
kibana.requires = requiredAgularModules;
diff --git a/src/kibana/partials/table.html b/src/kibana/partials/table.html
index 44cb3ef3692c1..00350829ef0eb 100644
--- a/src/kibana/partials/table.html
+++ b/src/kibana/partials/table.html
@@ -1,12 +1,6 @@
- {{col}} |
+ {{name}} |
-
-
-
- {{row._source[col] || row[col]}}
- |
-
-
+
\ No newline at end of file
diff --git a/src/kibana/services/courier.js b/src/kibana/services/courier.js
index 027cf57ce653e..d79d511de919f 100644
--- a/src/kibana/services/courier.js
+++ b/src/kibana/services/courier.js
@@ -1,6 +1,7 @@
define(function (require) {
var angular = require('angular');
var Courier = require('courier/courier');
+ var DataSource = require('courier/data_source/data_source');
var DocSource = require('courier/data_source/doc');
var errors = require('courier/errors');
var configFile = require('../../config');
@@ -13,6 +14,10 @@ define(function (require) {
.service('courier', function (es, $rootScope, promises) {
if (courier) return courier;
+ promises.playNice(DataSource.prototype, [
+ 'getFields'
+ ]);
+
promises.playNice(DocSource.prototype, [
'doUpdate',
'doIndex'
diff --git a/src/kibana/services/saved_searches.js b/src/kibana/services/saved_searches.js
new file mode 100644
index 0000000000000..647c987b52a53
--- /dev/null
+++ b/src/kibana/services/saved_searches.js
@@ -0,0 +1,43 @@
+define(function (require) {
+
+ var module = require('angular').module('kibana/services');
+
+ module.service('savedSearches', function (courier, configFile, $q) {
+ this.get = function (id) {
+ var docLoaded = id ? false : true;
+ var doc = courier.createSource('doc')
+ .index(configFile.kibanaIndex)
+ .type('saved_searches')
+ .id(id)
+ .on('results', function (doc) {
+ search.set(doc._source.state);
+
+ // set the
+ id = doc._id;
+ if (!docLoaded) {
+ docLoaded = true;
+ search.enable();
+ }
+ });
+
+ var search = courier.createSource('search');
+ search.save = function () {
+ var defer = $q.defer();
+
+ doc.doIndex({
+ state: search.toJSON()
+ }, function (err, id) {
+ if (err) return defer.reject(err);
+ defer.resolve();
+ });
+
+ return defer.promise;
+ };
+
+ if (!docLoaded) search.disableFetch();
+ return search;
+ };
+
+ this.create = this.get;
+ });
+});
\ No newline at end of file