Added kbnInfiniteScroll and kbnTruncated

This commit is contained in:
Spencer Alger 2014-02-28 15:55:42 -07:00
parent 9b99c18b0f
commit 636895423d
12 changed files with 417 additions and 65 deletions

View file

@ -4,12 +4,12 @@
<i
class="fa"
ng-class="{
'fa-check-square': !field.hidden,
'fa-square-o': field.hidden
'fa-check-square': field.display,
'fa-square-o': !field.display
}">
</i>
{{field.name}}
</span>
</li>
</ul>
<small class="pull-right"><a ng-click="refresh()">refresh field list</a></small>
<small><a ng-click="refresh()">refresh field list</a></small>

View file

@ -5,13 +5,6 @@
<input class="form-control" ng-model="index">
<label class="control-label" for="size">Query</label>
<input type="text" class="form-control" ng-model="query" placeholder="search">
<label class="control-label" for="size">Limit</label>
<select
class="form-control"
name="size"
ng-model="size"
ng-options="size.display for size in sizeOptions">
</select>
<label class="control-label" for="sort">Sort</label>
<select
class="form-control"
@ -24,16 +17,31 @@
</button>
</form>
</nav>
<div class="row">
<div class="col-md-10">
<kbn-table rows="rows" columns="columns"></kbn-table>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="form-inline pull-right">
<label class="control-label" for="size">Display</label>
<select
class="form-control"
name="size"
ng-model="size"
ng-options="size.display for size in sizeOptions">
</select>
</div>
</div>
</div>
<div class="col-md-2">
<disc-field-chooser
fields="fields"
toggle="toggleField"
refresh="refreshFieldList">
</disc-field-chooser>
<div class="row">
<div class="col-md-2">
<disc-field-chooser
fields="fields"
toggle="toggleField"
refresh="refreshFieldList">
</disc-field-chooser>
</div>
<div class="col-md-10">
<kbn-table rows="rows" columns="columns" ></kbn-table>
</div>
</div>
</div>
</div>

View file

@ -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');
}
};

View file

@ -38,6 +38,7 @@ define(function (require) {
restrict: 'E',
template: 'My favorite number is {{favoriteNum}} <button ng-click="click()">New Favorite</button>',
controller: function ($scope, config) {
// automatically write the value to elasticsearch when it is changed
config.$bind($scope, 'favoriteNum', {
default: 0
});

View file

@ -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();
}
};
});
});

View file

@ -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:
* ```
* <kbn-table results="queryResult"></kbn-table>
* <kbn-table columns="columnsToDisplay" rows="rowsToDisplay"></kbn-table>
* ```
*/
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;
}
if (!scope.rows || scope.rows.length === 0) return;
if (!scope.columns || scope.columns.length === 0) return;
function addRows() {
if (cursor > $scope.rows.length) {
$scope.addRows = null;
}
_.each(scope.rows, function (row) {
var tr = document.createElement('tr');
$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');
_.each(scope.columns, function (name) {
var td = document.createElement('td');
td.innerText = row._source[name] || row[name] || '';
tr.appendChild(td);
$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));
};
var topLevelDetails = '_index _type _id'.split(' ');
function createDetailsRow(row, id) {
var tr = document.createElement('tr');
var containerTd = document.createElement('td');
containerTd.setAttribute('colspan', $scope.columns.length);
tr.appendChild(containerTd);
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 -> || <field> || <val> ||
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;
}
}
};

View file

@ -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 = '<span>{{text}}</span>';
if (attrs.length && attrs.orig && attrs.orig.length > attrs.length) {
template += ' <a ng-click="toggle($event)">{{action}}</a>';
}
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';
};
}
};
});
});

View file

@ -3,4 +3,5 @@
<th ng-repeat="name in columns">{{name}}</th>
</thead>
<tbody></tbody>
</table>
</table>
<kbn-infinite-scroll more="addRows"></kbn-infinite-scroll>

View file

@ -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
};
});
});
};
});
});

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -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;
}
}
@import "./_table.less";