Merge branch 'master' into fix/desaturate-map-tiles

This commit is contained in:
Juan Thomassie 2014-12-31 10:20:15 -06:00
commit a8d6e0c651
43 changed files with 1341 additions and 353 deletions

View file

@ -0,0 +1,56 @@
define(function (require) {
var _ = require('lodash');
var INVALID = {}; // invalid flag
var FLOATABLE = /^[\d\.e\-\+]+$/i;
require('modules')
.get('kibana')
.directive('percentList', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $el, attrs, ngModelCntr) {
function parse(viewValue) {
if (!_.isString(viewValue)) return INVALID;
var nums = _(viewValue.split(','))
.invoke('trim')
.filter(Boolean)
.map(function (num) {
// prevent '100 boats' from passing
return FLOATABLE.test(num) ? parseFloat(num) : NaN;
});
var ration = nums.none(_.isNaN);
var ord = ration && nums.isOrdinal();
var range = ord && nums.min() >= 0 && nums.max() <= 100;
return range ? nums.value() : INVALID;
}
function makeString(list) {
if (!_.isArray(list)) return INVALID;
return list.join(', ');
}
function converter(/* fns... */) {
var fns = _.toArray(arguments);
return function (input) {
var value = input;
var valid = fns.every(function (fn) {
return (value = fn(value)) !== INVALID;
});
ngModelCntr.$setValidity('listInput', valid);
return valid ? value : void 0;
};
}
ngModelCntr.$parsers.push(converter(parse));
ngModelCntr.$formatters.push(converter(makeString));
}
};
});
});

View file

@ -1,6 +1,6 @@
<th width="1%"></th>
<th ng-if="timefield">
<span ng-click="sort(timefield)" tooltip="Sort by time">Time <i ng-class="headerClass(timefield)"></i></span>
<th ng-if="indexPattern.timeFieldName">
<span ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time">Time <i ng-class="headerClass(indexPattern.timeFieldName)"></i></span>
</th>
<th ng-repeat="name in columns">
<span ng-click="sort(name)" class="table-header-name" tooltip="Sort by {{name | shortDots}}">

View file

@ -5,17 +5,17 @@ define(function (require) {
require('filters/short_dots');
module.directive('kbnTableHeader', function () {
var headerHtml = require('text!plugins/discover/partials/table_header.html');
var headerHtml = require('text!components/doc_table/components/table_header.html');
return {
restrict: 'A',
scope: {
columns: '=',
sorting: '=',
indexPattern: '=',
timefield: '=?'
},
template: headerHtml,
controller: function ($scope) {
var sortableField = function (field) {
return $scope.indexPattern.fields.byName[field].sortable;
};

View file

@ -21,12 +21,12 @@ define(function (require) {
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
* ```
*/
module.directive('kbnTableRow', function ($compile, config, highlightFilter, shortDotsFilter) {
var openRowHtml = require('text!plugins/discover/partials/table_row/open.html');
var detailsHtml = require('text!plugins/discover/partials/table_row/details.html');
var cellTemplate = _.template(require('text!plugins/discover/partials/table_row/cell.html'));
module.directive('kbnTableRow', function ($compile, config, highlightFilter, shortDotsFilter, courier) {
var openRowHtml = require('text!components/doc_table/components/table_row/open.html');
var detailsHtml = require('text!components/doc_table/components/table_row/details.html');
var cellTemplate = _.template(require('text!components/doc_table/components/table_row/cell.html'));
var truncateByHeightTemplate = _.template(require('text!partials/truncate_by_height.html'));
var sourceTemplate = _.template(noWhiteSpace(require('text!plugins/discover/partials/table_row/_source.html')));
var sourceTemplate = _.template(noWhiteSpace(require('text!components/doc_table/components/table_row/_source.html')));
return {
restrict: 'A',
@ -34,7 +34,6 @@ define(function (require) {
columns: '=',
filter: '=',
indexPattern: '=',
timefield: '=?',
row: '=kbnTableRow'
},
link: function ($scope, $el, attrs) {
@ -42,6 +41,7 @@ define(function (require) {
$el.empty();
var init = function () {
_formatRow($scope.row);
createSummaryRow($scope.row, $scope.row._id);
};
@ -84,7 +84,7 @@ define(function (require) {
createSummaryRow($scope.row, $scope.row._id);
});
$scope.$watchMulti(['timefield', 'row.highlight'], function () {
$scope.$watchMulti(['indexPattern.timeFieldName', 'row.highlight'], function () {
createSummaryRow($scope.row, $scope.row._id);
});
@ -95,10 +95,10 @@ define(function (require) {
openRowHtml
];
if ($scope.timefield) {
if ($scope.indexPattern.timeFieldName) {
newHtmls.push(cellTemplate({
timefield: true,
formatted: _displayField(row, $scope.timefield)
formatted: _displayField(row, $scope.indexPattern.timeFieldName)
}));
}
@ -187,8 +187,8 @@ define(function (require) {
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];
// discover formats all of the values and puts them in $$_formatted for display
val = (row.$$_formatted || _formatRow(row))[field];
// undefined and null should just be an empty string
val = (val == null) ? '' : val;
@ -196,6 +196,26 @@ define(function (require) {
return val;
}
/*
* Format a field with the index pattern on scope.
*/
function _formatField(value, name) {
var defaultFormat = courier.indexPatterns.fieldFormats.defaultByType.string;
var field = $scope.indexPattern.fields.byName[name];
var formatter = (field && field.format) ? field.format : defaultFormat;
return formatter.convert(value);
}
/*
* Create the $$_formatted key on a row
*/
function _formatRow(row) {
row.$$_flattened = row.$$_flattened || $scope.indexPattern.flattenHit(row);
row.$$_formatted = row.$$_formatted || _.mapValues(row.$$_flattened, _formatField);
return row.$$_formatted;
}
init();
}
};

View file

@ -0,0 +1,36 @@
<paginate ng-if="!infiniteScroll" list="hits" per-page="50" top-controls="true">
<table class="kbn-table table" ng-if="indexPattern">
<thead
kbn-table-header
columns="persist.columns"
index-pattern="indexPattern"
sorting="persist.sorting">
</thead>
<tbody>
<tr ng-repeat="row in page|limitTo:limit track by row._index+row._id"
kbn-table-row="row"
columns="persist.columns"
sorting="persist.sorting"
index-pattern="indexPattern"
class="discover-table-row"></tr>
</tbody>
</table>
</paginate>
<table ng-if="infiniteScroll" class="kbn-table table" ng-if="indexPattern">
<thead
kbn-table-header
columns="persist.columns"
index-pattern="indexPattern"
sorting="persist.sorting">
</thead>
<tbody>
<tr ng-repeat="row in hits|limitTo:limit track by row._index+row._id"
kbn-table-row="row"
columns="persist.columns"
sorting="persist.sorting"
index-pattern="indexPattern"
class="discover-table-row"></tr>
</tbody>
</table>
<kbn-infinite-scroll ng-if="infiniteScroll" more="addRows"></kbn-infinite-scroll>

View file

@ -0,0 +1,94 @@
define(function (require) {
var _ = require('lodash');
var html = require('text!components/doc_table/doc_table.html');
var getSort = require('components/doc_table/lib/get_sort');
require('css!components/doc_table/doc_table.css');
require('directives/truncated');
require('components/doc_table/components/table_header');
require('components/doc_table/components/table_row');
require('modules').get('kibana')
.directive('docTable', function (config, Notifier) {
return {
restrict: 'E',
template: html,
scope: {
searchSource: '=',
sorting: '=',
columns: '=',
infiniteScroll: '=?',
filter: '=?',
},
link: function ($scope) {
var notify = new Notifier();
$scope.persist = {
sorting: $scope.sorting,
columns: $scope.columns
};
var prereq = (function () {
var fns = [];
return function register(fn) {
fns.push(fn);
return function () {
fn.apply(this, arguments);
if (fns.length) {
_.pull(fns, fn);
if (!fns.length) {
$scope.$root.$broadcast('ready:vis');
}
}
};
};
}());
$scope.addRows = function () {
$scope.limit += 50;
};
$scope.$on('$destroy', function () {
if ($scope.searchSource) $scope.searchSource.destroy();
});
$scope.$watch('searchSource', prereq(function (searchSource) {
if (!$scope.searchSource) return;
$scope.indexPattern = $scope.searchSource.get('index');
$scope.searchSource.size(config.get('discover:sampleSize'));
$scope.searchSource.sort(getSort($scope.sorting, $scope.indexPattern));
// Set the watcher after initialization
$scope.$watch('persist.sorting', function (newSort, oldSort) {
// Don't react if sort values didn't really change
if (newSort === oldSort) return;
$scope.searchSource.sort(getSort(newSort, $scope.indexPattern));
$scope.searchSource.fetchQueued();
});
// TODO: we need to have some way to clean up result requests
$scope.searchSource.onResults().then(function onResults(resp) {
// Reset infinite scroll limit
$scope.limit = 50;
// Abort if something changed
if ($scope.searchSource !== $scope.searchSource) return;
$scope.hits = resp.hits.hits;
return $scope.searchSource.onResults().then(onResults);
}).catch(notify.fatal);
$scope.searchSource.onError(notify.error).catch(notify.fatal);
}));
}
};
});
});

View file

@ -0,0 +1,9 @@
@import (reference) "../../styles/_bootstrap.less";
@import (reference) "../../styles/theme/_theme.less";
@import (reference) "lesshat.less";
doc-table {
overflow: auto;
margin: 5px;
.flex(1, 1, 100%);
}

View file

@ -0,0 +1,22 @@
define(function (require) {
var _ = require('lodash');
/**
* Take a sorting array and make it into an object
* @param {array} 2 item array [fieldToSort, directionToSort]
* @param {object} indexPattern used for determining default sort
* @returns {object} a sort object suitable for returning to elasticsearch
*/
return function (sort, indexPattern) {
var sortObj = {};
if (_.isArray(sort) && sort.length === 2) {
// At some point we need to refact the sorting logic, this array sucks.
sortObj[sort[0]] = sort[1];
} else if (indexPattern.timeFieldName) {
sortObj[indexPattern.timeFieldName] = 'desc';
} else {
sortObj._score = 'desc';
}
return sortObj;
};
});

View file

@ -209,10 +209,8 @@ define(function (require) {
* than the required number of data points
* @param {String} message - the message to provide with the error
*/
errors.NotEnoughData = function NotEnoughData() {
KbnError.call(this,
'There are not enough data points to render this chart',
errors.NotEnoughData);
errors.NotEnoughData = function NotEnoughData(message) {
KbnError.call(this, message, errors.NotEnoughData);
};
inherits(errors.NotEnoughData, KbnError);

View file

@ -265,13 +265,15 @@ define(function (require) {
AreaChart.prototype.checkIfEnoughData = function () {
var series = this.chartData.series;
var message = 'Area charts require more than one data point. Try adding ' +
'an X-Axis Aggregation';
var notEnoughData = series.some(function (obj) {
return obj.values.length < 2;
});
if (notEnoughData) {
throw new errors.NotEnoughData();
throw new errors.NotEnoughData(message);
}
};

View file

@ -186,7 +186,7 @@ define(function (require) {
color: self.darkerColor(defaultColor),
weight: 1.0,
opacity: 1,
fillOpacity: 0.7
fillOpacity: 0.75
};
}
}).addTo(map);
@ -247,7 +247,7 @@ define(function (require) {
color: self.darkerColor(color),
weight: 1.0,
opacity: 1,
fillOpacity: 0.7
fillOpacity: 0.75
};
}
}).addTo(map);
@ -303,14 +303,29 @@ define(function (require) {
var div = L.DomUtil.create('div', 'tilemap-legend');
var colors = self._attr.colors;
var labels = [];
for (var i = 0; i < colors.length; i++) {
var vals = self._attr.cScale.invertExtent(colors[i]);
var strokecol = self.darkerColor(colors[i]);
var i = 0;
var vals;
var strokecol;
if (data.properties.min === data.properties.max) {
// 1 val for legend
vals = self._attr.cScale.invertExtent(colors[i]);
strokecol = self.darkerColor(colors[i]);
labels.push(
'<i style="background:' + colors[i] + ';border-color:' + strokecol + '"></i> ' +
vals[0].toFixed(1) + ' &ndash; ' + vals[1].toFixed(1));
vals[0].toFixed(0));
} else {
// 3 to 5 vals for legend
for (i = 0; i < colors.length; i++) {
vals = self._attr.cScale.invertExtent(colors[i]);
strokecol = self.darkerColor(colors[i]);
labels.push(
'<i style="background:' + colors[i] + ';border-color:' + strokecol + '"></i> ' +
vals[0].toFixed(0) + ' &ndash; ' + vals[1].toFixed(0));
}
}
div.innerHTML = labels.join('<br>');
return div;
};
legend.addTo(map);
@ -407,7 +422,7 @@ define(function (require) {
};
/**
* radiusScale returns a circle radius from
* radiusScale returns a a number for scaled circle markers
* approx. square root of count
* which is multiplied by a factor based on the geohash precision
* for relative sizing of markers
@ -425,40 +440,40 @@ define(function (require) {
var maxr;
switch (precision) {
case 1:
maxr = 200;
maxr = 150;
break;
case 2:
maxr = 30;
maxr = 28;
break;
case 3:
maxr = 9;
maxr = 8;
break;
case 4:
maxr = 3;
maxr = 2;
break;
case 5:
maxr = 1.44;
maxr = 1.4;
break;
case 6:
maxr = 1.12;
break;
case 7:
maxr = 0.6;
break;
case 7:
maxr = 0.4;
break;
case 8:
maxr = 0.3;
maxr = 0.2;
break;
case 9:
maxr = 0.22;
maxr = 0.12;
break;
default:
maxr = 9;
maxr = 8;
}
return Math.pow(count, exp) / Math.pow(max, exp) * maxr;
};
/**
* returns a number to scale circle markers
* returns a number to scale shaded circle markers
* based on the geohash precision
*
* @method quantRadiusScale
@ -469,34 +484,34 @@ define(function (require) {
var maxr;
switch (precision) {
case 1:
maxr = 100;
maxr = 150;
break;
case 2:
maxr = 12;
maxr = 18;
break;
case 3:
maxr = 3;
maxr = 4.5;
break;
case 4:
maxr = 0.6;
maxr = 0.7;
break;
case 5:
maxr = 0.3;
maxr = 0.26;
break;
case 6:
maxr = 0.22;
maxr = 0.20;
break;
case 7:
maxr = 0.18;
break;
case 8:
maxr = 0.16;
break;
case 8:
maxr = 0.13;
break;
case 9:
maxr = 0.14;
maxr = 0.11;
break;
default:
maxr = 3;
maxr = 4.5;
}
return maxr;
};
@ -513,14 +528,20 @@ define(function (require) {
*/
TileMap.prototype.quantizeColorScale = function (count, min, max) {
var self = this;
var greens = ['#c7e9b4', '#7fcdbb', '#41b6c4', '#2c7fb8', '#253494'];
var reds = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
var blues = ['#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#084594'];
var colors = self._attr.colors = reds;
var reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
var reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
var reds1 = ['#ff6128'];
var colors = self._attr.colors = reds5;
if (max - min < 3) {
colors = self._attr.colors = reds1;
} else if (max - min < 25) {
colors = self._attr.colors = reds3;
}
var cScale = self._attr.cScale = d3.scale.quantize()
.domain([min, max])
.range(colors);
if (max === min) {
return colors[0];
} else {
@ -553,4 +574,4 @@ define(function (require) {
return TileMap;
};
});
});

View file

@ -9,12 +9,18 @@ define(function (require) {
restrict: 'E',
scope: true,
link: {
pre: function ($scope, $el) {
if ($el.find('paginate-controls').size() === 0) {
$el.append($compile('<paginate-controls>')($scope));
pre: function ($scope, $el, attrs) {
if (_.isUndefined(attrs.bottomControls)) attrs.bottomControls = true;
if ($el.find('paginate-controls.paginate-bottom').size() === 0 && attrs.bottomControls) {
$el.append($compile('<paginate-controls class="paginate-bottom">')($scope));
}
},
post: function ($scope, $el, attrs) {
if (_.isUndefined(attrs.topControls)) attrs.topControls = false;
if ($el.find('paginate-controls.paginate-top').size() === 0 && attrs.topControls) {
$el.prepend($compile('<paginate-controls class="paginate-top">')($scope));
}
var paginate = $scope.paginate;
// add some getters to the controller powered by attributes

View file

@ -0,0 +1,18 @@
define(function (require) {
var _ = require('lodash');
return function loadPanelFunction(Private) { // Inject services here
return function (panel, $scope) { // Function parameters here
var panelTypes = {
visualization: Private(require('plugins/dashboard/components/panel/lib/visualization')),
search: Private(require('plugins/dashboard/components/panel/lib/search'))
};
try {
return panelTypes[panel.type](panel, $scope);
} catch (e) {
throw new Error('Loader not found for unknown panel type: ' + panel.type);
}
};
};
});

View file

@ -0,0 +1,14 @@
define(function (require) {
return function searchLoader(savedSearches, Private) { // Inject services here
return function (panel, $scope) { // Function parameters here
return savedSearches.get(panel.id)
.then(function (savedSearch) {
return {
savedObj: savedSearch,
panel: panel,
edit: '#discover'
};
});
};
};
});

View file

@ -0,0 +1,21 @@
define(function (require) {
return function visualizationLoader(savedVisualizations, Private) { // Inject services here
var brushEvent = Private(require('utils/brush_event'));
var filterBarClickHandler = Private(require('components/filter_bar/filter_bar_click_handler'));
return function (panel, $scope) { // Function parameters here
return savedVisualizations.get(panel.id)
.then(function (savedVis) {
// $scope.state comes via $scope inheritence from the dashboard app. Don't love this.
savedVis.vis.listeners.click = filterBarClickHandler($scope.state);
savedVis.vis.listeners.brush = brushEvent;
return {
savedObj: savedVis,
panel: panel,
edit: '#visualize/edit'
};
});
};
};
});

View file

@ -0,0 +1,28 @@
<div class="panel panel-default" ng-switch on="panel.type" ng-if="savedObj || error">
<div class="panel-heading">
<span class="panel-title">{{savedObj.title}}</span>
<div class="btn-group">
<a ng-show="!appEmbedded" ng-href="{{edit}}/{{panel.id | uriescape}}"><i class="fa fa-pencil"></i></a>
<a ng-show="!appEmbedded" ng-click="remove()"><i class="fa fa-times"></i></a>
</div>
<div class="clearfix"></div>
</div>
<div ng-if="error" class="load-error">
<i class="fa fa-exclamation-triangle"></i>
<span ng-bind="error"></span>
</div>
<visualize ng-switch-when="visualization"
vis="savedObj.vis"
search-source="savedObj.searchSource"
class="panel-content">
</visualize>
<doc-table ng-switch-when="search"
search-source="savedObj.searchSource"
sorting="savedObj.sort"
columns="savedObj.columns"
class="panel-content">
</doc-table>
</div>

View file

@ -0,0 +1,46 @@
define(function (require) {
var moment = require('moment');
var $ = require('jquery');
require('modules')
.get('app/dashboard')
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $compile) {
var _ = require('lodash');
var filterBarClickHandler = Private(require('components/filter_bar/filter_bar_click_handler'));
var loadPanel = Private(require('plugins/dashboard/components/panel/lib/load_panel'));
var notify = new Notifier();
require('components/visualize/visualize');
require('components/doc_table/doc_table');
var brushEvent = Private(require('utils/brush_event'));
return {
restrict: 'E',
template: require('text!plugins/dashboard/components/panel/panel.html'),
requires: '^dashboardGrid',
link: function ($scope, $el) {
// using $scope inheritance, panels are available in AppState
var $state = $scope.state;
// receives $scope.panel from the dashboard grid directive, seems like should be isolate?
$scope.$watch('id', function (id) {
if (!$scope.panel.id || !$scope.panel.type) return;
loadPanel($scope.panel, $scope).then(function (panelConfig) {
// These could be done in loadPanel, putting them here to make them more explicit
$scope.savedObj = panelConfig.savedObj;
$scope.edit = panelConfig.edit;
$scope.$on('$destroy', panelConfig.savedObj.destroy);
}).catch(function (e) {
$scope.error = e.message;
});
});
$scope.remove = function () {
_.pull($state.panels, $scope.panel);
};
}
};
});
});

View file

@ -138,7 +138,16 @@ define(function (require) {
});
// ignore panels that don't have vis id's
if (!panel.visId) throw new Error('missing visId on panel');
if (!panel.id) {
// In the interest of backwards compat
if (panel.visId) {
panel.id = panel.visId;
panel.type = 'visualization';
delete panel.visId;
} else {
throw new Error('missing object id on panel');
}
}
panel.$scope = $scope.$new();
panel.$scope.panel = panel;

View file

@ -1,45 +0,0 @@
define(function (require) {
var moment = require('moment');
require('modules')
.get('app/dashboard')
.directive('dashboardPanel', function (savedVisualizations, Notifier, Private) {
var _ = require('lodash');
var filterBarClickHandler = Private(require('components/filter_bar/filter_bar_click_handler'));
var notify = new Notifier();
require('components/visualize/visualize');
var brushEvent = Private(require('utils/brush_event'));
return {
restrict: 'E',
template: require('text!plugins/dashboard/partials/panel.html'),
requires: '^dashboardGrid',
link: function ($scope, $el) {
// using $scope inheritance, panels are available in AppState
var $state = $scope.state;
// receives panel object from the dashboard grid directive
$scope.$watch('visId', function (visId) {
delete $scope.vis;
if (!$scope.panel.visId) return;
savedVisualizations.get($scope.panel.visId)
.then(function (savedVis) {
$scope.savedVis = savedVis;
$scope.$on('$destroy', savedVis.destroy);
savedVis.vis.listeners.click = filterBarClickHandler($state);
savedVis.vis.listeners.brush = brushEvent;
})
.catch(function (e) {
$scope.error = e.message;
});
});
$scope.remove = function () {
_.pull($state.panels, $scope.panel);
};
}
};
});
});

View file

@ -13,7 +13,7 @@ define(function (require) {
require('plugins/dashboard/directives/grid');
require('plugins/dashboard/directives/panel');
require('plugins/dashboard/components/panel/panel');
require('plugins/dashboard/services/saved_dashboards');
require('css!plugins/dashboard/styles/main.css');
@ -47,7 +47,7 @@ define(function (require) {
}
});
app.directive('dashboardApp', function (Notifier, courier, savedVisualizations, AppState, timefilter, kbnUrl) {
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl) {
return {
controller: function ($scope, $route, $routeParams, $location, configFile, Private) {
var notify = new Notifier({
@ -169,7 +169,12 @@ define(function (require) {
// called by the saved-object-finder when a user clicks a vis
$scope.addVis = function (hit) {
pendingVis++;
$state.panels.push({ visId: hit.id });
$state.panels.push({ id: hit.id, type: 'visualization' });
};
$scope.addSearch = function (hit) {
pendingVis++;
$state.panels.push({ id: hit.id, type: 'search' });
};
// Setup configurable values for config directive, after objects are initialized
@ -177,6 +182,7 @@ define(function (require) {
dashboard: dash,
save: $scope.save,
addVis: $scope.addVis,
addSearch: $scope.addSearch,
shareData: function () {
return {
link: $location.absUrl(),

View file

@ -1,16 +0,0 @@
<div class="panel panel-default">
<div class="panel-heading">
<span class="panel-title">{{savedVis.title}}</span>
<div class="btn-group">
<a ng-show="!appEmbedded" ng-href="#visualize/edit/{{panel.visId | uriescape}}"><i class="fa fa-pencil"></i></a>
<a ng-show="!appEmbedded" ng-click="remove()"><i class="fa fa-times"></i></a>
</div>
<div class="clearfix"></div>
</div>
<div ng-if="error" class="load-error">
<i class="fa fa-exclamation-triangle"></i>
<span ng-bind="error"></span>
</div>
<visualize ng-if="savedVis" vis="savedVis.vis" search-source="savedVis.searchSource"></visualize>
</div>

View file

@ -1 +1,14 @@
<saved-object-finder title="Choose a visualization" type="visualizations" on-choose="opts.addVis"></saved-object-finder>
<div ng-switch on="mode" ng-init="mode = 'visualization'" class="dashboard-panel-picker">
<ul class="nav nav-tabs">
<li ng-class="{active: mode == 'visualization'}"><a ng-click="mode='visualization'">Visualizations</a></li>
<li ng-class="{active: mode == 'search'}"><a ng-click="mode='search'">Searches</a></li>
</ul>
<li class="list-group-item" ng-switch-when="visualization">
<saved-object-finder title="Choose a visualization" type="visualizations" on-choose="opts.addVis"></saved-object-finder>
</li>
<li class="list-group-item" ng-switch-when="search">
<saved-object-finder title="Choose a search" type="searches" on-choose="opts.addSearch"></saved-object-finder>
</li>
</div>

View file

@ -101,7 +101,8 @@ dashboard-grid {
}
}
visualize {
.panel-content {
display: flex;
.flex(1, 1, 100%);
height: auto;
}
@ -109,6 +110,10 @@ dashboard-grid {
}
}
.dashboard-panel-picker > li.list-group-item {
border-top: 0px;
}
.dashboard-load {
margin: 10px;
}

View file

@ -3,8 +3,8 @@ define(function (require) {
require('directives/truncated');
require('directives/infinite_scroll');
require('plugins/discover/directives/table_header');
require('plugins/discover/directives/table_row');
require('components/doc_table/components/table_header');
require('components/doc_table/components/table_row');
var module = require('modules').get('app/discover');
@ -22,14 +22,12 @@ define(function (require) {
restrict: 'E',
template: html,
scope: {
fields: '=',
columns: '=',
rows: '=',
sorting: '=',
filtering: '=',
refresh: '=',
indexPattern: '=',
timefield: '=?'
},
link: function ($scope, $el) {
$scope.limit = 50;

View file

@ -160,7 +160,6 @@
sorting="state.sort"
filtering="filterQuery"
refresh="fetch"
timefield="opts.timefield"
index-pattern="indexPattern">
</kbn-table>

View file

@ -4,14 +4,12 @@
columns="columns"
index-pattern="indexPattern"
sorting="sorting"
timefield="timefield">
</thead>
<tbody>
<tr ng-repeat="row in rows |limitTo:limit track by row._index+row._id"
kbn-table-row="row"
columns="columns"
sorting="sorting"
timefield="timefield"
filter="filtering"
index-pattern="indexPattern"
class="discover-table-row"></tr>

View file

@ -1,4 +1,4 @@
kbn-table,tbody[kbn-rows] {
kbn-table, .kbn-table, tbody[kbn-rows] {
// sub tables should not have a leading border
.table .table {
margin-bottom: 0px;

View file

@ -264,7 +264,7 @@ notifications {
//== Table
kbn-table {
kbn-table, .kbn-table {
font-size: @font-size-small;
th {

View file

@ -11,198 +11,8 @@ define(function (require) {
*/
var _ = require('lodash_src');
_.mixin(require('lodash-deep'));
_.mixin({
inherits: function (Sub, Super) {
Sub.prototype = _.create(Super.prototype, { 'constructor': Sub });
Sub.Super = Super;
},
remove: function (array, index) {
array.splice(index, 1);
return array;
},
// If variable is value, then return alt. If variable is anything else, return value;
toggle: function (variable, value, alt) {
return variable === value ? alt : value;
},
toggleInOut: function (array, value) {
if (_.contains(array, value)) {
array = _.without(array, value);
} else {
array.push(value);
}
return array;
},
// NOTE: The flatten behavior here works if you don't need to keep a reference to the
// original value
flattenWith: function (dot, nestedObj, flattenArrays) {
var key; // original key
var stack = []; // track key stack
var flatObj = {};
(function flattenObj(obj) {
_.keys(obj).forEach(function (key) {
stack.push(key);
if (!flattenArrays && _.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key];
else if (_.isObject(obj[key])) flattenObj(obj[key]);
else flatObj[stack.join(dot)] = obj[key];
stack.pop();
});
}(nestedObj));
return flatObj;
},
// assign the properties of an object's subObject to the parent object.
// obj = { prop: { a: 1} } ===> obj = { a: 1 }
unwrapProp: function (obj, prop) {
var wrapped = obj[prop];
delete obj[prop];
_.assign(obj, wrapped);
},
optMemoize: function (fn) {
var memo = _.memoize(fn);
return function () {
if (arguments[0] == null) {
return fn.apply(this, arguments);
} else {
return memo.apply(this, arguments);
}
};
},
isNumeric: function (v) {
return !_.isNaN(v) && (typeof v === 'number' || (!_.isArray(v) && !_.isNaN(parseInt(v, 10))));
},
setValue: function (obj, name, value) {
var path = name.split('.');
var current = obj;
(function recurse() {
var step = path.shift();
if (path.length === 0) {
current[step] = value;
}
if (_.isObject(current)) {
current = current[step];
recurse();
}
}());
},
// limit the number of arguments that are passed to the function
limit: function (context, fn, count) {
// syntax without context limit(fn, 1)
if (count == null && _.isNumeric(fn)) {
count = fn;
fn = context;
context = null;
}
count = count || 0;
// shortcuts for common paths
// !!!! PLEASE don't use more than two arg
if (count === 0) return function () { return fn.call(context); };
if (count === 1) return function (a) { return fn.call(context, a); };
if (count === 2) return function (a, b) { return fn.call(context, a, b); };
// catch all version
return function () {
return fn.apply(context, [].slice.call(arguments, 0, count));
};
},
// call all functions in an array
callEach: function (arr) {
_.invoke(arr, 'call');
},
onceWithCb: function (fn) {
var callbacks = [];
// on initial flush, call the init function, but ensure
// that it only happens once
var flush = _.once(function (cntx, args) {
args.push(function finishedOnce() {
// override flush to simply schedule an asynchronous clear
flush = function () {
setTimeout(function () {
_.callEach(callbacks.splice(0));
}, 0);
};
flush();
});
fn.apply(cntx, args);
});
return function runOnceWithCb() {
var args = [].slice.call(arguments, 0);
var cb = args[args.length - 1];
if (typeof cb === 'function') {
callbacks.push(cb);
// trim the arg list so the other callback can
// be pushed if needed
args = args.slice(0, -1);
}
// always call flush, it might not do anything
flush(this, args);
};
},
chunk: function (arr, count) {
var size = Math.ceil(arr.length / count);
var chunks = new Array(count);
for (var i = 0; i < count; i ++) {
var start = i * size;
chunks[i] = arr.slice(start, start + size);
}
return chunks;
},
repeat: function (string, times) {
var out = '';
for (var i = 0; i < times; i++) out += string;
return out;
},
/**
* move an obj either up or down in the collection by
* injecting it either before/after the prev/next obj that
* satisfied the qualifier
*
* or, just from one index to another...
*
* @param {array} objs - the list to move the object within
* @param {number|any} obj - the object that should be moved, or the index that the object is currently at
* @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down
* @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck
* @return {array} - the objs argument
*/
move: function (objs, obj, below, qualifier) {
var origI = _.isNumber(obj) ? obj : objs.indexOf(obj);
if (origI === -1) return objs;
if (_.isNumber(below)) {
// move to a specific index
objs.splice(below, 0, objs.splice(origI, 1)[0]);
return objs;
}
below = !!below;
qualifier = _.createCallback(qualifier, null, 2);
var above = !below;
var finder = below ? _.findIndex : _.findLastIndex;
// find the index of the next/previous obj that meets the qualifications
var targetI = finder(objs, function (otherAgg, otherI) {
if (below && otherI <= origI) return;
if (above && otherI >= origI) return;
return !!qualifier(otherAgg, otherI);
});
if (targetI === -1) return objs;
// place the obj at it's new index
objs.splice(targetI, 0, objs.splice(origI, 1)[0]);
}
});
_.mixin(require('utils/_mixins_chainable'), { chain: true });
_.mixin(require('utils/_mixins_notchainable'), { chain: false });
return _;
});

View file

@ -0,0 +1,181 @@
define(function (require) {
/**
* THESE ARE AUTOMATICALLY INCLUDED IN LODASH
*
* use:
* var _ = require('lodash');
*
* require.js config points the 'lodash' id to
* this module, which provides a modified version
* of lodash.
*/
var _ = require('lodash_src');
return {
/**
* Remove an element at a specific index from an array
*
* @param {array} arr
* @param {number} index
* @return {array} arr
*/
remove: function (arr, index) {
arr.splice(index, 1);
return arr;
},
/**
* Remove or add a value to an array based on it's presense in the
* array initially.
*
* @param {array} arr
* @param {any} value - the value to toggle
* @return {array} arr
*/
toggleInOut: function (arr, value) {
if (_.contains(arr, value)) {
arr = _.without(arr, value);
} else {
arr.push(value);
}
return arr;
},
/**
* Flatten an object into a single-level object.
* NOTE: The flatten behavior here works if you don't need to keep a reference to the original value
*
* set flattenArrays to traverse into arrays and create properties like:
* {
* 'users.0.name': 'username1',
* 'users.1.name': 'username2',
* 'users.2.name': 'username3',
* }
*
* @param {string} dot - the seperator for keys, '.' is generally preferred
* @param {object} nestedObj - the object to flatten
* @param {Boolean} flattenArrays - should arrays be travered or left alone?
* @return {object}
*/
flattenWith: function (dot, nestedObj, flattenArrays) {
var key; // original key
var stack = []; // track key stack
var flatObj = {};
(function flattenObj(obj) {
_.keys(obj).forEach(function (key) {
stack.push(key);
if (!flattenArrays && _.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key];
else if (_.isObject(obj[key])) flattenObj(obj[key]);
else flatObj[stack.join(dot)] = obj[key];
stack.pop();
});
}(nestedObj));
return flatObj;
},
/**
* assign the properties of an object's subObject to the parent object.
*
* var obj = { prop: { a: 1} };
* _.unwrapProp(obj, 'prop'); // { a: 1 };
*
* @param {[type]} obj [description]
* @param {[type]} prop [description]
* @return {[type]} [description]
*/
unwrapProp: function (obj, prop) {
var wrapped = obj[prop];
delete obj[prop];
_.assign(obj, wrapped);
return obj;
},
/**
* Memoize a function, but only use the memoized version if
* a first argument is passed, otherwise execute the original method
* every time.
*
* @param {Function} fn
* @return {Function}
*/
optMemoize: function (fn) {
var memo = _.memoize(fn);
return function () {
if (arguments[0] == null) {
return fn.apply(this, arguments);
} else {
return memo.apply(this, arguments);
}
};
},
/**
* Alias to _.deepSet
*/
setValue: function (obj, path, value) {
_.deepSet(obj, path, value);
return obj;
},
/**
* Split an array into a series of smaller arrays, containing
* portions of the previous array.
*
* @param {array} arr - the array to chunk
* @param {number} count - the number of chunks to create
* @return {array[array]} - array of slice of arr
*/
chunk: function (arr, count) {
var size = Math.ceil(arr.length / count);
var chunks = new Array(count);
for (var i = 0; i < count; i ++) {
var start = i * size;
chunks[i] = arr.slice(start, start + size);
}
return chunks;
},
/**
* move an obj either up or down in the collection by
* injecting it either before/after the prev/next obj that
* satisfied the qualifier
*
* or, just from one index to another...
*
* @param {array} objs - the list to move the object within
* @param {number|any} obj - the object that should be moved, or the index that the object is currently at
* @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down
* @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck
* @return {array} - the objs argument
*/
move: function (objs, obj, below, qualifier) {
var origI = _.isNumber(obj) ? obj : objs.indexOf(obj);
if (origI === -1) return objs;
if (_.isNumber(below)) {
// move to a specific index
objs.splice(below, 0, objs.splice(origI, 1)[0]);
return objs;
}
below = !!below;
qualifier = _.createCallback(qualifier, null, 2);
var above = !below;
var finder = below ? _.findIndex : _.findLastIndex;
// find the index of the next/previous obj that meets the qualifications
var targetI = finder(objs, function (otherAgg, otherI) {
if (below && otherI <= origI) return;
if (above && otherI >= origI) return;
return !!qualifier(otherAgg, otherI);
});
if (targetI === -1) return objs;
// place the obj at it's new index
objs.splice(targetI, 0, objs.splice(origI, 1)[0]);
}
};
});

View file

@ -0,0 +1,185 @@
define(function (require) {
/**
* THESE ARE AUTOMATICALLY INCLUDED IN LODASH
*
* use:
* var _ = require('lodash');
*
* require.js config points the 'lodash' id to
* this module, which provides a modified version
* of lodash.
*/
var _ = require('lodash_src');
return {
/**
* Setup Class-like inheritance between two constructors.
* Exposes the Super class at SubClass.Super;
*
* @param {Constructor} Sub - The "Class" that should be extended
* @param {Constructor} Super - The parent "Class"
* @return {Constructor} - the sub argument;
*/
inherits: function (Sub, Super) {
Sub.prototype = _.create(Super.prototype, { 'constructor': Sub });
Sub.Super = Super;
return Sub;
},
/**
* Create a string by repeating another string n-times
*
* @param {string} str - the string to repeat
* @param {number} times - the number of times to repeat the string
* @return {string}
*/
repeat: function (str, times) {
var out = '';
for (var i = 0; i < times; i++) out += str;
return out;
},
/**
* If current is value (===), then return alt. If current is anything else, return value
*
* @param {any} current
* @param {any} value
* @param {any} alt
* @return {any} alt|value
*/
toggle: function (current, value, alt) {
return current === value ? alt : value;
},
/**
* Inverse of _.some(), reads better in some cases,
*
* @param {array} arr
* @param {Function} fn
* @param {any} cntx
* @return {Boolean}
*/
none: function (arr, fn, cntx) {
return !_.some(arr, fn, cntx);
},
/**
* check if the values in an array are all numbers, unique, and
* @param {[type]} arr [description]
* @return {Boolean} [description]
*/
isOrdinal: function (arr) {
if (!_.isArray(arr)) return false;
return _.all(arr, function (num, i, arr) {
if (!_.isNumber(num)) return false;
if (i === 0) return true;
return num > arr[i - 1];
});
},
/**
* Checks to see if an input value is number-like, this
* includes strings that parse into valid numbers and objects
* that don't have a type of number but still parse properly
* via-some sort of valueOf magic
*
* @param {any} v - the value to check
* @return {Boolean}
*/
isNumeric: function (v) {
return !_.isNaN(v) && (typeof v === 'number' || (!_.isArray(v) && !_.isNaN(parseFloat(v))));
},
/**
* Create a method that wraps another method which expects a callback as it's last
* argument. The wrapper method will call the wrapped function only once (the first
* time it is called), but will always call the callbacks passed to it. This has a
* similar effect to calling a promise-returning function that is wrapped with _.once
* but can be used outside of angular.
*
* @param {Function} fn - the function that should only be executed once and accepts
* a callback as it's last arg
* @return {Function} - the wrapper method
*/
onceWithCb: function (fn) {
var callbacks = [];
// on initial flush, call the init function, but ensure
// that it only happens once
var flush = _.once(function (cntx, args) {
args.push(function finishedOnce() {
// override flush to simply schedule an asynchronous clear
flush = function () {
setTimeout(function () {
_.callEach(callbacks.splice(0));
}, 0);
};
flush();
});
fn.apply(cntx, args);
});
return function runOnceWithCb() {
var args = [].slice.call(arguments, 0);
var cb = args[args.length - 1];
if (typeof cb === 'function') {
callbacks.push(cb);
// trim the arg list so the other callback can
// be pushed if needed
args = args.slice(0, -1);
}
// always call flush, it might not do anything
flush(this, args);
};
},
/**
* Create a function that will ignore all but n-number of arguments.
* This is useful for passing functions like _.parseInt to Array#map.
* Since _.parseInt accepts a second argument, it will try to use the
* index of the value passed as the base for that number and weird
* errors occur.
*
* @param {this} [context] - this value for fn, optional
* @param {Function} fn - the function to wrap
* @param {number} count - the number of args to accept
* @return {Function}
*/
limit: function (context, fn, count) {
// syntax without context limit(fn, 1)
if (count == null && _.isNumeric(fn)) {
count = fn;
fn = context;
context = null;
}
count = count || 0;
// shortcuts for common paths
// !!!! PLEASE don't use more than two arg
if (count === 0) return function () { return fn.call(context); };
if (count === 1) return function (a) { return fn.call(context, a); };
if (count === 2) return function (a, b) { return fn.call(context, a, b); };
// catch all version
return function () {
return fn.apply(context, [].slice.call(arguments, 0, count));
};
},
/**
* Call all of the function in an array
*
* @param {array[functions]} arr
* @return {undefined}
*/
callEach: function (arr) {
_.invoke(arr, 'call');
}
};
});

View file

@ -4,6 +4,7 @@ define(function (require) {
{ name: 'bytes', type: 'number', indexed: true, analyzed: true, count: 10 },
{ name: 'ssl', type: 'boolean', indexed: true, analyzed: true, count: 20 },
{ name: '@timestamp', type: 'date', indexed: true, analyzed: true, count: 30 },
{ name: 'time', type: 'date', indexed: true, analyzed: true, count: 30 },
{ name: 'utc_time', type: 'date', indexed: true, analyzed: true },
{ name: 'phpmemory', type: 'number', indexed: true, analyzed: true },
{ name: 'ip', type: 'ip', indexed: true, analyzed: true },

View file

@ -0,0 +1,18 @@
define(function (require) {
var hits = require('fixtures/real_hits');
return {
took: 73,
timed_out: false,
_shards: {
total: 144,
successful: 144,
failed: 0
},
hits: {
total : 49487,
max_score : 1.0,
hits: hits
}
};
});

View file

@ -0,0 +1,36 @@
define(function (require) {
var sinon = require('test_utils/auto_release_sinon');
var searchResponse = require('fixtures/search_response');
return function stubSearchSource(Private, $q) {
var deferedResult = $q.defer();
return {
sort: sinon.spy(),
size: sinon.spy(),
fetch: sinon.spy(),
destroy: sinon.spy(),
get: function (param) {
switch (param) {
case 'index':
return Private(require('fixtures/stubbed_logstash_index_pattern'));
default:
throw new Error('Param "' + param + '" is not implemented in the stubbed search source');
}
},
crankResults: function () {
deferedResult.resolve(searchResponse);
deferedResult = $q.defer();
},
onResults: function () {
// Up to the test to resolve this manually
// For example:
// someHandler.resolve(require('fixtures/search_response'))
return deferedResult.promise;
},
onError: function () { return $q.defer().promise; },
};
};
});

View file

@ -41,16 +41,8 @@ define(function (require) {
// For testing column removing/adding for the header and the rows
//
var columnTests = function (elemType, parentElem) {
it('should create only the toggle column by default', function (done) {
var childElems = parentElem.find(elemType);
expect(childElems.length).to.be(1);
done();
});
it('should create a time column if the timefield is defined', function (done) {
// Should include a column for toggling and the time column by default
$parentScope.timefield = '@timestamp';
parentElem.scope().$digest();
var childElems = parentElem.find(elemType);
expect(childElems.length).to.be(2);
done();
@ -62,22 +54,32 @@ define(function (require) {
$parentScope.columns = ['bytes'];
parentElem.scope().$digest();
childElems = parentElem.find(elemType);
expect(childElems.length).to.be(2);
expect($(childElems[1]).text()).to.contain('bytes');
expect(childElems.length).to.be(3);
expect($(childElems[2]).text()).to.contain('bytes');
$parentScope.columns = ['bytes', 'request_body'];
parentElem.scope().$digest();
childElems = parentElem.find(elemType);
expect(childElems.length).to.be(3);
expect($(childElems[2]).text()).to.contain('request_body');
expect(childElems.length).to.be(4);
expect($(childElems[3]).text()).to.contain('request_body');
$parentScope.columns = ['request_body'];
parentElem.scope().$digest();
childElems = parentElem.find(elemType);
expect(childElems.length).to.be(2);
expect($(childElems[1]).text()).to.contain('request_body');
expect(childElems.length).to.be(3);
expect($(childElems[2]).text()).to.contain('request_body');
done();
});
it('should create only the toggle column if there is no timeField', function (done) {
delete parentElem.scope().indexPattern.timeFieldName;
parentElem.scope().$digest();
var childElems = parentElem.find(elemType);
expect(childElems.length).to.be(1);
done();
});
};
@ -86,7 +88,7 @@ define(function (require) {
describe('kbnTableHeader', function () {
var $elem = angular.element(
'<thead kbn-table-header columns="columns" index-pattern="indexPattern" sort="sort" timefield="timefield"></thead>'
'<thead kbn-table-header columns="columns" index-pattern="indexPattern" sort="sort"></thead>'
);
beforeEach(function () {
@ -206,7 +208,6 @@ define(function (require) {
'filter="filtering"' +
'maxLength=maxLength ' +
'index-pattern="indexPattern"' +
'timefield="timefield" ' +
'></thead>'
);
@ -227,7 +228,6 @@ define(function (require) {
sorting: [],
filtering: sinon.spy(),
maxLength: 50,
timefield: '@timestamp'
});
});
afterEach(function () {
@ -265,7 +265,6 @@ define(function (require) {
'sorting="sorting"' +
'filter="filter"' +
'index-pattern="indexPattern"' +
'timefield="timefield" ' +
'></tr>'
);
@ -335,7 +334,6 @@ define(function (require) {
'sorting="sorting"' +
'filtering="filtering"' +
'index-pattern="indexPattern"' +
'timefield="timefield" ' +
'></tr>'
);
var $details;
@ -378,7 +376,7 @@ define(function (require) {
var $before;
beforeEach(module('kibana', 'apps/discover'));
beforeEach(inject(function ($rootScope, $compile) {
beforeEach(inject(function ($rootScope, $compile, Private) {
$root = $rootScope;
$root.row = getFakeRow(0, mapping);
$root.columns = ['_source'];
@ -386,7 +384,7 @@ define(function (require) {
$root.filtering = sinon.spy();
$root.maxLength = 50;
$root.mapping = mapping;
$root.timefield = '@timestamp';
$root.indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
$row = $('<tr>')
.attr({
@ -395,7 +393,6 @@ define(function (require) {
'sorting': 'sortin',
'filtering': 'filtering',
'index-pattern': 'indexPattern',
'timefield': 'timefield',
});
$scope = $root.$new();
@ -405,7 +402,7 @@ define(function (require) {
$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(1).text().trim()).to.match(/^time_formatted/);
expect($before.eq(2).find('dl dt').length).to.be(_.keys($scope.row.$$_flattened).length);
}));

View file

@ -0,0 +1,80 @@
define(function (require) {
describe('PercentList directive', function () {
var $ = require('jquery');
var _ = require('lodash');
require('components/agg_types/controls/_percent_list');
var $el;
var $scope;
var $compile;
var $rootScope;
beforeEach(module('kibana'));
beforeEach(inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
$el = $('<input>').attr('ng-model', 'val').attr('percent-list', '');
$scope = $rootScope.$new();
}));
afterEach(function () {
$el.remove();
$scope.$destroy();
});
it('filters out empty entries', function () {
$compile($el)($scope);
$el.val(',,1, ,, ,2, ,\n,');
$el.change();
expect($scope.val).to.eql([1, 2]);
});
it('fails on invalid numbers', function () {
$compile($el)($scope);
$el.val('foo,bar');
$el.change();
expect($scope.val).to.be(undefined);
expect($el.hasClass('ng-invalid')).to.be(true);
});
it('supports decimals', function () {
$compile($el)($scope);
$el.val('1.2,000001.6,99.10');
$el.change();
expect($scope.val).to.eql([1.2, 1.6, 99.10]);
});
it('ensures that the values are in order', function () {
$compile($el)($scope);
$el.val('1, 2, 3, 10, 4, 5');
$el.change();
expect($scope.val).to.be(undefined);
expect($el.hasClass('ng-invalid')).to.be(true);
});
it('ensures that the values are less between 0 and 100', function () {
$compile($el)($scope);
$el.val('-1, 0, 1');
$el.change();
expect($scope.val).to.be(undefined);
expect($el.hasClass('ng-invalid')).to.be(true);
$el.val('0, 1');
$el.change();
expect($scope.val).to.eql([0, 1]);
expect($el.hasClass('ng-invalid')).to.be(false);
$el.val('1, 101');
$el.change();
expect($scope.val).to.be(undefined);
expect($el.hasClass('ng-invalid')).to.be(true);
});
});
});

View file

@ -0,0 +1,104 @@
define(function (require) {
var angular = require('angular');
var $ = require('jquery');
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
var searchResponse = require('fixtures/search_response');
// Load the kibana app dependencies.
require('services/private');
require('components/doc_table/doc_table');
var $parentScope, $scope, $timeout, searchSource;
var init = function ($elem, props) {
inject(function ($rootScope, $compile, _$timeout_) {
$timeout = _$timeout_;
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
// I think the prereq requires this?
$timeout(function () {
$elem.scope().$digest();
}, 0);
$scope = $elem.isolateScope();
});
};
var destroy = function () {
$scope.$destroy();
$parentScope.$destroy();
};
describe('docTable', function () {
var $elem;
beforeEach(module('kibana'));
beforeEach(function () {
$elem = angular.element('<doc-table search-source="searchSource" columns="columns" sorting="sorting"></doc-table>');
inject(function (Private) {
searchSource = Private(require('fixtures/stubbed_search_source'));
});
init($elem, {
searchSource: searchSource,
columns: [],
sorting: ['@timestamp', 'desc']
});
$scope.$digest();
});
afterEach(function () {
destroy();
});
it('should compile', function () {
expect($elem.text()).to.not.be.empty();
});
it('should set the indexPattern to that of the searchSource', function () {
expect($scope.indexPattern).to.be(searchSource.get('index'));
});
it('should set size and sort on the searchSource', function () {
expect($scope.searchSource.sort.called).to.be(true);
expect($scope.searchSource.size.called).to.be(true);
});
it('should have an addRows function that increases the row cound', function () {
expect($scope.addRows).to.be.a(Function);
searchSource.crankResults();
$scope.$digest();
expect($scope.limit).to.be(50);
$scope.addRows();
expect($scope.limit).to.be(100);
});
it('should reset the row limit when results are received', function () {
$scope.limit = 100;
expect($scope.limit).to.be(100);
searchSource.crankResults();
$scope.$digest();
expect($scope.limit).to.be(50);
});
it('should put the hits array on scope', function () {
expect($scope.hits).to.be(undefined);
searchSource.crankResults();
$scope.$digest();
expect($scope.hits).to.be.an(Array);
});
it('should destroy the searchSource when the scope is destroyed', function () {
expect(searchSource.destroy.called).to.be(false);
$scope.$destroy();
expect(searchSource.destroy.called).to.be(true);
});
});
});

View file

@ -0,0 +1,35 @@
define(function (require) {
var getSort = require('components/doc_table/lib/get_sort');
var indexPattern =
describe('docTable', function () {
describe('getSort function', function () {
var timePattern = {
timeFieldName: 'time'
};
var noTimePattern = {};
it('should be a function', function () {
expect(getSort).to.be.a(Function);
});
it('should return an object if passed a 2 item array', function () {
expect(getSort(['foo', 'bar'], timePattern)).to.eql({foo: 'bar'});
expect(getSort(['foo', 'bar'], noTimePattern)).to.eql({foo: 'bar'});
});
it('should sort in reverse chrono order otherwise on time based patterns', function () {
expect(getSort([], timePattern)).to.eql({time: 'desc'});
expect(getSort(['foo'], timePattern)).to.eql({time: 'desc'});
expect(getSort({foo: 'bar'}, timePattern)).to.eql({time: 'desc'});
});
it('should sort by score on non-time patterns', function () {
expect(getSort([], noTimePattern)).to.eql({_score: 'desc'});
expect(getSort(['foo'], noTimePattern)).to.eql({_score: 'desc'});
expect(getSort({foo: 'bar'}, noTimePattern)).to.eql({_score: 'desc'});
});
});
});
});

View file

@ -11,6 +11,156 @@ define(function (require) {
angular.module('ZeroFilledArrayUtilService', ['kibana']);
describe('Vislib Zero Injection Module Test Suite', function () {
var dateHistogramRows = {
'rows': [
{
'label': 'Top 5 @tags: success',
'ordered': {
'date': true,
'interval': 60000,
'min': 1418410540548,
'max': 1418410936568
},
'series': [
{
'label': 'jpg',
'values': [
{ 'x': 1418410560000, 'y': 2 },
{ 'x': 1418410620000, 'y': 4 },
{ 'x': 1418410680000, 'y': 1 },
{ 'x': 1418410740000, 'y': 5 },
{ 'x': 1418410800000, 'y': 2 },
{ 'x': 1418410860000, 'y': 3 },
{ 'x': 1418410920000, 'y': 2 }
]
},
{
'label': 'css',
'values': [
{ 'x': 1418410560000, 'y': 1 },
{ 'x': 1418410620000, 'y': 3 },
{ 'x': 1418410680000, 'y': 1 },
{ 'x': 1418410740000, 'y': 4 },
{ 'x': 1418410800000, 'y': 2 }
]
},
{
'label': 'gif',
'values': [
{ 'x': 1418410500000, 'y': 1 },
{ 'x': 1418410680000, 'y': 3 },
{ 'x': 1418410740000, 'y': 2 }
]
}
]
},
{
'label': 'Top 5 @tags: info',
'ordered': {
'date': true,
'interval': 60000,
'min': 1418410540548,
'max': 1418410936568
},
'series': [
{
'label': 'jpg',
'values': [
{ 'x': 1418410560000, 'y': 4 },
{ 'x': 1418410620000, 'y': 2 },
{ 'x': 1418410680000, 'y': 1 },
{ 'x': 1418410740000, 'y': 5 },
{ 'x': 1418410800000, 'y': 2 },
{ 'x': 1418410860000, 'y': 3 },
{ 'x': 1418410920000, 'y': 2 }
]
},
{
'label': 'css',
'values': [
{ 'x': 1418410620000, 'y': 3 },
{ 'x': 1418410680000, 'y': 1 },
{ 'x': 1418410740000, 'y': 4 },
{ 'x': 1418410800000, 'y': 2 }
]
},
{
'label': 'gif',
'values': [
{ 'x': 1418410500000, 'y': 1 }
]
}
]
},
{
'label': 'Top 5 @tags: security',
'ordered': {
'date': true,
'interval': 60000,
'min': 1418410540548,
'max': 1418410936568
},
'series': [
{
'label': 'jpg',
'values': [
{ 'x': 1418410560000, 'y': 1 },
{ 'x': 1418410620000, 'y': 3 },
{ 'x': 1418410920000, 'y': 2 }
]
},
{
'label': 'gif',
'values': [
{ 'x': 1418410680000, 'y': 3 },
{ 'x': 1418410740000, 'y': 1 }
]
}
]
},
{
'label': 'Top 5 @tags: login',
'ordered': {
'date': true,
'interval': 60000,
'min': 1418410540548,
'max': 1418410936568
},
'series': [
{
'label': 'jpg',
'values': [
{ 'x': 1418410740000, 'y': 1 }
]
},
{
'label': 'css',
'values': [
{ 'x': 1418410560000, 'y': 1 }
]
}
]
},
{
'label': 'Top 5 @tags: warning',
'ordered': {
'date': true,
'interval': 60000,
'min': 1418410540548,
'max': 1418410936568
},
'series': [
{
'label': 'jpg',
'values': [
{ 'x': 1418410860000, 'y': 2 }
]
}
]
}
]
};
var seriesData = {
series: [
{
@ -150,7 +300,7 @@ define(function (require) {
});
it('should throw an error if property series, rows, or columns is not ' +
'present', function () {
'present', function () {
expect(function () {
injectZeros(childrenObject);
@ -158,7 +308,7 @@ define(function (require) {
});
it('should not throw an error if object has property series, rows, or ' +
'columns', function () {
'columns', function () {
expect(function () {
injectZeros(seriesObject);
@ -177,7 +327,7 @@ define(function (require) {
expect(_.isFunction(injectZeros)).to.be(true);
});
it('should return an object with series[0].values"', function () {
it('should return an object with series[0].values', function () {
expect(_.isObject(sample1)).to.be(true);
expect(_.isObject(sample1.series[0].values)).to.be(true);
});
@ -468,6 +618,7 @@ define(function (require) {
zeroFillArray = Private(require('components/vislib/components/zero_injection/zero_fill_data_array'));
createZeroArray = Private(require('components/vislib/components/zero_injection/zero_filled_array'));
arr1 = createZeroArray(xValueArr);
// Takes zero array as 1st arg and data array as 2nd arg
results = zeroFillArray(arr1, arr2);
});
@ -521,5 +672,37 @@ define(function (require) {
});
});
describe('Injected Zero values return in the correct order', function () {
var injectZeros;
var results;
beforeEach(function () {
module('ZeroInjectionUtilService');
});
beforeEach(function () {
inject(function (Private) {
injectZeros = Private(require('components/vislib/components/zero_injection/inject_zeros'));
results = injectZeros(dateHistogramRows);
});
});
it('should return an array of objects', function () {
results.rows.forEach(function (row) {
expect(_.isArray(row.series[0].values)).to.be(true);
});
});
it('should return ordered x values', function () {
var values = results.rows[0].series[0].values;
expect(values[0].x).to.be.lessThan(values[1].x);
expect(values[1].x).to.be.lessThan(values[2].x);
expect(values[2].x).to.be.lessThan(values[3].x);
expect(values[3].x).to.be.lessThan(values[4].x);
expect(values[4].x).to.be.lessThan(values[5].x);
expect(values[5].x).to.be.lessThan(values[6].x);
expect(values[6].x).to.be.lessThan(values[7].x);
});
});
});
});