diff --git a/src/kibana/components/agg_table/agg_table.html b/src/kibana/components/agg_table/agg_table.html index eb9f7a9f80607..bb2c632248d92 100644 --- a/src/kibana/components/agg_table/agg_table.html +++ b/src/kibana/components/agg_table/agg_table.html @@ -1,33 +1,8 @@ - -
- - - - - - - -
- - - -
-
+ rows="formattedRows" + columns="formattedColumns" + per-page="perPage">
@@ -35,4 +10,4 @@
-
\ No newline at end of file + diff --git a/src/kibana/components/agg_table/agg_table.js b/src/kibana/components/agg_table/agg_table.js index 7b5755c656f4c..3698a925c70db 100644 --- a/src/kibana/components/agg_table/agg_table.js +++ b/src/kibana/components/agg_table/agg_table.js @@ -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'; - }); + } } }; }); diff --git a/src/kibana/components/paginated_table/paginated_table.html b/src/kibana/components/paginated_table/paginated_table.html new file mode 100644 index 0000000000000..8262ffc984776 --- /dev/null +++ b/src/kibana/components/paginated_table/paginated_table.html @@ -0,0 +1,35 @@ + +
+ + + + + + + +
+ + + + +
+
+ + + +
+ +
\ No newline at end of file diff --git a/src/kibana/components/paginated_table/paginated_table.js b/src/kibana/components/paginated_table/paginated_table.js new file mode 100644 index 0000000000000..08af4c619135e --- /dev/null +++ b/src/kibana/components/paginated_table/paginated_table.js @@ -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'); + }); + } + }; + }); +}); diff --git a/src/kibana/components/vis/vis.js b/src/kibana/components/vis/vis.js index 4a20424116899..e5abf47cd17ca 100644 --- a/src/kibana/components/vis/vis.js +++ b/src/kibana/components/vis/vis.js @@ -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; }; -}); \ No newline at end of file +}); diff --git a/src/kibana/components/vislib/lib/data.js b/src/kibana/components/vislib/lib/data.js index db5fea220e4df..11818a22247ad 100644 --- a/src/kibana/components/vislib/lib/data.js +++ b/src/kibana/components/vislib/lib/data.js @@ -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; diff --git a/src/kibana/components/visualize/spy/spy.js b/src/kibana/components/visualize/spy/spy.js index fde6071e84cc7..2300c8791964b 100644 --- a/src/kibana/components/visualize/spy/spy.js +++ b/src/kibana/components/visualize/spy/spy.js @@ -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: $('
').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: $('
').appendTo($container) + }; + + current.$container.append($compile(newMode.template)(current.$scope)); + newMode.link && newMode.link(current.$scope, current.$container); }; } }; diff --git a/src/kibana/directives/paginate.js b/src/kibana/directives/paginate.js index 8a1c0665377cc..3f946514cb2b4 100644 --- a/src/kibana/directives/paginate.js +++ b/src/kibana/directives/paginate.js @@ -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; } diff --git a/src/kibana/directives/rows.js b/src/kibana/directives/rows.js index f8d39db74292f..4ec6e06ea8fc8 100644 --- a/src/kibana/directives/rows.js +++ b/src/kibana/directives/rows.js @@ -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(' '); + if (_.isObject(contents)) { + $cell.html($(contents.markup)); } else { - $cell.text(contents); + if (contents === '') { + $cell.html(' '); + } else { + $cell.text(contents); + } } + $tr.append($cell); } diff --git a/src/kibana/plugins/discover/components/field_chooser/lib/field_calculator.js b/src/kibana/plugins/discover/components/field_chooser/lib/field_calculator.js index 29c435a97dcca..a4164816a7e5e 100644 --- a/src/kibana/plugins/discover/components/field_chooser/lib/field_calculator.js +++ b/src/kibana/plugins/discover/components/field_chooser/lib/field_calculator.js @@ -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); diff --git a/src/kibana/plugins/discover/controllers/discover.js b/src/kibana/plugins/discover/controllers/discover.js index 083084fb054da..c65424dda1a66 100644 --- a/src/kibana/plugins/discover/controllers/discover.js +++ b/src/kibana/plugins/discover/controllers/discover.js @@ -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); diff --git a/src/kibana/plugins/discover/directives/table_row.js b/src/kibana/plugins/discover/directives/table_row.js index ef617f265649f..a4cfd87fbdc59 100644 --- a/src/kibana/plugins/discover/directives/table_row.js +++ b/src/kibana/plugins/discover/directives/table_row.js @@ -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 () { diff --git a/src/kibana/plugins/settings/sections/indices/_edit.html b/src/kibana/plugins/settings/sections/indices/_edit.html index b9644c5dfd2b5..bead028bb37e9 100644 --- a/src/kibana/plugins/settings/sections/indices/_edit.html +++ b/src/kibana/plugins/settings/sections/indices/_edit.html @@ -53,60 +53,15 @@

-
- - - - - - - - - - - - - - - - - -
name type - analyzed - - - indexed - - - popularity - -
- -   - - - - - - - - - - - - -
-
+
+ +
-
+
No scripted fields defined
diff --git a/src/kibana/plugins/settings/sections/indices/_edit.js b/src/kibana/plugins/settings/sections/indices/_edit.js index 0b74fcad21562..0b03000c944f2 100644 --- a/src/kibana/plugins/settings/sections/indices/_edit.js +++ b/src/kibana/plugins/settings/sections/indices/_edit.js @@ -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.'); diff --git a/src/kibana/plugins/settings/sections/indices/_popularity.html b/src/kibana/plugins/settings/sections/indices/_popularity.html new file mode 100644 index 0000000000000..a8d90dc6e9d51 --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/_popularity.html @@ -0,0 +1,9 @@ +
+ {{ field.count }} + + + + +
' \ No newline at end of file diff --git a/src/kibana/plugins/settings/styles/main.less b/src/kibana/plugins/settings/styles/main.less index da3e6567cd2ce..aa52bbe826d97 100644 --- a/src/kibana/plugins/settings/styles/main.less +++ b/src/kibana/plugins/settings/styles/main.less @@ -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 {} } diff --git a/test/unit/fixtures/fake_row.js b/test/unit/fixtures/fake_row.js index dc04f07edcf57..0760b02bbd721 100644 --- a/test/unit/fixtures/fake_row.js +++ b/test/unit/fixtures/fake_row.js @@ -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', diff --git a/test/unit/fixtures/stubbed_logstash_index_pattern.js b/test/unit/fixtures/stubbed_logstash_index_pattern.js index 7f319154ed82f..da9aace183705 100644 --- a/test/unit/fixtures/stubbed_logstash_index_pattern.js +++ b/test/unit/fixtures/stubbed_logstash_index_pattern.js @@ -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; + }; }); diff --git a/test/unit/specs/apps/discover/directives/field_calculator.js b/test/unit/specs/apps/discover/directives/field_calculator.js index 8bb37cb4d2089..d01d3c3b5fb6b 100644 --- a/test/unit/specs/apps/discover/directives/field_calculator.js +++ b/test/unit/specs/apps/discover/directives/field_calculator.js @@ -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); diff --git a/test/unit/specs/apps/discover/directives/field_chooser.js b/test/unit/specs/apps/discover/directives/field_chooser.js index 8431e5dbe2d2f..2f3a43806d46a 100644 --- a/test/unit/specs/apps/discover/directives/field_chooser.js +++ b/test/unit/specs/apps/discover/directives/field_chooser.js @@ -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 }); diff --git a/test/unit/specs/components/agg_table/_table.js b/test/unit/specs/components/agg_table/_table.js index 101b567fec07e..5b1d7dfd49c1c 100644 --- a/test/unit/specs/components/agg_table/_table.js +++ b/test/unit/specs/components/agg_table/_table.js @@ -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('')($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('')($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('')($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('')($scope); diff --git a/test/unit/specs/components/paginated_table/index.js b/test/unit/specs/components/paginated_table/index.js new file mode 100644 index 0000000000000..dfeec7a7c460c --- /dev/null +++ b/test/unit/specs/components/paginated_table/index.js @@ -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('')($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('')($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: '

I am HTML in a row

', + 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); + }); + }); + }); +}); diff --git a/test/unit/specs/components/vis/vis.js b/test/unit/specs/components/vis/vis.js new file mode 100644 index 0000000000000..88d33511e6de0 --- /dev/null +++ b/test/unit/specs/components/vis/vis.js @@ -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); + }); + }); + + }); +});