Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Rashid Khan 2014-03-03 14:47:27 -07:00
commit d5ae35c3ad
15 changed files with 555 additions and 77 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

@ -3,37 +3,41 @@
<form class="navbar-form navbar-left form-inline" role="search" ng-submit="fetch()">
<label class="control-label">Index</label>
<input class="form-control" ng-model="index">
<label class="control-label" for="size">Query</label>
<label class="control-label">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>
<label class="control-label">Sort</label>
<select
class="form-control"
name="sort"
ng-model="sort"
ng-options="field.name for field in fields">
</select>
<label class="control-label">Max Summary Length</label>
<input type="number" name class="form-control" ng-model="opts.maxSummaryLength">
<button type="submit" class="btn btn-default">
<i class="fa fa-search"></i>
</button>
</form>
</nav>
<div class="row">
<div class="col-md-10">
<kbn-table rows="rows" columns="columns"></kbn-table>
</div>
<div class="col-md-2">
<disc-field-chooser
fields="fields"
toggle="toggleField"
refresh="refreshFieldList">
</disc-field-chooser>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<div class="panel panel-primary">
<div class="panel-heading">
<h2 class="panel-title">Fields</h3>
</div>
<div class="panel-body">
<disc-field-chooser
fields="fields"
toggle="toggleField"
refresh="refreshFieldList">
</disc-field-chooser>
</div>
</div>
</div>
<div class="col-md-10">
<kbn-table rows="rows" columns="columns" max-length="opts.maxSummaryLength"></kbn-table>
</div>
</div>
</div>
</div>

View file

@ -8,16 +8,7 @@ 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 = [
var intervals = [
{ display: '', val: null },
{ display: 'Hourly', val: 'hourly' },
{ display: 'Daily', val: 'daily' },
@ -34,21 +25,24 @@ define(function (require, module, exports) {
source = savedSearches.create();
}
$scope.opts = {
// number of records to fetch, then paginate through
sampleSize: 500,
// max length for summaries in the table
maxSummaryLength: 100
};
// stores the complete list of fields
$scope.fields = null;
// stores the fields we want to fetch
$scope.columns = null;
// At what interval are your index patterns
$scope.intervalOptions = intervalOptions;
$scope.interval = $scope.intervalOptions[0];
// index pattern interval options
$scope.intervals = intervals;
$scope.interval = $scope.intervals[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 +52,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($scope.opts.sampleSize)
.query(!$scope.query ? null : {
query_string: {
query: $scope.query
}
})
.source(!$scope.columns ? null : {
include: $scope.columns
});
if ($scope.sort) {
@ -115,7 +106,7 @@ define(function (require, module, exports) {
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
hidden: field.hidden
display: field.display
};
}, {});
@ -131,12 +122,12 @@ define(function (require, module, exports) {
.each(function (name) {
var field = fields[name];
field.name = name;
_.defaults(field, currentState[name]);
if (!field.hidden) $scope.columns.push(name);
_.defaults(field, currentState[name]);
$scope.fields.push(field);
});
refreshColumns();
defer.resolve();
}, defer.reject);
@ -148,18 +139,10 @@ 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
$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();
}
refreshColumns();
};
$scope.refreshFieldList = function () {
@ -170,6 +153,17 @@ define(function (require, module, exports) {
});
};
function refreshColumns() {
// collect column names for displayed fields and sort
$scope.columns = _.transform($scope.fields, function (cols, field) {
if (field.display) cols.push(field.name);
}, []).sort();
if (!$scope.columns.length) {
$scope.columns.push('_source');
}
}
$scope.$emit('application.load');
});
});

View file

@ -0,0 +1,5 @@
disc-field-chooser ul {
margin: 0;
padding: 0;
list-style: none;
}

View file

@ -0,0 +1,7 @@
disc-field-chooser {
ul {
margin: 0;
padding: 0;
list-style: none;
}
}

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

@ -14,6 +14,7 @@ define(function (require) {
$scope.activeApp = '';
$scope.$on('$locationChangeSuccess', function (event, uri) {
if (!uri) return;
$scope.activeApp = uri.split('#')[1].split('/')[1];
});

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 winBottom = winHeight + $window.scrollTop();
var elTop = $element.offset().top;
var remaining = elTop - winBottom;
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

@ -2,6 +2,11 @@ define(function (require) {
var html = require('text!partials/table.html');
var angular = require('angular');
var _ = require('lodash');
var nextTick = require('utils/next_tick');
var $ = require('jquery');
require('directives/truncated');
require('directives/infinite_scroll');
var module = angular.module('kibana/directives');
@ -11,38 +16,304 @@ 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;
function scheduleNextRenderTick(cb) {
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(cb);
} else {
nextTick(cb);
}
}
return {
restrict: 'E',
template: html,
scope: {
columns: '=',
rows: '='
rows: '=',
maxLength: '=?'
},
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;
// rendering an entire page while the page is scrolling can cause a good
// bit of jank, lets only render a certain amount per "tick"
var rowsPerTick;
// set the maxLength for summaries
if ($scope.maxLength === void 0) {
$scope.maxLength = 250;
}
// rerender when either is changed
$scope.$watch('rows', render);
$scope.$watch('columns', render);
$scope.$watch('maxLength', 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;
var renderRows = (function () {
// basic buffer that will be pulled from when we are adding rows.
var queue = [];
var rendering = false;
_.each(scope.rows, function (row) {
var tr = document.createElement('tr');
return function renderRows(rows) {
[].push.apply(queue, rows);
if (!rendering) {
onTick();
}
};
_.each(scope.columns, function (name) {
var td = document.createElement('td');
td.innerText = row._source[name] || row[name] || '';
tr.appendChild(td);
function forEachRow(row, i, currentChunk) {
var id = rowId(row);
var $summary = createSummaryRow(row, id);
var $details = createDetailsRow(row, id);
// cursor is the end of current selection, so
// subtract the remaining queue size, then the
// size of this chunk, and add the current i
var currentPosition = cursor - queue.length - currentChunk.length + i;
if (currentPosition % 2) {
$summary.addClass('even');
$details.addClass('even');
}
$body.append([
$summary,
$details
]);
}
function onTick() {
// ensure that the rendering flag is set
rendering = true;
var performance = window.performance;
var timing;
if (
performance
&& rowsPerTick === void 0
&& typeof performance.now === 'function'
) {
timing = performance.now();
rowsPerTick = 30;
}
queue
// grab the first n from the buffer
.splice(0, rowsPerTick)
// render each row
.forEach(forEachRow);
if (timing) {
var time = performance.now() - timing;
var rowsRendered = rowsPerTick;
var msPerRow = time / rowsPerTick;
// aim to fit the rendering into 5 milliseconds
rowsPerTick = Math.ceil(5 / msPerRow);
console.log('completed render of %d rows in %d milliseconds. rowsPerTick set to %d', rowsRendered, time, rowsPerTick);
}
if (queue.length) {
// the queue is not empty, draw again next tick
scheduleNextRenderTick(onTick);
} else {
// unset the rendering flag
rendering = false;
}
}
}());
function addRows() {
if (cursor > $scope.rows.length) {
$scope.addRows = null;
}
renderRows($scope.rows.slice(cursor, cursor += pageSize));
}
// 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);
}
// rather than replace the entire row, just replace the
// children, this way we keep the "even" class on the row
appendDetailsToRow(
$(event.delegateTarget).next().empty(),
row,
id
);
};
var topLevelDetails = '_index _type _id'.split(' ');
function createDetailsRow(row, id) {
var $tr = $(document.createElement('tr'));
return appendDetailsToRow($tr, row, id);
}
function appendDetailsToRow($tr, row, id) {
// we need a td to wrap the details table
var containerTd = document.createElement('td');
containerTd.setAttribute('colspan', $scope.columns.length);
$tr.append(containerTd);
var open = !!~opened.indexOf(id);
$tr.toggle(open);
// it's closed, so no need to go any further
if (!open) return $tr;
// table that will hold details about the row
var table = document.createElement('table');
containerTd.appendChild(table);
table.className = 'table';
// body of the table
var tbody = document.createElement('tbody');
table.appendChild(tbody);
// itterate each row and append it to the 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, true);
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, truncate) {
var val = _getValForField(row, field, truncate);
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 {string} field - the name of the field (dot-seperated paths are accepted)
* @param {boolean} untruncate - Should truncated values have a "more" link to expand the text?
* @return {[type]} a string, which should be inserted as text, or an element
*/
function _getValForField(row, field, untruncate) {
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 > $scope.maxLength) {
if (untruncate) {
var complete = val;
val = document.createElement('kbn-truncated');
val.setAttribute('orig', complete);
val.setAttribute('length', $scope.maxLength);
val = $compile(val)($scope)[0];// return the actual element
} else {
val = val.substring(0, $scope.maxLength) + '...';
}
}
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,18 @@
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;
}
}
tr.even td {
background-color: #f1f1f1;
}
}

View file

@ -6986,3 +6986,20 @@ 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;
}
kbn-table tr.even td {
background-color: #f1f1f1;
}
disc-field-chooser ul {
margin: 0;
padding: 0;
list-style: none;
}

View file

@ -49,4 +49,7 @@ 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";
@import "../apps/discover/styles/main.less";