Merge branch 'master' into fix/#1903

This commit is contained in:
Shelby Sturgis 2014-12-01 18:22:12 +01:00
commit 9267cd88d6
23 changed files with 621 additions and 356 deletions

View file

@ -1,33 +1,8 @@
<paginate
<paginated-table
ng-if="formattedRows.length"
list="formattedRows"
per-page-prop="perPage"
class="agg-table">
<div class="agg-table-paginated">
<table class="table table-condensed">
<thead>
<tr bindonce>
<th
ng-repeat="col in table.columns"
ng-click="aggTable.cycleSort(col)"
ng-class="aggTable.getColumnClass(col, $first, $last)">
<span bo-text="col.title"></span>
<i
class="fa"
ng-class="{
'fa-sort-asc': aggTable.sort.col === col && aggTable.sort.asc,
'fa-sort-desc': aggTable.sort.col === col && !aggTable.sort.asc,
'fa-sort': !aggTable.sort || aggTable.sort.col !== col
}">
</i>
</th>
</tr>
</thead>
<tbody kbn-rows="page" kbn-rows-min="perPage"></tbody>
</table>
</div>
rows="formattedRows"
columns="formattedColumns"
per-page="perPage">
<div class="agg-table-controls">
<a class="small" ng-click="aggTable.exportAsCsv()">
@ -35,4 +10,4 @@
</a>
<paginate-controls></paginate-controls>
</div>
</paginate>
</paginated-table>

View file

@ -1,4 +1,5 @@
define(function (require) {
require('components/paginated_table/paginated_table');
require('services/compile_recursive_directive');
require('css!components/agg_table/agg_table.css');
@ -7,7 +8,6 @@ define(function (require) {
.directive('kbnAggTable', function ($filter, config, Private, compileRecursiveDirective) {
var _ = require('lodash');
var tabifyAggResponse = Private(require('components/agg_response/tabify/tabify'));
var orderBy = $filter('orderBy');
return {
@ -33,42 +33,6 @@ define(function (require) {
quoteValues: config.get('csv:quoteValues')
};
self.getColumnClass = function (col, $first, $last) {
var cls = [];
var agg = $scope.table.aggConfig(col);
if ($last || (agg.schema.group === 'metrics')) {
cls.push('visualize-table-right');
}
if (!self.sort || self.sort.field !== col) {
cls.push('no-sort');
}
return cls.join(' ');
};
self.cycleSort = function (col) {
if (!self.sort || self.sort.col !== col) {
self.sort = {
col: col,
asc: true
};
} else if (self.sort.asc) {
self.sort.asc = false;
} else {
self.sort = null;
}
if (self.sort && !self.sort.getter) {
var colI = $scope.table.columns.indexOf(self.sort.col);
self.sort.getter = function (row) {
return row[colI];
};
if (colI === -1) self.sort = null;
}
};
self.exportAsCsv = function () {
var csv = new Blob([self.toCsv()], { type: 'text/plain' });
self._saveAs(csv, self.csv.filename);
@ -104,31 +68,43 @@ define(function (require) {
}).join('');
};
$scope.$watchMulti([
'table',
'aggTable.sort.asc',
'aggTable.sort.col'
], function () {
$scope.$watch('table', function () {
var table = $scope.table;
if (!table) {
$scope.formattedRows = null;
$scope.formattedColumns = null;
return;
}
setFormattedRows(table);
setFormattedColumns(table);
});
function setFormattedColumns(table) {
$scope.formattedColumns = table.columns.map(function (col, i) {
var formattedColumn = {
title: col.title
};
var agg = $scope.table.aggConfig(col);
var last = i === (table.columns.length - 1);
if (last || (agg.schema.group === 'metrics')) {
formattedColumn.class = 'visualize-table-right';
}
return formattedColumn;
});
}
function setFormattedRows(table) {
var formatters = table.columns.map(function (col) {
return table.fieldFormatter(col);
});
// sort the row values, not formatted
if (self.sort) {
$scope.formattedRows = orderBy(table.rows, self.sort.getter, !self.sort.asc);
} else {
$scope.formattedRows = null;
}
// format all row values
$scope.formattedRows = ($scope.formattedRows || table.rows).map(function (row) {
$scope.formattedRows = (table.rows).map(function (row) {
return row.map(function (cell, i) {
return formatters[i](cell);
});
@ -136,7 +112,7 @@ define(function (require) {
// update the csv file's title
self.csv.filename = (table.title() || 'table') + '.csv';
});
}
}
};
});

View file

@ -0,0 +1,35 @@
<paginate
ng-if="sortedRows.length"
list="sortedRows"
per-page-prop="perPage"
class="agg-table">
<div class="agg-table-paginated">
<table class="table table-condensed">
<thead>
<tr bindonce>
<th
ng-repeat="col in columns"
ng-click="paginatedTable.sortColumn(col)"
class="{{ col.class }}">
<span bo-text="col.title"></span>
<kbn-info ng-if="col.info" info="{{ col.info }}" placement="top"></kbn-info>
<i
class="fa"
ng-class="{
'fa-sort-asc': paginatedTable.sort.columnName === col.title && paginatedTable.sort.direction === 'asc',
'fa-sort-desc': paginatedTable.sort.columnName === col.title && paginatedTable.sort.direction === 'desc',
'fa-sort': paginatedTable.sort.direction === null || paginatedTable.sort.columnName !== col.title
}">
</i>
</th>
</tr>
</thead>
<tbody kbn-rows="page" kbn-rows-min="perPage"></tbody>
</table>
</div>
<!-- auto-inserted by the paginate directive... -->
<!-- <paginate-controls></paginate-controls> -->
<div class="pagination-container" ng-transclude></div>
</paginate>

View file

@ -0,0 +1,79 @@
define(function (require) {
require('modules')
.get('kibana')
.directive('paginatedTable', function ($filter, config, Private) {
var _ = require('lodash');
var orderBy = $filter('orderBy');
return {
restrict: 'E',
template: require('text!components/paginated_table/paginated_table.html'),
transclude: true,
scope: {
rows: '=',
columns: '=',
perPage: '=?',
sortHandler: '=?',
showSelector: '=?'
},
controllerAs: 'paginatedTable',
controller: function ($scope) {
var self = this;
self.sort = {
columnName: null,
direction: null
};
self.sortColumn = function (col) {
var sortDirection;
var cols = _.pluck($scope.columns, 'title');
var index = cols.indexOf(col.title);
if (index === -1) return;
if (self.sort.columnName !== col.title) {
sortDirection = 'asc';
} else {
var directions = {
null: 'asc',
'asc': 'desc',
'desc': null
};
sortDirection = directions[self.sort.direction];
}
self.sort.columnName = col.title;
self.sort.direction = sortDirection;
self._setSortGetter(index);
};
self._setSortGetter = function (index) {
if (_.isFunction($scope.sortHandler)) {
// use custom sort handler
self.sort.getter = $scope.sortHandler(index);
} else {
// use generic sort handler
self.sort.getter = function (row) {
var value = row[index];
if (value.value) return value.value;
return value;
};
}
};
// update the sordedRows result
$scope.$watchMulti([
'paginatedTable.sort.direction',
'rows'
], function () {
if (self.sort.direction == null) {
$scope.sortedRows = $scope.rows.slice(0);
return;
}
$scope.sortedRows = orderBy($scope.rows, self.sort.getter, self.sort.direction === 'desc');
});
}
};
});
});

View file

@ -81,7 +81,8 @@ define(function (require) {
params: this.params,
aggs: this.aggs.map(function (agg) {
return agg.toJSON();
}).filter(Boolean)
}).filter(Boolean),
listeners: this.listeners
};
};
@ -99,4 +100,4 @@ define(function (require) {
return Vis;
};
});
});

View file

@ -125,7 +125,7 @@ define(function (require) {
var rootSeries = obj.series || (obj.slices && obj.slices.children);
var dataLength = rootSeries ? rootSeries.length : 0;
var label = dataLength === 1 ? rootSeries[0].label || rootSeries[0].name : undefined;
var children = (obj.slices && obj.slices.children && obj.slices.children[0].children);
var children = (obj.slices && obj.slices.children && obj.slices.children[0] && obj.slices.children[0].children);
if (!seriesLabel) {
seriesLabel = label;

View file

@ -17,7 +17,6 @@ define(function (require) {
link: function ($scope, $el) {
var $container = $el.find('.visualize-spy-container');
var fullPageSpy = false;
// $scope.spyMode = null; // inherited from the parent
$scope.modes = modes;
$scope.toggleDisplay = function () {
@ -36,38 +35,33 @@ define(function (require) {
var current = $scope.spyMode;
var change = false;
function set() {
// no change
if (current && newMode && newMode.name === current.name) return;
// clear the current value
if (current) {
current.$container.remove();
current.$scope.$destroy();
delete $scope.spyMode;
current = null;
change = true;
}
// no further changes
if (!newMode) return;
// no change
if (current && newMode && newMode.name === current.name) return;
// clear the current value
if (current) {
current.$container.remove();
current.$scope.$destroy();
delete $scope.spyMode;
current = null;
change = true;
current = $scope.spyMode = {
// copy a couple values over
name: newMode.name,
display: newMode.display,
fill: fullPageSpy,
$scope: $scope.$new(),
$container: $('<div class="visualize-spy-content">').appendTo($container)
};
current.$container.append($compile(newMode.template)(current.$scope));
newMode.link && newMode.link(current.$scope, current.$container);
}
// wrapped in fn to enable early return
set();
// no further changes
if (!newMode) return;
change = true;
current = $scope.spyMode = {
// copy a couple values over
name: newMode.name,
display: newMode.display,
fill: fullPageSpy,
$scope: $scope.$new(),
$container: $('<div class="visualize-spy-content">').appendTo($container)
};
current.$container.append($compile(newMode.template)(current.$scope));
newMode.link && newMode.link(current.$scope, current.$container);
};
}
};

View file

@ -69,7 +69,7 @@ define(function (require) {
}
self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp];
if (!self.perPage) {
if (self.perPage == null) {
self.perPage = ALL;
return;
}

View file

@ -3,7 +3,7 @@ define(function (require) {
var _ = require('lodash');
var module = require('modules').get('kibana');
module.directive('kbnRows', function ($parse) {
module.directive('kbnRows', function () {
return {
restrict: 'A',
link: function ($scope, $el, attr) {
@ -14,11 +14,16 @@ define(function (require) {
// access to it here. This may become a problem with the switch to BigNumber
if (_.isNumeric(contents)) $cell.addClass('numeric-value');
if (contents === '') {
$cell.html('&nbsp;');
if (_.isObject(contents)) {
$cell.html($(contents.markup));
} else {
$cell.text(contents);
if (contents === '') {
$cell.html('&nbsp;');
} else {
$cell.text(contents);
}
}
$tr.append($cell);
}

View file

@ -8,7 +8,7 @@ define(function (require) {
return _.map(data, function (row) {
var val;
val = _.isUndefined(row._source[name]) ? row[name] : row._source[name];
val = _.isUndefined(row._flattened[name]) ? row[name] : row._flattened[name];
// for fields that come back in weird formats like geo_point
if (val != null && normalize) val = normalize(val);

View file

@ -331,7 +331,7 @@ define(function (require) {
// Flatten the fields
var indexPattern = $scope.searchSource.get('index');
hit._source = indexPattern.flattenSearchResponse(hit._source);
hit._flattened = indexPattern.flattenSearchResponse(hit._source);
var formatValues = function (value, name) {
// add up the counts for each field name
@ -341,7 +341,7 @@ define(function (require) {
return ($scope.formatsByName[name] || defaultFormat).convert(value);
};
var formattedSource = _.mapValues(hit._source, formatValues);
var formattedSource = _.mapValues(hit._flattened, formatValues);
var formattedHits = _.mapValues(hit.fields, formatValues);
hit._formatted = _.merge(formattedSource, formattedHits);

View file

@ -77,7 +77,7 @@ define(function (require) {
// The fields to loop over
if (!row._fields) {
row._fields = _.union(
_.keys(row._source),
_.keys(row._formatted),
config.get('metaFields')
);
row._fields.sort();
@ -95,7 +95,7 @@ define(function (require) {
};
$detailsScope.showArrayInObjectsWarning = function (row, field) {
var value = row._source[field];
var value = row._formatted[field];
return _.isArray(value) && typeof value[0] === 'object';
};
@ -103,7 +103,7 @@ define(function (require) {
};
$scope.filter = function (row, field, operation) {
$scope.filtering(field, row._source[field] || row[field], operation);
$scope.filtering(field, row._flattened[field] || row[field], operation);
};
$scope.$watchCollection('columns', function () {

View file

@ -53,60 +53,15 @@
</li>
</ul>
<div ng-show="state.tab == 'fields'">
<paginate list="indexPattern.fields|orderBy:table.by:table.reverse" per-page="20">
<table class="table">
<thead>
<th ng-click="setFieldSort('name')">name <i ng-class="sortClass('name')"></i></th>
<th ng-click="setFieldSort('type')">type <i ng-class="sortClass('type')"></i></th>
<th ng-click="setFieldSort('analyzed')">
analyzed <kbn-info info="Analyzed fields may require extra memory to visualize" placement="top"></kbn-info>
<i ng-class="sortClass('analyzed')"></i>
</th>
<th ng-click="setFieldSort('indexed')">
indexed <kbn-info info="Fields that are not indexed are unavailable for search" placement="top"></kbn-info>
<i ng-class="sortClass('indexed')"></i>
</th>
<th ng-click="setFieldSort('count')">
popularity <kbn-info info="A gauge of how often this field is used" placement="top"></kbn-info>
<i ng-class="sortClass('count')"></i>
</th>
</thead>
<tr class="field-settings"
ng-repeat="field in page">
<td>
<span bo-text="field.displayName"></span>
&nbsp;
<span
bo-if="indexPattern.timeFieldName === field.name"
tooltip="This field represents the time that events occured"
class="label label-default">
<i class="fa fa-clock-o"></i>
</span>
</td>
<td>
<span bo-text="field.type"></span>
<i
bo-if="field.type == 'conflict'"
tooltip="The type of this field changes across indices. It is unavailable for many analysis functions"
class="fa fa-warning text-color-warning"></i>
</td>
<td><span bo-text="field.analyzed ? 'yes' : 'no'"></span></td>
<td><span bo-text="field.indexed ? 'yes' : 'no'"></span></td>
<td>
<span bo-text="field.count"></span>
<span class="field-popularize">
<span ng-click="indexPattern.popularizeField(field.name, 1)" class="label label-default"><i class="fa fa-plus"></i></span>
<span ng-click="indexPattern.popularizeField(field.name, -1)" class="label label-default"><i class="fa fa-minus"></i></span>
</span>
</td>
</tr>
</table>
</paginate>
<div ng-show="state.tab == 'fields'" class="fields">
<paginated-table
columns="fieldColumns"
rows="fieldRows"
per-page="perPage">
</paginated-table>
</div>
<div ng-show="state.tab == 'scriptedFields'">
<div ng-show="state.tab == 'scriptedFields'" class="scripted-fields">
No scripted fields defined
</div>

View file

@ -1,5 +1,6 @@
define(function (require) {
var _ = require('lodash');
require('components/paginated_table/paginated_table');
require('routes')
.when('/settings/indices/:id', {
@ -13,9 +14,13 @@ define(function (require) {
});
require('modules').get('apps/settings')
.controller('settingsIndicesEdit', function ($scope, $location, $route, config, courier, Notifier, Private, AppState) {
.controller('settingsIndicesEdit', function ($scope, $location, $route, $compile,
config, courier, Notifier, Private, AppState) {
var rowScopes = []; // track row scopes, so they can be destroyed as needed
var notify = new Notifier();
var $state = $scope.state = new AppState();
var popularityHtml = require('text!plugins/settings/sections/indices/_popularity.html');
var refreshKibanaIndex = Private(require('plugins/settings/sections/indices/_refresh_kibana_index'));
$scope.indexPattern = $route.current.locals.indexPattern;
@ -23,13 +28,50 @@ define(function (require) {
$scope.fieldTypes = Private(require('plugins/settings/sections/indices/_field_types'));
$scope.table = {
by: 'name',
reverse: false,
page: 0,
max: 35
$scope.fieldColumns = [{
title: 'name'
}, {
title: 'type'
}, {
title: 'analyzed',
info: 'Analyzed fields may require extra memory to visualize'
}, {
title: 'indexed',
info: 'Fields that are not indexed are unavailable for search'
}, {
title: 'popularity',
info: 'A gauge of how often this field is used',
}];
$scope.showPopularityControls = function (field) {
$scope.popularityHoverState = (field) ? field : null;
};
$scope.$watchCollection('indexPattern.fields', function () {
_.invoke(rowScopes, '$destroy');
$scope.fieldRows = $scope.indexPattern.fields.map(function (field) {
var childScope = $scope.$new();
rowScopes.push(childScope);
childScope.field = field;
// update the active field via object comparison
if (_.isEqual(field, $scope.popularityHoverState)) {
$scope.showPopularityControls(field);
}
return [field.name, field.type, field.analyzed, field.indexed,
{
markup: $compile(popularityHtml)(childScope),
value: field.count
}
];
});
});
$scope.perPage = 25;
$scope.changeTab = function (obj) {
$state.tab = obj.index;
$state.save();
@ -65,24 +107,6 @@ define(function (require) {
config.set('defaultIndex', $scope.indexPattern.id);
};
$scope.setFieldSort = function (by) {
if ($scope.table.by === by) {
$scope.table.reverse = !$scope.table.reverse;
} else {
$scope.table.by = by;
}
};
$scope.sortClass = function (column) {
if ($scope.table.by !== column) return;
return $scope.table.reverse ? ['fa', 'fa-sort-asc'] : ['fa', 'fa-sort-desc'];
};
$scope.tablePages = function () {
if (!$scope.indexPattern.fields) return 0;
return Math.ceil($scope.indexPattern.fields.length / $scope.table.max);
};
$scope.setIndexPatternsTimeField = function (field) {
if (field.type !== 'date') {
notify.error('That field is a ' + field.type + ' not a date.');

View file

@ -0,0 +1,9 @@
<div ng-mouseover="showPopularityControls(field)" ng-mouseout="showPopularityControls(false)">
<span>{{ field.count }}</span>
<span class="field-popularize" ng-show="popularityHoverState == field">
<span ng-click="indexPattern.popularizeField(field.name, 1)"
class="label label-default"><i class="fa fa-plus"></i></span>
<span ng-click="indexPattern.popularizeField(field.name, -1)"
class="label label-default"><i class="fa fa-minus"></i></span>
</span>
</div>'

View file

@ -129,6 +129,13 @@ kbn-settings-objects-view {
}
}
kbn-settings-indices .fields {
& th:first-child,
& td:first-child {
width: 35%;
}
}
.kbn-settings-indices-create {
.time-and-pattern > div {}
}

View file

@ -6,6 +6,7 @@ define(function (require) {
return function (id, mapping) {
var fake = {
_formatted: _.mapValues(mapping, function (f, c) { return c + '_formatted_' + id + longString; }),
_flattened: _.mapValues(mapping, function (f, c) { return c + '_flattened_' + id + longString; }),
_source: _.mapValues(mapping, function (f, c) { return c + '_original_' + id + longString; }),
_id: id,
_index: 'test',

View file

@ -1,9 +1,10 @@
define(function (require) {
return function stubbedLogstashIndexPatternService(Private) {
var StubIndexPattern = Private(require('test_utils/stub_index_pattern'));
var fieldFormats = Private(require('components/index_patterns/_field_formats'));
var flattenSearchResponse = require('components/index_patterns/_flatten_search_response');
var _ = require('lodash');
return new StubIndexPattern('logstash-*', 'time', [
var indexPattern = new StubIndexPattern('logstash-*', 'time', [
{ name: 'bytes', displayName: 'bytes', type: 'number', indexed: true, analyzed: true, count: 10 },
{ name: 'ssl', displayName: 'ssl', type: 'boolean', indexed: true, analyzed: true, count: 20 },
{ name: '@timestamp', displayName: '@timestamp', type: 'date', indexed: true, analyzed: true, count: 30 },
@ -19,5 +20,10 @@ define(function (require) {
{ name: '_type', displayName: '_type', type: 'string', indexed: true, analyzed: true, count: 0 },
{ name: 'custom_user_field', displayName: 'custom_user_field', type: 'conflict', indexed: false, analyzed: false, count: 0 }
]);
indexPattern.flattenSearchResponse = _.bind(flattenSearchResponse, indexPattern);
return indexPattern;
};
});

View file

@ -87,7 +87,14 @@ define(function (require) {
});
describe('getFieldValues', function () {
var hits = require('fixtures/real_hits.js');
var hits;
beforeEach(function () {
hits = _.each(require('fixtures/real_hits.js'), function (hit) {
hit._flattened = indexPattern.flattenSearchResponse(hit._source);
});
});
it('Should return an array of values for _source fields', function () {
var extensions = fieldCalculator.getFieldValues(hits, indexPattern.fields.byName.extension);
expect(extensions).to.be.an(Array);

View file

@ -45,10 +45,14 @@ define(function (require) {
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
});
var hits = _.each(require('fixtures/hits.js'), function (hit) {
hit._flattened = indexPattern.flattenSearchResponse(hit._source);
});
init($elem, {
fields: _.map(indexPattern.fields.raw, function (v, i) { return _.merge(v, {display: false, rowCount: i}); }),
toggle: sinon.spy(),
data: require('fixtures/hits'),
data: hits,
filter: sinon.spy(),
indexPattern: indexPattern
});

View file

@ -103,158 +103,6 @@ define(function (require) {
});
});
describe('aggTable.cycleSort()', function () {
var vis;
beforeEach(function () {
vis = new Vis(indexPattern, {
type: 'table',
aggs: [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 }
]
}
}
]
});
vis.aggs.forEach(function (agg, i) {
agg.id = 'agg_' + (i + 1);
});
});
function checkAgainst(aggTable, $el, selector) {
return function (asc, firstCol) {
switch (asc) {
case null:
expect(aggTable.sort == null).to.be(true);
break;
case true:
case false:
expect(aggTable.sort).to.have.property('asc', asc);
break;
}
var $leftCol = $el.find(selector || 'tr td:first-child');
firstCol.forEach(function (val, i) {
expect($leftCol.eq(i).text().trim()).to.be(val);
});
};
}
it('sorts by the column passed in', function () {
$scope.table = tabifyAggResponse(vis, fixtures.oneRangeBucket, { canSplit: false });
var $el = $compile('<kbn-agg-table table="table">')($scope);
$scope.$digest();
var sortCol = $scope.table.columns[0];
var $tableScope = $el.isolateScope();
var aggTable = $tableScope.aggTable;
var check = checkAgainst(aggTable, $el);
// default state
check(null, [
'0.0-1000.0',
'1000.0-2000.0'
]);
// enable accending
aggTable.cycleSort(sortCol);
$scope.$digest();
check(true, [
'0.0-1000.0',
'1000.0-2000.0'
]);
// enable descending
aggTable.cycleSort(sortCol);
$scope.$digest();
check(false, [
'1000.0-2000.0',
'0.0-1000.0'
]);
// disable sort
aggTable.cycleSort(sortCol);
$scope.$digest();
check(null, [
'0.0-1000.0',
'1000.0-2000.0'
]);
});
it('sorts new tables by the previous sort rule', function () {
$scope.table = tabifyAggResponse(vis, fixtures.oneRangeBucket, { canSplit: false });
var $el = $compile('<kbn-agg-table table="table">')($scope);
$scope.$digest();
var sortCol = $scope.table.columns[0];
var $tableScope = $el.isolateScope();
var aggTable = $tableScope.aggTable;
var check = checkAgainst(aggTable, $el);
// enable accending, then descending
aggTable.cycleSort(sortCol);
aggTable.cycleSort(sortCol);
$scope.$digest();
check(false, [
'1000.0-2000.0',
'0.0-1000.0'
]);
var prevFormattedRows = $tableScope.formattedRows;
// change the table and trigger the watchers
$scope.table = tabifyAggResponse(vis, fixtures.oneRangeBucket, { canSplit: false });
$scope.$digest();
// prove that the rows were recreated
expect($tableScope.formattedRows).to.not.be(prevFormattedRows);
// check that the order is right
check(false, [
'1000.0-2000.0',
'0.0-1000.0'
]);
});
it('sorts ascending when switching from another column', function () {
$scope.table = tabifyAggResponse(vis, fixtures.oneRangeBucket, { canSplit: false });
var $el = $compile('<kbn-agg-table table="table">')($scope);
$scope.$digest();
var $tableScope = $el.isolateScope();
var aggTable = $tableScope.aggTable;
var rangeCol = $scope.table.columns[0];
var countCol = $scope.table.columns[1];
var checkRange = checkAgainst(aggTable, $el, 'tr td:first-child');
var checkCount = checkAgainst(aggTable, $el, 'tr td:last-child');
// sort count accending
aggTable.cycleSort(countCol);
$scope.$digest();
checkCount(true, [
'298',
'606'
]);
// switch to sorting range ascending
aggTable.cycleSort(rangeCol);
$scope.$digest();
checkRange(true, [
'0.0-1000.0',
'1000.0-2000.0'
]);
});
});
describe('aggTable.toCsv()', function () {
it('escapes and formats the rows and columns properly', function () {
var $el = $compile('<kbn-agg-table table="table">')($scope);

View file

@ -0,0 +1,232 @@
define(function (require) {
require('components/paginated_table/paginated_table');
var _ = require('lodash');
var faker = require('faker');
var sinon = require('sinon/sinon');
describe('paginated table', function () {
var $el;
var $rootScope;
var $compile;
var $scope;
var $elScope;
var $orderBy;
var defaultPerPage = 10;
var makeData = function (colCount, rowCount) {
var cols = faker.Lorem.words(colCount).map(function (word) {
return { title: word };
});
var rows = [];
_.times(rowCount, function () {
rows.push(faker.Lorem.words(colCount));
});
return {
columns: cols,
rows: rows
};
};
var renderTable = function (cols, rows, perPage) {
$scope.cols = cols || [];
$scope.rows = rows || [];
$scope.perPage = perPage || defaultPerPage;
$el = $compile('<paginated-table columns="cols" rows="rows" per-page="perPage">')($scope);
$scope.$digest();
};
beforeEach(function () {
module('kibana');
inject(function (_$rootScope_, _$compile_, $filter) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$orderBy = $filter('orderBy');
});
$scope = $rootScope.$new();
});
afterEach(function () {
$scope.$destroy();
});
describe('rendering', function () {
it('should not display without rows', function () {
var cols = [{
title: 'test1'
}];
var rows = [];
renderTable(cols, rows);
expect($el.children().size()).to.be(0);
});
it('should render columns and rows', function () {
var data = makeData(2, 2);
var cols = data.columns;
var rows = data.rows;
renderTable(cols, rows);
expect($el.children().size()).to.be(1);
var tableRows = $el.find('tbody tr');
// should pad rows
expect(tableRows.size()).to.be(defaultPerPage);
// should contain the row data
expect(tableRows.eq(0).find('td').eq(0).text()).to.be(rows[0][0]);
expect(tableRows.eq(0).find('td').eq(1).text()).to.be(rows[0][1]);
expect(tableRows.eq(1).find('td').eq(0).text()).to.be(rows[1][0]);
expect(tableRows.eq(1).find('td').eq(1).text()).to.be(rows[1][1]);
});
it('should paginate rows', function () {
// note: paginate truncates pages, so don't make too many
var rowCount = _.random(16, 24);
var perPageCount = _.random(5, 8);
var data = makeData(3, rowCount);
var pageCount = Math.ceil(rowCount / perPageCount);
renderTable(data.columns, data.rows, perPageCount);
var tableRows = $el.find('tbody tr');
expect(tableRows.size()).to.be(perPageCount);
// add 2 for the first and last page links
expect($el.find('paginate-controls a').size()).to.be(pageCount + 2);
});
});
describe('sorting', function () {
var data;
var lastRowIndex;
var paginatedTable;
beforeEach(function () {
data = makeData(3, 3);
data.rows.push(['zzzz', 'zzzz', 'zzzz']);
data.rows.push(['aaaa', 'aaaa', 'aaaa']);
lastRowIndex = data.rows.length - 1;
renderTable(data.columns, data.rows);
paginatedTable = $el.isolateScope().paginatedTable;
});
afterEach(function () {
$scope.$destroy();
});
it('should not sort by default', function () {
var tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be(data.rows[0][0]);
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('aaaa');
});
it('should sort ascending on first invocation', function () {
// sortColumn
paginatedTable.sortColumn(data.columns[0]);
$scope.$digest();
var tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('aaaa');
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('zzzz');
});
it('should sort desciending on second invocation', function () {
// sortColumn
paginatedTable.sortColumn(data.columns[0]);
paginatedTable.sortColumn(data.columns[0]);
$scope.$digest();
var tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('zzzz');
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('aaaa');
});
it('should clear sorting on third invocation', function () {
// sortColumn
paginatedTable.sortColumn(data.columns[0]);
paginatedTable.sortColumn(data.columns[0]);
paginatedTable.sortColumn(data.columns[0]);
$scope.$digest();
var tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be(data.rows[0][0]);
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('aaaa');
});
});
describe('custom sorting', function () {
var data;
var paginatedTable;
var sortHandler;
beforeEach(function () {
sortHandler = sinon.spy();
data = makeData(3, 3);
$scope.cols = data.columns;
$scope.rows = data.rows;
$scope.perPage = defaultPerPage;
$scope.sortHandler = sortHandler;
$el = $compile('<paginated-table columns="cols" rows="rows" per-page="perPage"' +
'sort-handler="sortHandler">')($scope);
$scope.$digest();
paginatedTable = $el.isolateScope().paginatedTable;
});
it('should allow custom sorting handler', function () {
var columnIndex = 1;
paginatedTable.sortColumn(data.columns[columnIndex]);
$scope.$digest();
expect(sortHandler.callCount).to.be(1);
expect(sortHandler.getCall(0).args[0]).to.be(columnIndex);
});
});
describe('object rows', function () {
var cols;
var rows;
var paginatedTable;
beforeEach(function () {
cols = [{
title: 'object test'
}];
rows = [
['aaaa'],
[{
markup: '<h1>I am HTML in a row</h1>',
value: 'zzzz'
}],
['bbbb']
];
renderTable(cols, rows);
paginatedTable = $el.isolateScope().paginatedTable;
});
it('should append object markup', function () {
var tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('h1').size()).to.be(0);
expect(tableRows.eq(1).find('h1').size()).to.be(1);
expect(tableRows.eq(2).find('h1').size()).to.be(0);
});
it('should sort using object value', function () {
paginatedTable.sortColumn(cols[0]);
$scope.$digest();
var tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('h1').size()).to.be(0);
expect(tableRows.eq(1).find('h1').size()).to.be(0);
// html row should be the last row
expect(tableRows.eq(2).find('h1').size()).to.be(1);
paginatedTable.sortColumn(cols[0]);
$scope.$digest();
tableRows = $el.find('tbody tr');
// html row should be the first row
expect(tableRows.eq(0).find('h1').size()).to.be(1);
expect(tableRows.eq(1).find('h1').size()).to.be(0);
expect(tableRows.eq(2).find('h1').size()).to.be(0);
});
});
});
});

View file

@ -0,0 +1,107 @@
define(function (require) {
var _ = require('lodash');
var indexPattern;
var Vis;
var visTypes;
describe('Vis Class', function () {
var vis;
var stateFixture = {
type: 'pie',
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'terms', schema: 'segment', params: { field: 'machine.os' }},
{ type: 'terms', schema: 'segment', params: { field: 'geo.src' }}
],
params: { isDonut: true },
listeners: { click: _.noop }
};
beforeEach(module('kibana'));
beforeEach(inject(function (Private) {
Vis = Private(require('components/vis/vis'));
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
visTypes = Private(require('registry/vis_types'));
}));
beforeEach(function () {
vis = new Vis(indexPattern, stateFixture);
});
var verifyVis = function (vis) {
expect(vis).to.have.property('aggs');
expect(vis.aggs).to.have.length(3);
expect(vis).to.have.property('type');
expect(vis.type).to.eql(visTypes.byName['pie']);
expect(vis).to.have.property('listeners');
expect(vis.listeners).to.have.property('click');
expect(vis.listeners.click).to.eql(_.noop);
expect(vis).to.have.property('params');
expect(vis.params).to.have.property('isDonut', true);
expect(vis).to.have.property('indexPattern', indexPattern);
};
describe('initialization', function () {
it('should set the state', function () {
verifyVis(vis);
});
});
describe('getState()', function () {
it('should get a state that represents the... er... state', function () {
var state = vis.getState();
expect(state).to.have.property('type', 'pie');
expect(state).to.have.property('params');
expect(state.params).to.have.property('isDonut', true);
expect(state).to.have.property('listeners');
expect(state.listeners).to.have.property('click');
expect(state.listeners.click).to.eql(_.noop);
expect(state).to.have.property('aggs');
expect(state.aggs).to.have.length(3);
});
});
describe('clone()', function () {
it('should make clone of itself', function () {
var clone = vis.clone();
verifyVis(clone);
});
});
describe('setState()', function () {
it('should set the state to defualts', function () {
var vis = new Vis(indexPattern);
expect(vis).to.have.property('type');
expect(vis.type).to.eql(visTypes.byName['histogram']);
expect(vis).to.have.property('aggs');
expect(vis.aggs).to.have.length(1);
expect(vis).to.have.property('listeners');
expect(vis.listeners).to.eql({});
expect(vis).to.have.property('params');
expect(vis.params).to.have.property('addLegend', true);
expect(vis.params).to.have.property('addTooltip', true);
expect(vis.params).to.have.property('mode', 'stacked');
expect(vis.params).to.have.property('shareYAxis', true);
});
});
describe('isHierarchical()', function () {
it('should return true for hierarchical vis (like pie)', function () {
expect(vis.isHierarchical()).to.be(true);
});
it('should return false for non-hierarchical vis (like histogram)', function () {
var vis = new Vis(indexPattern);
expect(vis.isHierarchical()).to.be(false);
});
});
});
});