diff --git a/src/kibana/apps/discover/field_chooser.html b/src/kibana/apps/discover/field_chooser.html index ab27846749338..5c26f46c5c9b8 100644 --- a/src/kibana/apps/discover/field_chooser.html +++ b/src/kibana/apps/discover/field_chooser.html @@ -4,12 +4,12 @@ {{field.name}} -refresh field list \ No newline at end of file +refresh field list \ No newline at end of file diff --git a/src/kibana/apps/discover/index.html b/src/kibana/apps/discover/index.html index 035bb36186cd2..4438fd006367a 100644 --- a/src/kibana/apps/discover/index.html +++ b/src/kibana/apps/discover/index.html @@ -5,13 +5,6 @@ - - + + + -
- - +
+
+ + +
+
+ +
\ 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}} ', 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";