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 @@ + +refresh field list \ No newline at end of file diff --git a/src/kibana/apps/discover/field_chooser.js b/src/kibana/apps/discover/field_chooser.js new file mode 100644 index 0000000000000..9515dbe2b5e1a --- /dev/null +++ b/src/kibana/apps/discover/field_chooser.js @@ -0,0 +1,16 @@ +define(function (require) { + var app = require('angular').module('app/discover'); + var html = require('text!./field_chooser.html'); + + app.directive('discFieldChooser', function () { + return { + restrict: 'E', + scope: { + fields: '=', + toggle: '=', + refresh: '=' + }, + template: html + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/apps/discover/index.html b/src/kibana/apps/discover/index.html index 5bf9d89871816..035bb36186cd2 100644 --- a/src/kibana/apps/discover/index.html +++ b/src/kibana/apps/discover/index.html @@ -1,43 +1,39 @@
-

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