-
+
\ No newline at end of file
diff --git a/src/kibana/apps/discover/index.js b/src/kibana/apps/discover/index.js
index 5fbc223b1dfac..1f6364bd9a5d8 100644
--- a/src/kibana/apps/discover/index.js
+++ b/src/kibana/apps/discover/index.js
@@ -8,15 +8,6 @@ define(function (require, module, exports) {
var app = angular.module('app/discover');
- var sizeOptions = [
- { display: '30', val: 30 },
- { display: '50', val: 50 },
- { display: '80', val: 80 },
- { display: '125', val: 125 },
- { display: '250', val: 250 },
- { display: '500', val: 500 }
- ];
-
var intervalOptions = [
{ display: '', val: null },
{ display: 'Hourly', val: 'hourly' },
@@ -40,15 +31,11 @@ define(function (require, module, exports) {
// stores the fields we want to fetch
$scope.columns = null;
- // At what interval are your index patterns
+ // index pattern interval options
$scope.intervalOptions = intervalOptions;
$scope.interval = $scope.intervalOptions[0];
- // options to control the size of the queries
- $scope.sizeOptions = sizeOptions;
- $scope.size = $scope.sizeOptions[0];
-
- // the index that will be
+ // the index to use when they don't specify one
config.$watch('discover.defaultIndex', function (val) {
if (!val) return config.set('discover.defaultIndex', '_all');
if (!$scope.index) {
@@ -58,25 +45,22 @@ define(function (require, module, exports) {
});
source
- .size(30)
.$scope($scope)
.inherits(courier.rootSearchSource)
.on('results', function (res) {
if (!$scope.fields) getFields();
+
$scope.rows = res.hits.hits;
});
$scope.fetch = function () {
if (!$scope.fields) getFields();
source
- .size($scope.size.val)
+ .size(500)
.query(!$scope.query ? null : {
query_string: {
query: $scope.query
}
- })
- .source(!$scope.columns ? null : {
- include: $scope.columns
});
if ($scope.sort) {
@@ -115,7 +99,7 @@ define(function (require, module, exports) {
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
- hidden: field.hidden
+ display: field.display
};
}, {});
@@ -133,10 +117,14 @@ define(function (require, module, exports) {
field.name = name;
_.defaults(field, currentState[name]);
- if (!field.hidden) $scope.columns.push(name);
+ if (field.display) $scope.columns.push(name);
$scope.fields.push(field);
});
+ if (!$scope.columns.length) {
+ $scope.columns.push('_source');
+ }
+
defer.resolve();
}, defer.reject);
@@ -148,17 +136,16 @@ define(function (require, module, exports) {
$scope.toggleField = function (name) {
var field = _.find($scope.fields, { name: name });
- // toggle the hidden property
- field.hidden = !field.hidden;
+ // toggle the display property
+ field.display = !field.display;
- // collect column names for non-hidden fields and sort
+ // collect column names for displayed fields and sort
$scope.columns = _.transform($scope.fields, function (cols, field) {
- if (!field.hidden) cols.push(field.name);
+ if (field.display) cols.push(field.name);
}, []).sort();
- // if we are just removing a field, no reason to refetch
- if (!field.hidden) {
- $scope.fetch();
+ if (!$scope.columns.length) {
+ $scope.columns.push('_source');
}
};
diff --git a/src/kibana/apps/examples/index.js b/src/kibana/apps/examples/index.js
index 77a37238c38f1..de90bab59b9c2 100644
--- a/src/kibana/apps/examples/index.js
+++ b/src/kibana/apps/examples/index.js
@@ -38,6 +38,7 @@ define(function (require) {
restrict: 'E',
template: 'My favorite number is {{favoriteNum}}
New Favorite ',
controller: function ($scope, config) {
+ // automatically write the value to elasticsearch when it is changed
config.$bind($scope, 'favoriteNum', {
default: 0
});
diff --git a/src/kibana/directives/infinite_scroll.js b/src/kibana/directives/infinite_scroll.js
new file mode 100644
index 0000000000000..dec98f24bc069
--- /dev/null
+++ b/src/kibana/directives/infinite_scroll.js
@@ -0,0 +1,47 @@
+define(function (require) {
+ var module = require('angular').module('kibana/directives');
+ var $ = require('jquery');
+
+ module.directive('kbnInfiniteScroll', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ more: '='
+ },
+ link: function ($scope, $element, attrs) {
+ var $window = $(window);
+ var checkTimer;
+
+ function onScroll() {
+ if (!$scope.more) return;
+
+ var winHeight = $window.height();
+ var windowBottom = winHeight + $window.scrollTop();
+ var elementBottom = $element.offset().top + $element.height();
+ var remaining = elementBottom - windowBottom;
+
+ if (remaining <= winHeight * 0.50) {
+ $scope[$scope.$$phase ? '$eval' : '$apply'](function () {
+ $scope.more();
+ });
+ }
+ }
+
+ function scheduleCheck() {
+ if (checkTimer) return;
+ checkTimer = setTimeout(function () {
+ checkTimer = null;
+ onScroll();
+ }, 50);
+ }
+
+ $window.on('scroll', scheduleCheck);
+ $scope.$on('$destroy', function () {
+ clearTimeout(checkTimer);
+ $window.off('scroll', scheduleCheck);
+ });
+ scheduleCheck();
+ }
+ };
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/directives/table.js b/src/kibana/directives/table.js
index 4e54b0d0b1f23..20ed3b5711aac 100644
--- a/src/kibana/directives/table.js
+++ b/src/kibana/directives/table.js
@@ -3,6 +3,9 @@ define(function (require) {
var angular = require('angular');
var _ = require('lodash');
+ require('directives/truncated');
+ require('directives/infinite_scroll');
+
var module = angular.module('kibana/directives');
/**
@@ -11,10 +14,13 @@ define(function (require) {
* displays results in a simple table view. Pass the result object
* via the results attribute on the kbnTable element:
* ```
- *
+ *
* ```
*/
- module.directive('kbnTable', function () {
+ module.directive('kbnTable', function ($compile) {
+ // base class for all dom nodes
+ var DOMNode = window.Node;
+
return {
restrict: 'E',
template: html,
@@ -22,27 +28,195 @@ define(function (require) {
columns: '=',
rows: '='
},
- link: function (scope, element, attrs) {
- scope.$watch('rows', render);
- scope.$watch('columns', render);
+ link: function ($scope, element, attrs) {
+ // track a list of id's that are currently open, so that
+ // render can easily render in the same current state
+ var opened = [];
+
+ // the current position in the list of rows
+ var cursor = 0;
+
+ // the page size to load rows (out of the rows array, load 50 at a time)
+ var pageSize = 50;
+
+ // rerender when either is changed
+ $scope.$watch('rows', render);
+ $scope.$watch('columns', render);
+ // the body of the table
+ var $body = element.find('tbody');
+
+ // itterate the columns and rows, rebuild the table's html
function render() {
- var $body = element.find('tbody').empty();
+ $body.empty();
+ if (!$scope.rows || $scope.rows.length === 0) return;
+ if (!$scope.columns || $scope.columns.length === 0) return;
+ cursor = 0;
+ addRows();
+ $scope.addRows = addRows;
+ }
+
+ function addRows() {
+ if (cursor > $scope.rows.length) {
+ $scope.addRows = null;
+ }
+
+ $scope.rows.slice(cursor, cursor += pageSize).forEach(function (row, i) {
+ var id = rowId(row);
+ var $summary = createSummaryRow(row, id);
+ if (i % 2) $summary.addClass('even');
+
+ $body.append([
+ $summary,
+ createDetailsRow(row, id)
+ ]);
+ });
+ }
+
+ // for now, rows are "tracked" by their index, but this could eventually
+ // be configured so that changing the order of the rows won't prevent
+ // them from staying open on update
+ function rowId(row) {
+ var id = $scope.rows.indexOf(row);
+ return ~id ? id : null;
+ }
+
+ // inverse of rowId()
+ function rowForId(id) {
+ return $scope.rows[id];
+ }
+
+ // toggle display of the rows details, a full list of the fields from each row
+ $scope.toggleRow = function (id, event) {
+ var row = rowForId(id);
+
+ if (~opened.indexOf(id)) {
+ _.pull(opened, id);
+ } else {
+ opened.push(id);
+ }
+
+ angular
+ .element(event.delegateTarget)
+ .next()
+ .replaceWith(createDetailsRow(row, id));
+ };
- if (!scope.rows || scope.rows.length === 0) return;
- if (!scope.columns || scope.columns.length === 0) return;
+ var topLevelDetails = '_index _type _id'.split(' ');
+ function createDetailsRow(row, id) {
+ var tr = document.createElement('tr');
- _.each(scope.rows, function (row) {
- var tr = document.createElement('tr');
+ var containerTd = document.createElement('td');
+ containerTd.setAttribute('colspan', $scope.columns.length);
+ tr.appendChild(containerTd);
- _.each(scope.columns, function (name) {
- var td = document.createElement('td');
- td.innerText = row._source[name] || row[name] || '';
- tr.appendChild(td);
+ if (!~opened.indexOf(id)) {
+ // short circuit if the row is hidden
+ tr.style.display = 'none';
+ return tr;
+ }
+
+ var table = document.createElement('table');
+ containerTd.appendChild(table);
+ table.className = 'table';
+
+ var tbody = document.createElement('tbody');
+ table.appendChild(tbody);
+
+ _(row._source)
+ .keys()
+ .concat(topLevelDetails)
+ .sort()
+ .each(function (field) {
+ var tr = document.createElement('tr');
+ // tr -> ||
|| ||
+
+ var fieldTd = document.createElement('td');
+ fieldTd.textContent = field;
+ fieldTd.className = 'field-name';
+ tr.appendChild(fieldTd);
+
+ var valTd = document.createElement('td');
+ _displayField(valTd, row, field);
+ tr.appendChild(valTd);
+
+ tbody.appendChild(tr);
});
- $body.append(tr);
+ return tr;
+ }
+
+ // create a tr element that lists the value for each *column*
+ function createSummaryRow(row, id) {
+ var tr = document.createElement('tr');
+ tr.setAttribute('ng-click', 'toggleRow(' + JSON.stringify(id) + ', $event)');
+ var $tr = $compile(tr)($scope);
+
+ _.each($scope.columns, function (column) {
+ var td = document.createElement('td');
+ _displayField(td, row, column);
+ $tr.append(td);
});
+
+ return $tr;
+ }
+
+ /**
+ * Fill an element with the value of a field
+ */
+ function _displayField(el, row, field) {
+ var val = _getValForField(row, field);
+ if (val instanceof DOMNode) {
+ el.appendChild(val);
+ } else {
+ el.textContent = val;
+ }
+ return el;
+ }
+
+ /**
+ * get the value of a field from a row, serialize it to a string
+ * and truncate it if necessary
+ *
+ * @param {object} row - the row to pull the value from
+ * @param {[type]} field - the name of the field (dot-seperated paths are accepted)
+ * @return {[type]} a string, which should be inserted as text, or an element
+ */
+ function _getValForField(row, field) {
+ var val;
+
+ // is field name a path?
+ if (~field.indexOf('.')) {
+ var path = field.split('.');
+ // only check source for "paths"
+ var current = row._source;
+ var step;
+ while (step = path.shift() && current) {
+ // walk from the _source to the specified by the path
+ current = current[step];
+ }
+ val = current;
+ } else {
+ // simple, with a fallback to row
+ val = row._source[field] || row[field];
+ }
+
+ // undefined and null should just be an empty string
+ val = (val == null) ? '' : val;
+
+ // stringify array's and objects
+ if (typeof val === 'object') val = JSON.stringify(val);
+
+ // truncate
+ if (typeof val === 'string' && val.length > 150) {
+ var complete = val;
+ val = document.createElement('kbn-truncated');
+ val.setAttribute('orig', complete);
+ val.setAttribute('length', 150);
+ val = $compile(val)($scope)[0];// return the actual element
+ }
+
+ return val;
}
}
};
diff --git a/src/kibana/directives/truncated.js b/src/kibana/directives/truncated.js
new file mode 100644
index 0000000000000..7d41c4c72a8b2
--- /dev/null
+++ b/src/kibana/directives/truncated.js
@@ -0,0 +1,40 @@
+define(function (require) {
+ var module = require('angular').module('kibana/directives');
+ var $ = require('jquery');
+
+ module.directive('kbnTruncated', function ($compile) {
+ return {
+ restrict: 'E',
+ scope: {
+ orig: '@',
+ length: '@'
+ },
+ template: function ($element, attrs) {
+ var template = '{{text}} ';
+ if (attrs.length && attrs.orig && attrs.orig.length > attrs.length) {
+ template += ' {{action}} ';
+ }
+ return template;
+ },
+ link: function ($scope, $element, attrs) {
+ var fullText = $scope.orig;
+ var truncated = fullText.substring(0, $scope.length);
+
+ if (fullText === truncated) return;
+
+ truncated += '...';
+
+ $scope.expanded = false;
+ $scope.text = truncated;
+ $scope.action = 'more';
+
+ $scope.toggle = function ($event) {
+ $event.stopPropagation();
+ $scope.expanded = !$scope.expanded;
+ $scope.text = $scope.expanded ? fullText : truncated;
+ $scope.action = $scope.expanded ? 'less' : 'more';
+ };
+ }
+ };
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/partials/table.html b/src/kibana/partials/table.html
index 00350829ef0eb..ec8b9b21544b0 100644
--- a/src/kibana/partials/table.html
+++ b/src/kibana/partials/table.html
@@ -3,4 +3,5 @@
{{name}}
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/src/kibana/services/visualization.js b/src/kibana/services/visualization.js
new file mode 100644
index 0000000000000..a7efd86b1a6f2
--- /dev/null
+++ b/src/kibana/services/visualization.js
@@ -0,0 +1,69 @@
+define(function (require) {
+ var angular = require('angular');
+ var _ = require('lodash');
+
+ var module = angular.module('kibana/services');
+
+ module.service('visualizations', function (courier, es, config, visFactory, $q) {
+ this.getOrCreate = function (reject, resolve, id) {
+ if (!id) return this.create(id);
+
+ return this.get(id)
+ .catch(function (err) {
+ return this.create(id);
+ });
+ };
+
+ this.get = function (id) {
+ var defer = $q.defer();
+
+ var settingSource = courier.createSource('doc')
+ .index(config.get('visualizations.index'))
+ .type(config.get('visualizations.type'))
+ .id(id)
+ .on('update', function onResult(doc) {
+ if (doc.found) {
+ // the source will re-emit it's most recent result
+ // once "results" is listened for
+ defer.resolve(visFactory(settingSource));
+ } else {
+ defer.reject(new Error('Doc not found'));
+ }
+ });
+
+ return defer.promise;
+ };
+
+ this.create = function (reject, resolve) {
+ var defer = $q.defer();
+
+ var docSource = courier.createSource('doc')
+ .index(config.get('visualizations.index'))
+ .type(config.get('visualizations.type'));
+
+ defer.resolve(visFactory(docSource));
+ return defer.promise;
+ };
+
+ this.list = function (reject, resolve) {
+ return es.search({
+ index: config.get('visualizations.index'),
+ type: config.get('visualizations.type'),
+ body: {
+ query: {
+ match_all: {}
+ }
+ }
+ }).then(function (resp) {
+ return _.map(resp.hits.hits, function (hit) {
+ return {
+ name: hit._source.title,
+ id: hit._id,
+ type: hit._source.type
+ };
+ });
+ });
+ };
+
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/styles/_table.less b/src/kibana/styles/_table.less
new file mode 100644
index 0000000000000..f2b9f594b0ee6
--- /dev/null
+++ b/src/kibana/styles/_table.less
@@ -0,0 +1,14 @@
+kbn-table {
+ // sub tables should not have a leading border
+ .table .table {
+ margin-bottom: 0px;
+
+ tr:first-child > td {
+ border-top: none;
+ }
+
+ td.field-name {
+ font-weight: bold;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/kibana/styles/main.css b/src/kibana/styles/main.css
index a421fcaa1bcef..29f3b7c9151a2 100644
--- a/src/kibana/styles/main.css
+++ b/src/kibana/styles/main.css
@@ -6986,3 +6986,12 @@ body {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
text-align: center;
}
+kbn-table .table .table {
+ margin-bottom: 0px;
+}
+kbn-table .table .table tr:first-child > td {
+ border-top: none;
+}
+kbn-table .table .table td.field-name {
+ font-weight: bold;
+}
diff --git a/src/kibana/styles/main.less b/src/kibana/styles/main.less
index 86dd6a6dde459..ae3f8284ca5c0 100644
--- a/src/kibana/styles/main.less
+++ b/src/kibana/styles/main.less
@@ -49,4 +49,6 @@ body {
@shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);
.box-shadow(@shadow);
text-align: center;
-}
\ No newline at end of file
+}
+
+@import "./_table.less";