mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Merge pull request #423 from rashidkpc/experiment/table
Discover table performance/wrapping enhancement
This commit is contained in:
commit
b1d91ed995
14 changed files with 591 additions and 274 deletions
|
@ -1,71 +1,13 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var html = require('text!apps/discover/partials/table.html');
|
||||
var detailsHtml = require('text!apps/discover/partials/row_details.html');
|
||||
var moment = require('moment');
|
||||
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
|
||||
require('directives/truncated');
|
||||
require('directives/infinite_scroll');
|
||||
require('apps/discover/directives/table_header');
|
||||
require('apps/discover/directives/table_row');
|
||||
|
||||
var module = require('modules').get('app/discover');
|
||||
|
||||
module.directive('kbnTableHeader', function () {
|
||||
var headerHtml = require('text!apps/discover/partials/table_header.html');
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
columns: '=',
|
||||
sorting: '=',
|
||||
mapping: '=',
|
||||
timefield: '=?'
|
||||
},
|
||||
template: headerHtml,
|
||||
controller: function ($scope) {
|
||||
$scope.headerClass = function (column) {
|
||||
if (!$scope.mapping) return;
|
||||
if ($scope.mapping[column] && !$scope.mapping[column].indexed) return;
|
||||
|
||||
var sorting = $scope.sorting;
|
||||
var defaultClass = ['fa', 'fa-sort', 'table-header-sortchange'];
|
||||
|
||||
if (!sorting) return defaultClass;
|
||||
|
||||
if (column === sorting[0]) {
|
||||
return ['fa', sorting[1] === 'asc' ? 'fa-sort-up' : 'fa-sort-down'];
|
||||
} else {
|
||||
return defaultClass;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.moveLeft = function (column) {
|
||||
var index = _.indexOf($scope.columns, column);
|
||||
if (index === 0) return;
|
||||
|
||||
_.move($scope.columns, index, --index);
|
||||
};
|
||||
|
||||
$scope.moveRight = function (column) {
|
||||
var index = _.indexOf($scope.columns, column);
|
||||
if (index === $scope.columns.length - 1) return;
|
||||
|
||||
_.move($scope.columns, index, ++index);
|
||||
};
|
||||
|
||||
$scope.sort = function (column) {
|
||||
if ($scope.mapping[column] && !$scope.mapping[column].indexed) return;
|
||||
var sorting = $scope.sorting || [];
|
||||
$scope.sorting = [column, sorting[1] === 'asc' ? 'desc' : 'asc'];
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* kbnTable directive
|
||||
*
|
||||
|
@ -89,7 +31,7 @@ define(function (require) {
|
|||
mapping: '=',
|
||||
timefield: '=?'
|
||||
},
|
||||
link: function ($scope, element) {
|
||||
link: function ($scope, $el) {
|
||||
$scope.limit = 50;
|
||||
$scope.addRows = function () {
|
||||
if ($scope.limit < config.get('discover:sampleSize')) {
|
||||
|
@ -100,164 +42,4 @@ define(function (require) {
|
|||
};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* kbnTableRow directive
|
||||
*
|
||||
* Display a row in the table
|
||||
* ```
|
||||
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
|
||||
* ```
|
||||
*/
|
||||
module.directive('kbnTableRow', function ($compile, config) {
|
||||
// base class for all dom nodes
|
||||
var DOMNode = window.Node;
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
columns: '=',
|
||||
filtering: '=',
|
||||
mapping: '=',
|
||||
timefield: '=?',
|
||||
row: '=kbnTableRow'
|
||||
},
|
||||
link: function ($scope, element, attrs) {
|
||||
element.after('<tr>');
|
||||
|
||||
var init = function () {
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
};
|
||||
|
||||
// track a list of id's that are currently open, so that
|
||||
// render can easily render in the same current state
|
||||
var opened = [];
|
||||
|
||||
// whenever we compile, we should create a child scope that we can then detroy
|
||||
var $child;
|
||||
|
||||
// toggle display of the rows details, a full list of the fields from each row
|
||||
$scope.toggleRow = function () {
|
||||
var row = $scope.row;
|
||||
var id = row._id;
|
||||
|
||||
$scope.open = !$scope.open;
|
||||
|
||||
var $tr = element;
|
||||
var $detailsTr = $tr.next();
|
||||
|
||||
///
|
||||
// add/remove $details children
|
||||
///
|
||||
|
||||
$detailsTr.toggle($scope.open);
|
||||
|
||||
if (!$scope.open) {
|
||||
// close the child scope if it exists
|
||||
$child.$destroy();
|
||||
// no need to go any further
|
||||
return;
|
||||
} else {
|
||||
$child = $scope.$new();
|
||||
}
|
||||
|
||||
// The fields to loop over
|
||||
row._fields = row._fields || _.keys(row._source).concat(config.get('metaFields')).sort();
|
||||
row._mode = 'table';
|
||||
|
||||
// empty the details and rebuild it
|
||||
$detailsTr
|
||||
.empty()
|
||||
.append(
|
||||
$('<td>').attr('colspan', $scope.columns.length + 2).append(detailsHtml)
|
||||
);
|
||||
|
||||
var showFilters = function (mapping) {
|
||||
var validTypes = ['string', 'number', 'date', 'ip'];
|
||||
if (!mapping.indexed) return false;
|
||||
return _.contains(validTypes, mapping.type);
|
||||
};
|
||||
|
||||
$child.row = row;
|
||||
$child.showFilters = showFilters;
|
||||
|
||||
$compile($detailsTr)($child);
|
||||
};
|
||||
|
||||
$scope.filter = function (row, field, operation) {
|
||||
$scope.filtering(field, row._source[field] || row[field], operation);
|
||||
};
|
||||
|
||||
$scope.$watchCollection('columns', function (columns) {
|
||||
element.empty();
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
});
|
||||
|
||||
$scope.$watch('timefield', function (timefield) {
|
||||
element.empty();
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
});
|
||||
|
||||
// create a tr element that lists the value for each *column*
|
||||
function createSummaryRow(row, id) {
|
||||
|
||||
var expandTd = $('<td>')
|
||||
.append(
|
||||
$('<i class="fa"></span>')
|
||||
.attr('ng-class', '{"fa-caret-right": !open, "fa-caret-down": open}')
|
||||
)
|
||||
.attr('ng-click', 'toggleRow()');
|
||||
$compile(expandTd)($scope);
|
||||
element.append(expandTd);
|
||||
|
||||
var td = $(document.createElement('td'));
|
||||
if ($scope.timefield) {
|
||||
td.addClass('discover-table-timefield');
|
||||
td.attr('width', '1%');
|
||||
_displayField(td, row, $scope.timefield);
|
||||
element.append(td);
|
||||
}
|
||||
|
||||
_.each($scope.columns, function (column) {
|
||||
td = $(document.createElement('td'));
|
||||
_displayField(td, row, column);
|
||||
element.append(td);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill an element with the value of a field
|
||||
*/
|
||||
function _displayField(el, row, field) {
|
||||
return el.html(
|
||||
$('<div>').addClass('truncate-by-height')
|
||||
.text(_getValForField(row, field))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @return {[type]} a string, which should be inserted as text, or an element
|
||||
*/
|
||||
function _getValForField(row, field) {
|
||||
var val;
|
||||
|
||||
// discover formats all of the values and puts them in _formatted for display
|
||||
val = row._formatted[field] || row[field];
|
||||
|
||||
// undefined and null should just be an empty string
|
||||
val = (val == null) ? '' : val;
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
|
|
56
src/kibana/apps/discover/directives/table_header.js
Normal file
56
src/kibana/apps/discover/directives/table_header.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var module = require('modules').get('app/discover');
|
||||
|
||||
module.directive('kbnTableHeader', function () {
|
||||
var headerHtml = require('text!apps/discover/partials/table_header.html');
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
columns: '=',
|
||||
sorting: '=',
|
||||
mapping: '=',
|
||||
timefield: '=?'
|
||||
},
|
||||
template: headerHtml,
|
||||
controller: function ($scope) {
|
||||
$scope.headerClass = function (column) {
|
||||
if (!$scope.mapping) return;
|
||||
if ($scope.mapping[column] && !$scope.mapping[column].indexed) return;
|
||||
|
||||
var sorting = $scope.sorting;
|
||||
var defaultClass = ['fa', 'fa-sort', 'table-header-sortchange'];
|
||||
|
||||
if (!sorting) return defaultClass;
|
||||
|
||||
if (column === sorting[0]) {
|
||||
return ['fa', sorting[1] === 'asc' ? 'fa-sort-up' : 'fa-sort-down'];
|
||||
} else {
|
||||
return defaultClass;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.moveLeft = function (column) {
|
||||
var index = _.indexOf($scope.columns, column);
|
||||
if (index === 0) return;
|
||||
|
||||
_.move($scope.columns, index, --index);
|
||||
};
|
||||
|
||||
$scope.moveRight = function (column) {
|
||||
var index = _.indexOf($scope.columns, column);
|
||||
if (index === $scope.columns.length - 1) return;
|
||||
|
||||
_.move($scope.columns, index, ++index);
|
||||
};
|
||||
|
||||
$scope.sort = function (column) {
|
||||
if ($scope.mapping[column] && !$scope.mapping[column].indexed) return;
|
||||
var sorting = $scope.sorting || [];
|
||||
$scope.sorting = [column, sorting[1] === 'asc' ? 'desc' : 'asc'];
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
223
src/kibana/apps/discover/directives/table_row.js
Normal file
223
src/kibana/apps/discover/directives/table_row.js
Normal file
|
@ -0,0 +1,223 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var htmlEscape = require('utils/html_escape');
|
||||
var module = require('modules').get('app/discover');
|
||||
|
||||
/**
|
||||
* kbnTableRow directive
|
||||
*
|
||||
* Display a row in the table
|
||||
* ```
|
||||
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
|
||||
* ```
|
||||
*/
|
||||
module.directive('kbnTableRow', function ($compile, config) {
|
||||
var openRowHtml = require('text!apps/discover/partials/table_row/open.html');
|
||||
var detailsHtml = require('text!apps/discover/partials/table_row/details.html');
|
||||
var cellTemplate = _.template(require('text!apps/discover/partials/table_row/cell.html'));
|
||||
var truncateByHeightTemplate = _.template(require('text!partials/truncate_by_height.html'));
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
columns: '=',
|
||||
filtering: '=',
|
||||
mapping: '=',
|
||||
timefield: '=?',
|
||||
row: '=kbnTableRow'
|
||||
},
|
||||
link: function ($scope, $el, attrs) {
|
||||
$el.after('<tr>');
|
||||
$el.empty();
|
||||
|
||||
var init = function () {
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
};
|
||||
|
||||
// when we compile the details, we use this $scope
|
||||
var $detailsScope;
|
||||
|
||||
// when we compile the toggle button in the summary, we use this $scope
|
||||
var $toggleScope;
|
||||
|
||||
// toggle display of the rows details, a full list of the fields from each row
|
||||
$scope.toggleRow = function () {
|
||||
var row = $scope.row;
|
||||
|
||||
$scope.open = !$scope.open;
|
||||
|
||||
var $tr = $el;
|
||||
var $detailsTr = $tr.next();
|
||||
|
||||
///
|
||||
// add/remove $details children
|
||||
///
|
||||
|
||||
$detailsTr.toggle($scope.open);
|
||||
|
||||
if (!$scope.open) {
|
||||
// close the child scope if it exists
|
||||
$detailsScope.$destroy();
|
||||
// no need to go any further
|
||||
return;
|
||||
} else {
|
||||
$detailsScope = $scope.$new();
|
||||
}
|
||||
|
||||
// The fields to loop over
|
||||
row._fields = row._fields || _.keys(row._source).concat(config.get('metaFields')).sort();
|
||||
row._mode = 'table';
|
||||
|
||||
// empty the details and rebuild it
|
||||
$detailsTr.html(detailsHtml);
|
||||
|
||||
$detailsScope.row = row;
|
||||
$detailsScope.showFilters = function (mapping) {
|
||||
var validTypes = ['string', 'number', 'date', 'ip'];
|
||||
if (!mapping.indexed) return false;
|
||||
return _.contains(validTypes, mapping.type);
|
||||
};
|
||||
|
||||
$compile($detailsTr)($detailsScope);
|
||||
};
|
||||
|
||||
$scope.filter = function (row, field, operation) {
|
||||
$scope.filtering(field, row._source[field] || row[field], operation);
|
||||
};
|
||||
|
||||
$scope.$watchCollection('columns', function () {
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
});
|
||||
|
||||
$scope.$watch('timefield', function () {
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
});
|
||||
|
||||
// create a tr element that lists the value for each *column*
|
||||
function createSummaryRow(row) {
|
||||
// We just create a string here because its faster.
|
||||
var newHtmls = [
|
||||
openRowHtml
|
||||
];
|
||||
|
||||
if ($scope.timefield) {
|
||||
newHtmls.push(cellTemplate({
|
||||
timefield: true,
|
||||
formatted: _displayField(row, $scope.timefield)
|
||||
}));
|
||||
}
|
||||
|
||||
$scope.columns.forEach(function (column) {
|
||||
newHtmls.push(cellTemplate({
|
||||
timefield: false,
|
||||
formatted: _displayField(row, column, true)
|
||||
}));
|
||||
});
|
||||
|
||||
var $cells = $el.children();
|
||||
newHtmls.forEach(function (html, i) {
|
||||
var $cell = $cells.eq(i);
|
||||
if ($cell.data('discover:html') === html) return;
|
||||
|
||||
var reuse = _.find($cells.slice(i + 1), function (cell) {
|
||||
return $.data(cell, 'discover:html') === html;
|
||||
});
|
||||
|
||||
var $target = reuse ? $(reuse).detach() : $(html);
|
||||
$target.data('discover:html', html);
|
||||
var $before = $cells.eq(i - 1);
|
||||
if ($before.size()) {
|
||||
$before.after($target);
|
||||
} else {
|
||||
$el.append($target);
|
||||
}
|
||||
|
||||
// rebuild cells since we modified the children
|
||||
$cells = $el.children();
|
||||
|
||||
if (i === 0 && !reuse) {
|
||||
$toggleScope = $scope.$new();
|
||||
$compile($target)($toggleScope);
|
||||
}
|
||||
});
|
||||
|
||||
// trim off cells that were not used rest of the cells
|
||||
$cells.filter(':gt(' + (newHtmls.length - 1) + ')').remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill an element with the value of a field
|
||||
*/
|
||||
function _displayField(row, field, breakWords) {
|
||||
var text = _getValForField(row, field);
|
||||
var minLineLength = 20;
|
||||
|
||||
|
||||
if (breakWords) {
|
||||
text = htmlEscape(text);
|
||||
var lineSize = 0;
|
||||
var newText = '';
|
||||
for (var i = 0, len = text.length; i < len; i++) {
|
||||
var chr = text.charAt(i);
|
||||
newText += chr;
|
||||
|
||||
switch (chr) {
|
||||
case ' ':
|
||||
case '&':
|
||||
case ';':
|
||||
case ':':
|
||||
case ',':
|
||||
// natural line break, reset line size
|
||||
lineSize = 0;
|
||||
break;
|
||||
default:
|
||||
lineSize++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (lineSize > minLineLength) {
|
||||
// continuous text is longer then we want,
|
||||
// so break it up with a <wbr>
|
||||
lineSize = 0;
|
||||
newText += '<wbr>';
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length > minLineLength) {
|
||||
return truncateByHeightTemplate({
|
||||
body: newText
|
||||
});
|
||||
}
|
||||
|
||||
text = newText;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @return {[type]} a string, which should be inserted as text, or an element
|
||||
*/
|
||||
function _getValForField(row, field) {
|
||||
var val;
|
||||
|
||||
// discover formats all of the values and puts them in _formatted for display
|
||||
val = row._formatted[field] || row[field];
|
||||
|
||||
// undefined and null should just be an empty string
|
||||
val = (val == null) ? '' : val;
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
<div class="discover-table-details">
|
||||
<ul class="nav nav-tabs discover-table-details-toggle">
|
||||
<li ng-class="{active: row._mode == 'table'}"><a ng-click="row._mode='table'">Table</a></li>
|
||||
<li ng-class="{active: row._mode == 'json'}"><a ng-click="row._mode='json'">JSON</a></li>
|
||||
</ul>
|
||||
|
||||
<table class="table table-condensed" ng-show="row._mode == 'table'" bindonce>
|
||||
<tbody>
|
||||
<tr ng-repeat="field in row._fields" bindonce>
|
||||
<td field-name="field"
|
||||
field-type="mapping[field].type"
|
||||
width="1%"
|
||||
class="discover-table-details-field">
|
||||
</td>
|
||||
<td width="1%" class="discover-table-details-buttons">
|
||||
<span bo-show="showFilters(mapping[field])">
|
||||
<i ng-click="filter(row, field, '+')" class="fa fa-search-plus"></i>
|
||||
<i ng-click="filter(row, field, '-')" class="fa fa-search-minus"></i>
|
||||
</span>
|
||||
<span bo-show="!showFilters(mapping[field])" tooltip="Unindexed fields can not be searched">
|
||||
<i class="fa fa-search-plus text-muted"></i>
|
||||
<i class="fa fa-search-minus text-muted"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<i bo-show="!mapping[field]"
|
||||
tooltip-placement="top"
|
||||
tooltip="No cached mapping for this field. Refresh your mapping from the Settings > Indices page"
|
||||
class="fa fa-warning text-color-warning ng-scope"></i>
|
||||
<span class="discover-table-details-value">{{row._formatted[field] || row[field]}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<pre ng-show="row._mode == 'json'">{{row._source | json}}</pre>
|
||||
</div>
|
3
src/kibana/apps/discover/partials/table_row/cell.html
Normal file
3
src/kibana/apps/discover/partials/table_row/cell.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<td <%= timefield ? 'class="discover-table-timefield" width="1%"' : '' %>>
|
||||
<%= formatted %>
|
||||
</td>
|
40
src/kibana/apps/discover/partials/table_row/details.html
Normal file
40
src/kibana/apps/discover/partials/table_row/details.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<td colspan="{{ columns.length + 2 }}">
|
||||
<div class="discover-table-details">
|
||||
<ul class="nav nav-tabs discover-table-details-toggle">
|
||||
<li ng-class="{active: row._mode == 'table'}"><a ng-click="row._mode='table'">Table</a></li>
|
||||
<li ng-class="{active: row._mode == 'json'}"><a ng-click="row._mode='json'">JSON</a></li>
|
||||
</ul>
|
||||
|
||||
<table class="table table-condensed" ng-show="row._mode == 'table'" bindonce>
|
||||
<tbody>
|
||||
<tr ng-repeat="field in row._fields" bindonce>
|
||||
<td field-name="field"
|
||||
field-type="mapping[field].type"
|
||||
width="1%"
|
||||
class="discover-table-details-field">
|
||||
</td>
|
||||
<td width="1%" class="discover-table-details-buttons">
|
||||
<span bo-show="showFilters(mapping[field])">
|
||||
<i ng-click="filter(row, field, '+')" class="fa fa-search-plus"></i>
|
||||
<i ng-click="filter(row, field, '-')" class="fa fa-search-minus"></i>
|
||||
</span>
|
||||
<span bo-show="!showFilters(mapping[field])" tooltip="Unindexed fields can not be searched">
|
||||
<i class="fa fa-search-plus text-muted"></i>
|
||||
<i class="fa fa-search-minus text-muted"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<i bo-show="!mapping[field]"
|
||||
tooltip-placement="top"
|
||||
tooltip="No cached mapping for this field. Refresh your mapping from the Settings > Indices page"
|
||||
class="fa fa-warning text-color-warning ng-scope"></i>
|
||||
<span class="discover-table-details-value">{{row._formatted[field] || row[field]}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<pre ng-show="row._mode == 'json'">{{row._source | json}}</pre>
|
||||
</div>
|
||||
</td>
|
6
src/kibana/apps/discover/partials/table_row/open.html
Normal file
6
src/kibana/apps/discover/partials/table_row/open.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<td ng-click="toggleRow()">
|
||||
<i
|
||||
class="fa discover-table-open-icon"
|
||||
ng-class="{ 'fa-caret-down': open, 'fa-caret-right': !open }">
|
||||
</i>
|
||||
</td>
|
|
@ -64,9 +64,16 @@
|
|||
padding-left: 0px !important;
|
||||
padding-right: 0px !important;
|
||||
|
||||
.discover-table-timefield {
|
||||
&-timefield {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-open-icon {
|
||||
// when switching between open and closed, the toggle changes size
|
||||
// slightly which is a problem because it forces the entire table to
|
||||
// re-render which is SLOW
|
||||
width: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.discover-table-footer {
|
||||
|
|
3
src/kibana/partials/truncate_by_height.html
Normal file
3
src/kibana/partials/truncate_by_height.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="truncate-by-height">
|
||||
<%= body %>
|
||||
</div>
|
17
src/kibana/utils/html_escape.js
Normal file
17
src/kibana/utils/html_escape.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'\'': ''',
|
||||
'"': '"',
|
||||
};
|
||||
|
||||
var regex = new RegExp('[' + _.keys(map).join('') + ']', 'g');
|
||||
return function htmlEscape(text) {
|
||||
return text.replace(regex, function (c) {
|
||||
return map[c];
|
||||
});
|
||||
};
|
||||
});
|
|
@ -1,15 +1,19 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var longString = Array(200).join('_');
|
||||
|
||||
return function (id, mapping) {
|
||||
var columns = _.keys(mapping);
|
||||
return {
|
||||
_formatted: _.zipObject(_.map(columns, function (c) { return [c, c + '_formatted_' + id + longString]; })),
|
||||
_source: _.zipObject(_.map(columns, function (c) { return [c, c + '_original_' + id + longString]; })),
|
||||
_id: id,
|
||||
_index: 'test',
|
||||
sort: [id]
|
||||
};
|
||||
var fake = {
|
||||
_formatted: _.mapValues(mapping, function (f, c) { return c + '_formatted_' + id + longString; }),
|
||||
_source: _.mapValues(mapping, function (f, c) { return c + '_original_' + id + longString; }),
|
||||
_id: id,
|
||||
_index: 'test',
|
||||
sort: [id]
|
||||
};
|
||||
|
||||
fake._formatted._source = '_source_formatted_' + id + longString;
|
||||
|
||||
return fake;
|
||||
};
|
||||
});
|
|
@ -103,6 +103,7 @@
|
|||
'specs/utils/versionmath',
|
||||
'specs/utils/routes/index',
|
||||
'specs/utils/sequencer',
|
||||
'specs/utils/html_escape',
|
||||
'specs/courier/search_source/_get_normalized_sort',
|
||||
'specs/factories/base_object',
|
||||
'specs/state_management/state',
|
||||
|
|
|
@ -195,7 +195,6 @@ define(function (require) {
|
|||
expect($scope.columns[0]).to.be('bytes');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('kbnTable', function () {
|
||||
|
@ -259,12 +258,9 @@ define(function (require) {
|
|||
expect(tr.length).to.be(100);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('kbnTableRow', function () {
|
||||
|
||||
var $elem = angular.element(
|
||||
'<tr kbn-table-row="row" ' +
|
||||
'columns="columns" ' +
|
||||
|
@ -359,11 +355,209 @@ define(function (require) {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('row diffing', function () {
|
||||
var $row;
|
||||
var $scope;
|
||||
var $root;
|
||||
var $before;
|
||||
|
||||
beforeEach(module('kibana', 'apps/discover'));
|
||||
beforeEach(inject(function ($rootScope, $compile) {
|
||||
$root = $rootScope;
|
||||
$root.row = getFakeRow(0, mapping);
|
||||
$root.columns = ['_source'];
|
||||
$root.sorting = [];
|
||||
$root.filtering = sinon.spy();
|
||||
$root.maxLength = 50;
|
||||
$root.mapping = mapping;
|
||||
$root.timefield = 'timestamp';
|
||||
|
||||
$row = $('<tr>')
|
||||
.attr({
|
||||
'kbn-table-row': 'row',
|
||||
'columns': 'columns',
|
||||
'sorting': 'sortin',
|
||||
'filtering': 'filtering',
|
||||
'mapping': 'mapping',
|
||||
'timefield': 'timefield',
|
||||
});
|
||||
|
||||
$scope = $root.$new();
|
||||
$compile($row)($scope);
|
||||
$root.$apply();
|
||||
|
||||
$before = $row.find('td');
|
||||
expect($before).to.have.length(3);
|
||||
expect($before.eq(0).text().trim()).to.be('');
|
||||
expect($before.eq(1).text().trim()).to.match(/^timestamp_formatted/);
|
||||
expect($before.eq(2).text().trim()).to.match(/^_source_formatted/);
|
||||
}));
|
||||
|
||||
afterEach(function () {
|
||||
$row.remove();
|
||||
});
|
||||
|
||||
it('handles a new column', function () {
|
||||
$root.columns.push('bytes');
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(4);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
});
|
||||
|
||||
it('handles two new columns at once', function () {
|
||||
$root.columns.push('bytes');
|
||||
$root.columns.push('request');
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(5);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(4).text().trim()).to.match(/^request_formatted/);
|
||||
});
|
||||
|
||||
it('handles three new columns in odd places', function () {
|
||||
$root.columns = [
|
||||
'timestamp',
|
||||
'bytes',
|
||||
'_source',
|
||||
'request'
|
||||
];
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(6);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after.eq(2).text().trim()).to.match(/^timestamp_formatted/);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after[4]).to.be($before[2]);
|
||||
expect($after.eq(5).text().trim()).to.match(/^request_formatted/);
|
||||
});
|
||||
|
||||
|
||||
it('handles a removed column', function () {
|
||||
_.pull($root.columns, '_source');
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(2);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
});
|
||||
|
||||
it('handles two removed columns', function () {
|
||||
// first add a column
|
||||
$root.columns.push('timestamp');
|
||||
$root.$apply();
|
||||
|
||||
var $mid = $row.find('td');
|
||||
expect($mid).to.have.length(4);
|
||||
|
||||
$root.columns.pop();
|
||||
$root.columns.pop();
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(2);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
});
|
||||
|
||||
it('handles three removed random columns', function () {
|
||||
// first add two column
|
||||
$root.columns.push('timestamp', 'bytes');
|
||||
$root.$apply();
|
||||
|
||||
var $mid = $row.find('td');
|
||||
expect($mid).to.have.length(5);
|
||||
|
||||
$root.columns[0] = false; // _source
|
||||
$root.columns[2] = false; // bytes
|
||||
$root.columns = $root.columns.filter(Boolean);
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(3);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after.eq(2).text().trim()).to.match(/^timestamp_formatted/);
|
||||
});
|
||||
|
||||
it('handles two columns with the same content', function () {
|
||||
$root.row._formatted.bytes = $root.row._formatted._source;
|
||||
$root.columns.push('bytes');
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(4);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^_source_formatted/);
|
||||
});
|
||||
|
||||
it('handles two columns swapping position', function () {
|
||||
$root.columns.push('bytes');
|
||||
$root.$apply();
|
||||
|
||||
var $mid = $row.find('td');
|
||||
expect($mid).to.have.length(4);
|
||||
|
||||
$root.columns.reverse();
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(4);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($mid[3]);
|
||||
expect($after[3]).to.be($mid[2]);
|
||||
});
|
||||
|
||||
it('handles four columns all reversing position', function () {
|
||||
$root.columns.push('bytes', 'response', 'timestamp');
|
||||
$root.$apply();
|
||||
|
||||
var $mid = $row.find('td');
|
||||
expect($mid).to.have.length(6);
|
||||
|
||||
$root.columns.reverse();
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(6);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($mid[5]);
|
||||
expect($after[3]).to.be($mid[4]);
|
||||
expect($after[4]).to.be($mid[3]);
|
||||
expect($after[5]).to.be($mid[2]);
|
||||
});
|
||||
|
||||
it('handles multiple columns with the same name', function () {
|
||||
$root.columns.push('bytes', 'bytes', 'bytes');
|
||||
$root.$apply();
|
||||
|
||||
var $after = $row.find('td');
|
||||
expect($after).to.have.length(6);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(4).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(5).text().trim()).to.match(/^bytes_formatted/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
19
test/unit/specs/utils/html_escape.js
Normal file
19
test/unit/specs/utils/html_escape.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
define(function (require) {
|
||||
describe('HTML Escape Util', function () {
|
||||
var htmlEscape = require('utils/html_escape');
|
||||
|
||||
it('removes tags by replacing their angle-brackets', function () {
|
||||
expect(htmlEscape('<h1>header</h1>')).to.eql('<h1>header</h1>');
|
||||
});
|
||||
|
||||
it('removes attributes from tags using " and '', function () {
|
||||
expect(htmlEscape('<h1 onclick="alert(\'hi\');">header</h1>'))
|
||||
.to.eql('<h1 onclick="alert('hi');">header</h1>');
|
||||
});
|
||||
|
||||
it('escapes existing html entities by escaping their leading &', function () {
|
||||
expect(htmlEscape('<h1>header</h1>'))
|
||||
.to.eql('&lt;h1&gt;header&lt;/h1&gt;');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue