mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Merge pull request #17 from spenceralger/discover_crud
Added kbnInfiniteScroll and kbnTruncated
This commit is contained in:
commit
8b9ed4f3f9
12 changed files with 417 additions and 65 deletions
|
@ -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>
|
|
@ -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>
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
47
src/kibana/directives/infinite_scroll.js
Normal file
47
src/kibana/directives/infinite_scroll.js
Normal 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();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
40
src/kibana/directives/truncated.js
Normal file
40
src/kibana/directives/truncated.js
Normal 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';
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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>
|
69
src/kibana/services/visualization.js
Normal file
69
src/kibana/services/visualization.js
Normal 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
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
});
|
14
src/kibana/styles/_table.less
Normal file
14
src/kibana/styles/_table.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue