Merge branch 'master' into fix/3560

This commit is contained in:
Shelby Sturgis 2015-04-28 22:25:27 -04:00
commit 55fa3f5d66
59 changed files with 1117 additions and 478 deletions

View file

@ -0,0 +1,9 @@
<div class="form-group">
<label>Values</label>
<kbn-number-list
ng-model="agg.params.values"
unit-name="value"
range="[-Infinity,Infinity]"
>
</kbn-number-list>
</div>

View file

@ -0,0 +1,9 @@
<div class="form-group">
<label>Percents</label>
<kbn-number-list
ng-model="agg.params.percents"
unit-name="percent"
range="[0,100]"
>
</kbn-number-list>
</div>

View file

@ -1,34 +0,0 @@
<div class="form-group" ng-controller="agg.type.params.byName.percents.controller">
<label>Percentiles</label>
<div
ng-repeat="value in agg.params.percents track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="agg.params.percents[$index]"
values-list="agg.params.percents"
values-list-min="0"
values-list-max="100"
input-focus
class="form-control">
<button type="button" ng-click="remove($index, 1)" class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
<input ng-model="validLength" name="validLength" required type="hidden">
<div class="hintbox" ng-show="aggForm.validLength.$invalid">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Required:</strong> You mush specify at least one percentile
</p>
</div>
<button
ng-click="add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add Percent
</button>
</div>

View file

@ -1,33 +0,0 @@
<div class="form-group" ng-controller="agg.type.params.byName.values.controller">
<label>Values</label>
<div
ng-repeat="value in agg.params.values track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="agg.params.values[$index]"
values-list="agg.params.values"
values-list-min="0"
input-focus
class="form-control">
<button type="button" ng-click="remove($index, 1)" class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
<input ng-model="validLength" name="validLength" required type="hidden">
<div class="hintbox" ng-show="aggForm.validLength.$invalid">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Required:</strong> You must specify at least one value
</p>
</div>
<button
ng-click="add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add value
</button>
</div>

View file

@ -5,8 +5,9 @@ define(function (require) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
require('components/agg_types/controls/_values_list');
var valuesEditor = require('text!components/agg_types/controls/values.html');
var valuesEditor = require('text!components/agg_types/controls/percentile_ranks.html');
// required by the values editor
require('components/number_list/number_list');
var valueProps = {
makeLabel: function () {
@ -28,20 +29,7 @@ define(function (require) {
{
name: 'values',
editor: valuesEditor,
default: [],
controller: function ($scope) {
$scope.remove = function (index) {
$scope.agg.params.values.splice(index, 1);
};
$scope.add = function () {
$scope.agg.params.values.push(_.last($scope.agg.params.values) + 1);
};
$scope.$watchCollection('agg.params.values', function (values) {
$scope.validLength = _.size(values) || null;
});
}
default: []
}
],
getResponseAggs: function (agg) {
@ -60,4 +48,4 @@ define(function (require) {
}
});
};
});
});

View file

@ -6,8 +6,9 @@ define(function (require) {
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
var ordinalSuffix = require('utils/ordinal_suffix');
require('components/agg_types/controls/_values_list');
var percentEditor = require('text!components/agg_types/controls/percents.html');
var percentsEditor = require('text!components/agg_types/controls/percentiles.html');
// required by the percentiles editor
require('components/number_list/number_list');
var valueProps = {
makeLabel: function () {
@ -28,21 +29,8 @@ define(function (require) {
},
{
name: 'percents',
editor: percentEditor,
default: [1, 5, 25, 50, 75, 95, 99],
controller: function ($scope) {
$scope.remove = function (index) {
$scope.agg.params.percents.splice(index, 1);
};
$scope.add = function () {
$scope.agg.params.percents.push(_.last($scope.agg.params.percents) + 1);
};
$scope.$watchCollection('agg.params.percents', function (percents) {
$scope.validLength = _.size(percents) || null;
});
}
editor: percentsEditor,
default: [1, 5, 25, 50, 75, 95, 99]
}
],
getResponseAggs: function (agg) {
@ -61,4 +49,4 @@ define(function (require) {
}
});
};
});
});

View file

@ -45,7 +45,7 @@ define(function (require) {
queue.forEach(function (q) { q.reject(err); });
})
.finally(function () {
$rootScope.$emit('change:config', updated.concat(deleted));
$rootScope.$broadcast('change:config', updated.concat(deleted));
});
};
@ -70,7 +70,7 @@ define(function (require) {
var defer = Promise.defer();
queue.push(defer);
notify.log('config change: ' + key + ': ' + oldVal + ' -> ' + newVal);
$rootScope.$emit('change:config.' + key, newVal, oldVal);
$rootScope.$broadcast('change:config.' + key, newVal, oldVal);
// reset the fire timer
clearTimeout(timer);
@ -80,4 +80,4 @@ define(function (require) {
};
};
});
});

View file

@ -19,7 +19,7 @@ define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var defaults = require('components/config/defaults');
var defaults = Private(require('components/config/defaults'));
var DelayedUpdater = Private(require('components/config/_delayed_updater'));
var vals = Private(require('components/config/_vals'));
@ -123,6 +123,29 @@ define(function (require) {
if (updater) updater.fire();
};
/**
* A little helper for binding config variables to $scopes
*
* @param {Scope} $scope - an angular $scope object
* @param {string} key - the config key to bind to
* @param {string} [property] - optional property name where the value should
* be stored. Defaults to the config key
* @return {function} - an unbind function
*/
config.$bind = function ($scope, key, property) {
if (!property) property = key;
var update = function () {
$scope[property] = config.get(key);
};
update();
return _.partial(_.invoke, [
$scope.$on('change:config.' + key, update),
$scope.$on('init:config', update)
], 'call');
};
/*****
* PRIVATE API
*****/

View file

@ -1,86 +1,93 @@
define(function (require) {
var _ = require('lodash');
return function () {
var _ = require('lodash');
return {
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "hh:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are ISO 8601 intervals:' +
' http://en.wikipedia.org/wiki/ISO_8601#Time_intervals'
},
'defaultIndex': {
value: null,
description: 'The index to access if no index is set',
},
'metaFields': {
value: ['_source', '_id', '_type', '_index'],
description: 'Fields that exist outside of _source to merge into our document when displaying it',
},
'discover:sampleSize': {
value: 500,
description: 'The number of rows to show in the table',
},
'fields:popularLimit': {
value: 10,
description: 'The top N most popular fields to show',
},
'format:numberPrecision': {
value: 3,
description: 'Round numbers to this many decimal places',
},
'histogram:barTarget': {
value: 50,
description: 'Attempt to generate around this many bar when using "auto" interval in date histograms',
},
'histogram:maxBars': {
value: 100,
description: 'Never show more than this many bar in date histograms, scale values if needed',
},
'visualization:tileMap:maxPrecision': {
value: 7,
description: 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, ' +
'12 is the max. Explanation of cell dimensions: http://www.elastic.co/guide/en/elasticsearch/reference/current/' +
'search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator',
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',
},
'csv:quoteValues': {
value: true,
description: 'Should values be quoted in csv exports?',
},
'history:limit': {
value: 10,
description: 'In fields that have history (e.g. query inputs), show this many recent values',
},
'shortDots:enable': {
value: false,
description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz',
},
'truncate:maxHeight': {
value: 115,
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
}
return {
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "hh:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are ISO 8601 intervals:' +
' http://en.wikipedia.org/wiki/ISO_8601#Time_intervals'
},
'defaultIndex': {
value: null,
description: 'The index to access if no index is set',
},
'metaFields': {
value: ['_source', '_id', '_type', '_index'],
description: 'Fields that exist outside of _source to merge into our document when displaying it',
},
'discover:sampleSize': {
value: 500,
description: 'The number of rows to show in the table',
},
'fields:popularLimit': {
value: 10,
description: 'The top N most popular fields to show',
},
'format:numberPrecision': {
value: 3,
description: 'Round numbers to this many decimal places',
},
'histogram:barTarget': {
value: 50,
description: 'Attempt to generate around this many bar when using "auto" interval in date histograms',
},
'histogram:maxBars': {
value: 100,
description: 'Never show more than this many bar in date histograms, scale values if needed',
},
'visualization:tileMap:maxPrecision': {
value: 7,
description: 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, ' +
'12 is the max. Explanation of cell dimensions: http://www.elastic.co/guide/en/elasticsearch/reference/current/' +
'search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator',
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',
},
'csv:quoteValues': {
value: true,
description: 'Should values be quoted in csv exports?',
},
'history:limit': {
value: 10,
description: 'In fields that have history (e.g. query inputs), show this many recent values',
},
'shortDots:enable': {
value: false,
description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz',
},
'truncate:maxHeight': {
value: 115,
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
},
'indexPattern:fieldMapping:lookBack': {
value: 5,
description: 'For index patterns containing timestamps in their names, look for this many recent matching ' +
'patterns from which to query the field mapping.'
}
};
};
});
});

View file

@ -43,6 +43,7 @@ define(function (require) {
// the id of the document
self.id = config.id || void 0;
self.defaults = config.defaults;
/**
* Asynchronously initialize this object - will only run
@ -185,14 +186,12 @@ define(function (require) {
});
}
/**
* Save this object
* Serialize this object
*
* @return {Promise}
* @resolved {String} - The id of the doc
* @return {Object}
*/
self.save = function () {
self.serialize = function () {
var body = {};
_.forOwn(mapping, function (fieldMapping, fieldName) {
@ -209,6 +208,18 @@ define(function (require) {
};
}
return body;
};
/**
* Save this object
*
* @return {Promise}
* @resolved {String} - The id of the doc
*/
self.save = function () {
var body = self.serialize();
// Slugify the object id
self.id = slugifyId(self.id);

View file

@ -1,6 +1,6 @@
<th width="1%"></th>
<th ng-if="indexPattern.timeFieldName">
<span ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time">Time <i ng-class="headerClass(indexPattern.timeFieldName)"></i></span>
<span>Time <i ng-class="headerClass(indexPattern.timeFieldName)" ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time"></i></span>
</th>
<th ng-repeat="name in columns">
<span class="table-header-name">
@ -11,4 +11,4 @@
<i ng-click="moveLeft(name)" class="fa fa-angle-double-left" ng-show="!$first" tooltip="Move column to the left" tooltip-append-to-body="1"></i>
<i ng-click="moveRight(name)" class="fa fa-angle-double-right" ng-show="!$last" tooltip="Move column to the right" tooltip-append-to-body="1"></i>
</span>
</th>
</th>

View file

@ -1,5 +1,5 @@
define(function (require) {
return function MapperService(Private, Promise, es, configFile) {
return function MapperService(Private, Promise, es, configFile, config) {
var _ = require('lodash');
var moment = require('moment');
@ -51,7 +51,7 @@ define(function (require) {
promise = self.getIndicesForIndexPattern(indexPattern)
.then(function (existing) {
if (existing.matches.length === 0) throw new IndexPatternMissingIndices();
return existing.matches.slice(-5); // Grab the most recent 5
return existing.matches.slice(-config.get('indexPattern:fieldMapping:lookBack')); // Grab the most recent
});
}

View file

@ -0,0 +1,34 @@
<div
ng-repeat="value in numberListCntr.getList() track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="numberListCntr.getList()[$index]"
kbn-number-list-input
input-focus
class="form-control">
<button
ng-click="numberListCntr.remove($index, 1)"
class="btn btn-danger btn-xs"
type="button">
<i class="fa fa-times"></i>
</button>
</div>
<p ng-show="numberListCntr.invalidLength()" class="text-danger text-center">
You must specify at least one {{numberListCntr.getUnitName()}}
</p>
<p ng-show="numberListCntr.undefinedLength()" class="text-primary text-center">
<!-- be a bit more polite when the form is first init'd -->
Please specify at least one {{numberListCntr.getUnitName()}}
</p>
<button
ng-click="numberListCntr.add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add {{numberListCntr.getUnitName()}}
</button>

View file

@ -0,0 +1,108 @@
define(function (require) {
var _ = require('lodash');
var parseRange = require('utils/range');
require('components/number_list/number_list_input');
require('modules')
.get('kibana')
.directive('kbnNumberList', function () {
return {
restrict: 'E',
template: require('text!components/number_list/number_list.html'),
controllerAs: 'numberListCntr',
require: 'ngModel',
controller: function ($scope, $attrs, $parse) {
var self = this;
// Called from the pre-link function once we have the controllers
self.init = function (modelCntr) {
self.modelCntr = modelCntr;
self.getList = function () {
return self.modelCntr.$modelValue;
};
self.getUnitName = _.partial($parse($attrs.unit), $scope);
var defaultRange = self.range = parseRange('[0,Infinity)');
$scope.$watch(function () {
return $attrs.range;
}, function (range, prev) {
if (!range) {
self.range = defaultRange;
return;
}
try {
self.range = parseRange(range);
} catch (e) {
throw new TypeError('Unable to parse range: ' + e.message);
}
});
/**
* Remove an item from list by index
* @param {number} index
* @return {undefined}
*/
self.remove = function (index) {
var list = self.getList();
if (!list) return;
list.splice(index, 1);
};
/**
* Add an item to the end of the list
* @return {undefined}
*/
self.add = function () {
var list = self.getList();
if (!list) return;
list.push(_.last(list) + 1);
};
/**
* Check to see if the list is too short.
*
* @return {Boolean}
*/
self.tooShort = function () {
return _.size(self.getList()) < 1;
};
/**
* Check to see if the list is too short, but simply
* because the user hasn't interacted with it yet
*
* @return {Boolean}
*/
self.undefinedLength = function () {
return self.tooShort() && (self.modelCntr.$untouched && self.modelCntr.$pristine);
};
/**
* Check to see if the list is too short
*
* @return {Boolean}
*/
self.invalidLength = function () {
return self.tooShort() && !self.undefinedLength();
};
$scope.$watchCollection(self.getList, function () {
self.modelCntr.$setValidity('numberListLength', !self.tooShort());
});
};
},
link: {
pre: function ($scope, $el, attrs, ngModelCntr) {
$scope.numberListCntr.init(ngModelCntr);
}
},
};
});
});

View file

@ -6,18 +6,21 @@ define(function (require) {
var INVALID = {}; // invalid flag
var FLOATABLE = /^[\d\.e\-\+]+$/i;
var VALIDATION_ERROR = 'numberListRangeAndOrder';
var DIRECTIVE_ATTR = 'kbn-number-list-input';
require('modules')
.get('kibana')
.directive('valuesList', function ($parse) {
.directive('kbnNumberListInput', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $el, attrs, ngModelController) {
require: ['ngModel', '^kbnNumberList'],
link: function ($scope, $el, attrs, controllers) {
var ngModelCntr = controllers[0];
var numberListCntr = controllers[1];
var $setModel = $parse(attrs.ngModel).assign;
var $repeater = $el.closest('[ng-repeat]');
var $listGetter = $parse(attrs.valuesList);
var $minValue = $parse(attrs.valuesListMin);
var $maxValue = $parse(attrs.valuesListMax);
var handlers = {
up: change(add, 1),
@ -29,14 +32,16 @@ define(function (require) {
tab: go('next'),
'shift-tab': go('prev'),
'shift-enter': numberListCntr.add,
backspace: removeIfEmpty,
delete: removeIfEmpty
};
function removeIfEmpty(event) {
if ($el.val() === '') {
if (!ngModelCntr.$viewValue) {
$get('prev').focus();
$scope.remove($scope.$index);
numberListCntr.remove($scope.$index);
event.preventDefault();
}
@ -44,7 +49,7 @@ define(function (require) {
}
function $get(dir) {
return $repeater[dir]().find('[values-list]');
return $repeater[dir]().find('[' + DIRECTIVE_ATTR + ']');
}
function go(dir) {
@ -88,7 +93,7 @@ define(function (require) {
function change(using, mod) {
return function () {
var str = String(ngModelController.$viewValue);
var str = String(ngModelCntr.$viewValue);
var val = parse(str);
if (val === INVALID) return;
@ -96,7 +101,7 @@ define(function (require) {
if (next === INVALID) return;
$el.val(next);
ngModelController.$setViewValue(next);
ngModelCntr.$setViewValue(next);
};
}
@ -117,17 +122,26 @@ define(function (require) {
});
function parse(viewValue) {
viewValue = String(viewValue || 0);
var num = viewValue.trim();
if (!FLOATABLE.test(num)) return INVALID;
num = parseFloat(num);
if (isNaN(num)) return INVALID;
var num = viewValue;
var list = $listGetter($scope);
var min = list[$scope.$index - 1] || $minValue($scope);
var max = list[$scope.$index + 1] || $maxValue($scope);
if (typeof num !== 'number' || isNaN(num)) {
// parse non-numbers
num = String(viewValue || 0).trim();
if (!FLOATABLE.test(num)) return INVALID;
if (num <= min || num >= max) return INVALID;
num = parseFloat(num);
if (isNaN(num)) return INVALID;
}
var range = numberListCntr.range;
if (!range.within(num)) return INVALID;
if ($scope.$index > 0) {
var i = $scope.$index - 1;
var list = numberListCntr.getList();
var prev = list[i];
if (num <= prev) return INVALID;
}
return num;
}
@ -137,31 +151,35 @@ define(function (require) {
{
fn: $scope.$watchCollection,
get: function () {
return $listGetter($scope);
return numberListCntr.getList();
}
}
], function () {
var valid = parse(ngModelController.$viewValue) !== INVALID;
ngModelController.$setValidity('valuesList', valid);
var valid = parse(ngModelCntr.$viewValue) !== INVALID;
ngModelCntr.$setValidity(VALIDATION_ERROR, valid);
});
function validate(then) {
return function (input) {
var value = parse(input);
var valid = value !== INVALID;
value = valid ? value : void 0;
ngModelController.$setValidity('valuesList', valid);
value = valid ? value : input;
ngModelCntr.$setValidity(VALIDATION_ERROR, valid);
then && then(input, value);
return value;
};
}
ngModelController.$parsers.push(validate());
ngModelController.$formatters.push(validate(function (input, value) {
ngModelCntr.$parsers.push(validate());
ngModelCntr.$formatters.push(validate(function (input, value) {
if (input !== value) $setModel($scope, value);
}));
if (parse(ngModelCntr.$viewValue) === INVALID) {
ngModelCntr.$setTouched();
}
}
};
});
});
});

View file

@ -1,19 +1,23 @@
define(function (require) {
var _ = require('lodash');
var modules = require('modules');
var urlParam = '_a';
function AppStateProvider(Private, $rootScope, getAppState) {
var State = Private(require('components/state_management/state'));
_(AppState).inherits(State);
function AppState(defaults) {
AppState.Super.call(this, '_a', defaults);
AppState.Super.call(this, urlParam, defaults);
getAppState._set(this);
}
// if the url param is missing, write it back
AppState.prototype._persistAcrossApps = false;
AppState.prototype.destroy = function () {
AppState.Super.prototype.destroy.call(this);
getAppState._set(null);
@ -26,13 +30,19 @@ define(function (require) {
.factory('AppState', function (Private) {
return Private(AppStateProvider);
})
.service('getAppState', function () {
.service('getAppState', function ($location) {
var currentAppState;
function get() {
return currentAppState;
}
// Checks to see if the appState might already exist, even if it hasn't been newed up
get.previouslyStored = function () {
var search = $location.search();
return search[urlParam] ? true : false;
};
get._set = function (current) {
currentAppState = current;
};

View file

@ -43,7 +43,8 @@ define(function (require) {
if (self.shouldAutoReload(next, prev)) {
var appState = getAppState();
appState.destroy();
if (appState) appState.destroy();
reloading = $rootScope.$on('$locationChangeSuccess', function () {
// call the "unlisten" function returned by $on
reloading();

View file

@ -102,6 +102,14 @@ define(function (require) {
}
};
Vis.prototype.hasSchemaAgg = function (schemaName, aggTypeName) {
var aggs = this.aggs.bySchemaName[schemaName] || [];
return aggs.some(function (agg) {
if (!agg.type || !agg.type.name) return false;
return agg.type.name === aggTypeName;
});
};
return Vis;
};
});

View file

@ -133,7 +133,7 @@ define(function (require) {
var events = self.events ? self.events.eventResponse(d, i) : d;
return render(tooltipFormatter(events));
})
.on('mouseout.tip', function () {
.on('mouseleave.tip', function () {
render();
});
});

View file

@ -138,14 +138,14 @@ define(function (require) {
// legend
legendDiv.selectAll('li')
.filter(function (d) {
return this.getAttribute('data-label') !== label;
return this.getAttribute('data-label') !== label.toString();
})
.classed('blur_shape', true);
// all data-label attribute
charts.selectAll('[data-label]')
.filter(function (d) {
return this.getAttribute('data-label') !== label;
return this.getAttribute('data-label') !== label.toString();
})
.classed('blur_shape', true);

View file

@ -4,6 +4,7 @@ define(function (require) {
var $ = require('jquery');
var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart'));
var TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
var errors = require('errors');
require('css!components/vislib/styles/main');
@ -266,6 +267,9 @@ define(function (require) {
var yScale = this.handler.yAxis.yScale;
var minWidth = 20;
var minHeight = 20;
var addTimeMarker = this._attr.addTimeMarker;
var times = this._attr.times || [];
var timeMarker;
var div;
var svg;
var width;
@ -283,6 +287,10 @@ define(function (require) {
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
@ -333,6 +341,10 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', 1);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};

View file

@ -5,6 +5,7 @@ define(function (require) {
var moment = require('moment');
var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart'));
var TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
var errors = require('errors');
require('css!components/vislib/styles/main');
@ -269,8 +270,12 @@ define(function (require) {
var elHeight = this._attr.height = $elem.height();
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var xScale = this.handler.xAxis.xScale;
var minWidth = 20;
var minHeight = 20;
var addTimeMarker = this._attr.addTimeMarker;
var times = this._attr.times || [];
var timeMarker;
var div;
var svg;
var width;
@ -285,6 +290,10 @@ define(function (require) {
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
@ -325,6 +334,10 @@ define(function (require) {
.style('stroke-width', 1);
}
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};

View file

@ -5,6 +5,7 @@ define(function (require) {
var errors = require('errors');
var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart'));
var TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
require('css!components/vislib/styles/main');
/**
@ -258,10 +259,14 @@ define(function (require) {
var elHeight = this._attr.height = $elem.height();
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var xScale = this.handler.xAxis.xScale;
var minWidth = 20;
var minHeight = 20;
var startLineX = 0;
var lineStrokeWidth = 1;
var addTimeMarker = this._attr.addTimeMarker;
var times = this._attr.times || [];
var timeMarker;
var div;
var svg;
var width;
@ -288,6 +293,10 @@ define(function (require) {
width = elWidth - margin.left - margin.right;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
@ -331,6 +340,10 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};

View file

@ -178,34 +178,13 @@ define(function (require) {
} else if (this._attr.mapType === 'Shaded Geohash Grid') {
featureLayer = this.shadedGeohashGrid(map, mapData);
} else {
featureLayer = this.pinMarkers(mapData);
featureLayer = this.scaledCircleMarkers(map, mapData);
}
}
return featureLayer;
};
/**
* Type of data overlay for map:
* creates featurelayer from mapData (geoJson)
* with default leaflet pin markers
*
* @method pinMarkers
* @param mapData {Object}
* @return {Leaflet object} featureLayer
*/
TileMap.prototype.pinMarkers = function (mapData) {
var self = this;
var featureLayer = L.geoJson(mapData, {
onEachFeature: function (feature, layer) {
self.bindPopup(feature, layer);
}
});
return featureLayer;
};
/**
* Type of data overlay for map:
* creates featurelayer from mapData (geoJson)

View file

@ -0,0 +1,74 @@
define(function (require) {
var datemath = require('utils/datemath');
return function TimeMarkerFactory(d3) {
function TimeMarker(times, xScale, height) {
if (!(this instanceof TimeMarker)) {
return new TimeMarker(times, xScale, height);
}
var currentTimeArr = [{
'time': new Date().getTime(),
'class': 'time-marker',
'color': '#c80000',
'opacity': 0.3,
'width': 2
}];
this.xScale = xScale;
this.height = height;
this.times = (times.length) ? times.map(function (d) {
return {
'time': datemath.parse(d.time),
'class': d.class || 'time-marker',
'color': d.color || '#c80000',
'opacity': d.opacity || 0.3,
'width': d.width || 2
};
}) : currentTimeArr;
}
TimeMarker.prototype._isTimeBasedChart = function (selection) {
var data = selection.data();
return data.every(function (datum) {
return (datum.ordered && datum.ordered.date);
});
};
TimeMarker.prototype.render = function (selection) {
var self = this;
// return if not time based chart
if (!self._isTimeBasedChart(selection)) return;
selection.each(function () {
d3.select(this).selectAll('time-marker')
.data(self.times)
.enter().append('line')
.attr('class', function (d) {
return d.class;
})
.attr('pointer-events', 'none')
.attr('stroke', function (d) {
return d.color;
})
.attr('stroke-width', function (d) {
return d.width;
})
.attr('stroke-opacity', function (d) {
return d.opacity;
})
.attr('x1', function (d) {
return self.xScale(d.time);
})
.attr('x2', function (d) {
return self.xScale(d.time);
})
.attr('y1', self.height)
.attr('y2', self.xScale.range()[0]);
});
};
return TimeMarker;
};
});

View file

@ -49,7 +49,7 @@ define(function (require) {
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl) {
return {
controller: function ($scope, $route, $routeParams, $location, configFile, Private) {
controller: function ($scope, $route, $routeParams, $location, configFile, Private, getAppState) {
var filterBarWatchFilters = Private(require('components/filter_bar/lib/watchFilters'));
var notify = new Notifier({
@ -57,6 +57,12 @@ define(function (require) {
});
var dash = $scope.dash = $route.current.locals.dash;
if (dash.timeRestore && dash.timeTo && dash.timeFrom && !getAppState.previouslyStored()) {
timefilter.time.to = dash.timeTo;
timefilter.time.from = dash.timeFrom;
}
$scope.$on('$destroy', dash.destroy);
var matchQueryFilter = function (filter) {
@ -135,6 +141,8 @@ define(function (require) {
$state.title = dash.id = dash.title;
$state.save();
dash.panelsJSON = angular.toJson($state.panels);
dash.timeFrom = dash.timeRestore ? timefilter.time.from : undefined;
dash.timeTo = dash.timeRestore ? timefilter.time.to : undefined;
dash.save()
.then(function (id) {

View file

@ -3,5 +3,11 @@
<label for="dashboardTitle">Save As</label>
<input id="dashboardTitle" type="text" ng-model="opts.dashboard.title" class="form-control" placeholder="Dashboard title" input-focus="select">
</div>
<div class="form-group">
<label>
<input type="checkbox" ng-model="opts.dashboard.timeRestore" ng-checked="opts.dashboard.timeRestore">
Store time with dashboard <i class="fa fa-info-circle ng-scope" tooltip="Change the time filter to the currently selected time each time this dashboard is loaded" tooltip-placement="" tooltip-popup-delay="250"></i>
</label>
</div>
<button type="submit" ng-disabled="!opts.dashboard.title" class="btn btn-primary" aria-label="Save dashboard">Save</button>
</form>

View file

@ -1,6 +1,7 @@
define(function (require) {
var module = require('modules').get('app/dashboard');
var _ = require('lodash');
var moment = require('moment');
// Used only by the savedDashboards service, usually no reason to change this
module.factory('SavedDashboard', function (courier) {
@ -23,7 +24,10 @@ define(function (require) {
hits: 'integer',
description: 'string',
panelsJSON: 'string',
version: 'integer'
version: 'integer',
timeRestore: 'boolean',
timeTo: 'string',
timeFrom: 'string'
},
// defeult values to assign to the doc
@ -32,7 +36,10 @@ define(function (require) {
hits: 0,
description: '',
panelsJSON: '[]',
version: 1
version: 1,
timeRestore: false,
timeTo: undefined,
timeFrom: undefined
},
searchSource: true,

View file

@ -484,6 +484,7 @@ define(function (require) {
type: 'histogram',
params: {
addLegend: false,
addTimeMarker: true
},
listeners: {
click: function (e) {

View file

@ -1,6 +1,5 @@
define(function (require) {
var _ = require('lodash');
var configDefaults = require('components/config/defaults');
require('modules').get('apps/settings')
.directive('advancedRow', function (config, Notifier, Private) {
@ -13,6 +12,7 @@ define(function (require) {
configs: '='
},
link: function ($scope) {
var configDefaults = Private(require('components/config/defaults'));
var notify = new Notifier();
var keyCodes = {
ESC: 27
@ -66,4 +66,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -1,6 +1,5 @@
define(function (require) {
var _ = require('lodash');
var configDefaults = require('components/config/defaults');
var getValType = require('plugins/settings/sections/advanced/lib/get_val_type');
@ -16,6 +15,7 @@ define(function (require) {
return {
restrict: 'E',
link: function ($scope) {
var configDefaults = Private(require('components/config/defaults'));
var keyCodes = {
ESC: 27
};
@ -70,4 +70,4 @@ define(function (require) {
display: 'Advanced',
url: '#/settings/advanced'
};
});
});

View file

@ -22,11 +22,8 @@ define(function (require) {
template: require('text!plugins/settings/sections/indices/index.html'),
link: function ($scope) {
$scope.edittingId = $route.current.params.id;
$scope.defaultIndex = config.get('defaultIndex');
$rootScope.$on('change:config.defaultIndex', function () {
$scope.defaultIndex = config.get('defaultIndex');
});
config.$bind($scope, 'defaultIndex');
$scope.$watch('defaultIndex', function (defaultIndex) {
$scope.indexPatternList = _($route.current.locals.indexPatternIds)
.map(function (id) {
@ -50,4 +47,4 @@ define(function (require) {
display: 'Indices',
url: '#/settings/indices',
};
});
});

View file

@ -24,6 +24,7 @@
<textarea rows="1" msd-elastic ng-if="field.type === 'text'" ng-model="field.value" class="form-control span12"/>
<input ng-if="field.type === 'number'" type="number" ng-model="field.value" class="form-control span12"/>
<div ng-if="field.type === 'json' || field.type === 'array'" ui-ace="{ onLoad: aceLoaded, mode: 'json' }" id="{{field.name}}" ng-model="field.value" class="form-control"></div>
<input ng-if="field.type === 'boolean'" type="checkbox" ng-model="field.value" ng-checked="field.value">
</div>
</form>
<div class="form-group">

View file

@ -55,11 +55,12 @@ define(function (require) {
} else if (_.isArray(field.value)) {
field.type = 'array';
field.value = angular.toJson(field.value, true);
} else if (_.isBoolean(field.value)) {
field.type = 'boolean';
field.value = field.value;
} else if (_.isPlainObject(field.value)) {
// do something recursive
return _.reduce(field.value, _.partialRight(createField, parents), memo);
} else {
return;
}
memo.push(field);
@ -74,15 +75,10 @@ define(function (require) {
$scope.title = inflection.singularize(serviceObj.title);
es.get({
index: config.file.kibana_index,
type: service.type,
id: $routeParams.id
})
.then(function (obj) {
service.get($routeParams.id).then(function (obj) {
$scope.obj = obj;
$scope.link = service.urlFor(obj._id);
$scope.fields = _.reduce(obj._source, createField, []);
$scope.link = service.urlFor(obj.id);
$scope.fields = _.reduce(_.defaults(obj.serialize(), obj.defaults), createField, []);
})
.catch(notify.fatal);

View file

@ -17,4 +17,10 @@
Scale Y-Axis to Data Bounds
</label>
</div>
<div class="vis-option-item" ng-show="vis.hasSchemaAgg('segment', 'date_histogram')">
<label>
<input type="checkbox" ng-model="vis.params.addTimeMarker" ng-checked="vis.params.addTimeMarker">
Current time marker
</label>
</div>
</div>

View file

@ -20,7 +20,9 @@ define(function (require) {
scale: 'linear',
mode: 'stacked',
interpolate: 'linear',
defaultYExtents: false
defaultYExtents: false,
times: [],
addTimeMarker: false
},
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'],

View file

@ -16,7 +16,9 @@ define(function (require) {
addLegend: true,
scale: 'linear',
mode: 'stacked',
defaultYExtents: false
defaultYExtents: false,
times: [],
addTimeMarker: false
},
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'percentage', 'grouped'],

View file

@ -20,7 +20,9 @@ define(function (require) {
drawLinesBetweenPoints: true,
radiusRatio: 9,
scale: 'linear',
defaultYExtents: false
defaultYExtents: false,
times: [],
addTimeMarker: false
},
scales: ['linear', 'log', 'square root'],
editor: require('text!plugins/vis_types/vislib/editors/line.html')

View file

@ -15,7 +15,7 @@ define(function (require) {
mapType: 'Scaled Circle Markers',
isDesaturated: true
},
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Pins'],
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid'],
editor: require('text!plugins/vis_types/vislib/editors/tile_map.html')
},
responseConverter: geoJsonConverter,

View file

@ -1,84 +1,81 @@
<ng-form name="aggForm">
<!-- header -->
<div class="vis-editor-agg-header">
<!-- header -->
<div class="vis-editor-agg-header">
<!-- open/close editor -->
<button
aria-label="{{ editorOpen ? 'Close Editor' : 'Open Editor' }}"
ng-click="editorOpen = !editorOpen"
type="button"
class="btn btn-xs vis-editor-agg-header-toggle">
<i aria-hidden="true" ng-class="{ 'fa-caret-down': editorOpen, 'fa-caret-right': !editorOpen }" class="fa"></i>
</button>
<!-- open/close editor -->
<!-- title -->
<span class="vis-editor-agg-header-title">
{{ agg.schema.title }}
</span>
<!-- description -->
<span ng-if="!editorOpen && aggForm.$valid" class="vis-editor-agg-header-description">
{{ describe() }}
</span>
<!-- error -->
<span ng-if="!editorOpen && aggForm.$invalid" class="vis-editor-agg-header-description danger">
{{ aggForm.describeErrors() }}
</span>
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<!-- up button -->
<button
aria-label="{{ editorOpen ? 'Close Editor' : 'Open Editor' }}"
ng-click="editorOpen = !editorOpen"
aria-label="Increase Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $first }"
ng-click="moveUp(agg)"
tooltip="Increase Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs vis-editor-agg-header-toggle">
<i aria-hidden="true" ng-class="{ 'fa-caret-down': editorOpen, 'fa-caret-right': !editorOpen }" class="fa"></i>
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-up"></i>
</button>
<!-- title -->
<span class="vis-editor-agg-header-title">
{{ agg.schema.title }}
</span>
<!-- down button -->
<button
aria-lebl="Decrease Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $last }"
ng-click="moveDown(agg)"
tooltip="Decrease Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-down"></i>
</button>
<!-- description -->
<span ng-if="!editorOpen && aggForm.$valid" class="vis-editor-agg-header-description">
{{ describe() }}
</span>
<!-- error -->
<span ng-if="!editorOpen && aggForm.$invalid" class="vis-editor-agg-header-description danger">
{{ aggForm.describeErrors() }}
</span>
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<!-- up button -->
<button
aria-label="Increase Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $first }"
ng-click="moveUp(agg)"
tooltip="Increase Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-up"></i>
</button>
<!-- down button -->
<button
aria-lebl="Decrease Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $last }"
ng-click="moveDown(agg)"
tooltip="Decrease Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-down"></i>
</button>
<!-- remove button -->
<button
ng-if="canRemove(agg)"
aria-label="Remove Dimension"
ng-if="stats.count > stats.min"
ng-click="remove(agg)"
tooltip="Remove Dimension"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-danger">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
</div>
<!-- remove button -->
<button
ng-if="canRemove(agg)"
aria-label="Remove Dimension"
ng-if="stats.count > stats.min"
ng-click="remove(agg)"
tooltip="Remove Dimension"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-danger">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
</div>
</div>
<vis-editor-agg-params
agg="agg"
group-name="groupName"
ng-show="editorOpen"
class="vis-editor-agg-editor">
</vis-editor-agg-params>
<vis-editor-agg-params
agg="agg"
group-name="groupName"
ng-show="editorOpen"
class="vis-editor-agg-editor">
</vis-editor-agg-params>
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
class="vis-editor-agg-add vis-editor-agg-add-subagg">
</vis-editor-agg-add>
</ng-form>
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
class="vis-editor-agg-add vis-editor-agg-add-subagg">
</vis-editor-agg-add>

View file

@ -15,10 +15,14 @@ define(function (require) {
});
return {
restrict: 'E',
restrict: 'A',
template: require('text!plugins/visualize/editor/agg.html'),
link: function ($scope, $el) {
$scope.editorOpen = $scope.agg.brandNew;
require: 'form',
link: function ($scope, $el, attrs, kbnForm) {
$scope.editorOpen = !!$scope.agg.brandNew;
if (!$scope.editorOpen) {
$scope.$evalAsync(kbnForm.$setTouched);
}
$scope.$watchMulti([
'$index',

View file

@ -14,9 +14,9 @@
</nesting-indicator>
<!-- agg.html - controls for aggregation -->
<vis-editor-agg class="vis-editor-agg"></vis-editor-agg>
<ng-form vis-editor-agg name="aggForm" class="vis-editor-agg"></ng-form>
</div>
<vis-editor-agg-add ng-if="stats.count === 0" class="vis-editor-agg-add"></vis-editor-agg-add>
</div>
</li>
</li>

View file

@ -9,30 +9,26 @@
<nav class="navbar navbar-default navbar-static-top subnav">
<div class="container-fluid">
<ul class="nav navbar-nav" ng-init="visConfigSection = vis.type.requiresSearch ? 'data' : 'options'">
<li ng-class="{active: visConfigSection == 'data'}" ng-show="vis.type.schemas.metrics">
<a class="navbar-link active" ng-click="visConfigSection='data'">Data</a>
<!-- tabs -->
<ul class="nav navbar-nav">
<li ng-class="{active: sidebar.section == 'data'}" ng-show="vis.type.schemas.metrics">
<a class="navbar-link active" ng-click="sidebar.section='data'">Data</a>
</li>
<li ng-class="{active: visConfigSection == 'options'}">
<a class="navbar-link" ng-click="visConfigSection='options'">Options</a>
<li ng-class="{active: sidebar.section == 'options'}">
<a class="navbar-link" ng-click="sidebar.section='options'">Options</a>
</li>
</ul>
<!-- controls -->
<ul class="nav navbar-nav navbar-right">
<li ng-if="visualizeEditor.softErrorCount() > 0"
disabled
tooltip="{{visualizeEditor.describeErrors()}}" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
tooltip="{{ visualizeEditor.describeErrors() }}" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<a class="danger navbar-link">
<i class="fa fa-warning"></i>
</a>
</li>
<li tooltip="Discard changes" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<button class="btn-default navbar-btn-link"
ng-disabled="!vis.dirty"
ng-click="resetEditableVis()">
<i class="fa fa-close"></i>
</button>
</li>
<li tooltip="Apply changes" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<button class="btn-success navbar-btn-link"
type="submit"
@ -41,11 +37,19 @@
<i class="fa fa-play"></i>
</button>
</li>
<li tooltip="Discard changes" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<button class="btn-default navbar-btn-link"
ng-disabled="!vis.dirty"
ng-click="resetEditableVis()">
<i class="fa fa-close"></i>
</button>
</li>
</ul>
</div>
</nav>
<div class="vis-editor-config" ng-show="visConfigSection == 'data'">
<div class="vis-editor-config" ng-show="sidebar.section == 'data'">
<ul class="list-unstyled">
<!-- metrics -->
<vis-editor-agg-group ng-if="vis.type.schemas.metrics" group-name="metrics"></vis-editor-agg-group>
@ -55,16 +59,12 @@
</ul>
</div>
<div class="vis-editor-config" ng-show="visConfigSection == 'options'">
<div class="vis-editor-config" ng-show="sidebar.section == 'options'">
<ul class="list-unstyled">
<!-- vis options -->
<vis-editor-vis-options
vis="vis"
>
</vis-editor-vis-options>
<vis-editor-vis-options vis="vis"></vis-editor-vis-options>
<!-- apply/discard -->
</ul>
</div>

View file

@ -11,9 +11,11 @@ define(function (require) {
restrict: 'E',
template: require('text!plugins/visualize/editor/sidebar.html'),
scope: true,
link: function ($scope) {
controllerAs: 'sidebar',
controller: function ($scope) {
$scope.$bind('vis', 'editableVis');
this.section = _.get($scope, 'vis.type.requiresSearch') ? 'data' : 'options';
}
};
});
});
});

View file

@ -3,29 +3,36 @@ define(function () {
var lineSize = 0;
var newText = '';
var inHtmlTag = false;
var inHtmlChar = false;
for (var i = 0, len = text.length; i < len; i++) {
var chr = text.charAt(i);
newText += chr;
switch (chr) {
case ' ':
case ';':
case ':':
case ',':
// natural line break, reset line size
lineSize = 0;
break;
case '<':
inHtmlTag = true;
break;
case '>':
inHtmlTag = false;
lineSize = 0;
break;
default:
if (!inHtmlTag) lineSize++;
break;
case ' ':
case ':':
case ',':
// natural line break, reset line size
lineSize = 0;
break;
case '<':
inHtmlTag = true;
break;
case '>':
inHtmlTag = false;
lineSize = 0;
break;
case '&':
inHtmlChar = true;
break;
case ';':
inHtmlChar = false;
lineSize = 0;
break;
default:
if (!inHtmlTag && !inHtmlChar) lineSize++;
break;
}
if (lineSize > minLineLength) {
@ -38,4 +45,4 @@ define(function () {
return newText;
};
});
});

68
src/kibana/utils/range.js Normal file
View file

@ -0,0 +1,68 @@
define(function (require) {
var _ = require('lodash');
/**
* Regexp portion that matches our number
*
* supports:
* -100
* -100.0
* 0
* 0.10
* Infinity
* -Infinity
*
* @type {String}
*/
var _RE_NUMBER = '(\\-?(?:\\d+(?:\\.\\d+)?|Infinity))';
/**
* Regexp for the interval notation
*
* supports:
* [num, num]
* ( num , num ]
* [Infinity,num)
*
* @type {RegExp}
*/
var RANGE_RE = new RegExp('^\\s*([\\[|\\(])\\s*' + _RE_NUMBER + '\\s*,\\s*' + _RE_NUMBER + '\\s*([\\]|\\)])\\s*$');
function parse(input) {
var match = String(input).match(RANGE_RE);
if (!match) {
throw new TypeError('expected input to be in interval notation eg. (100, 200]');
}
return new Range(
match[1] === '[',
parseFloat(match[2]),
parseFloat(match[3]),
match[4] === ']'
);
}
function Range(/* minIncl, min, max, maxIncl */) {
var args = _.toArray(arguments);
if (args[1] > args[2]) args.reverse();
this.minInclusive = args[0];
this.min = args[1];
this.max = args[2];
this.maxInclusive = args[3];
}
Range.prototype.within = function (n) {
if (this.min === n && !this.minInclusive) return false;
if (this.min > n) return false;
if (this.max === n && !this.maxInclusive) return false;
if (this.max < n) return false;
return true;
};
return parse;
});

View file

@ -10,7 +10,7 @@ define(function (require) {
'=' : '-equal-'
};
_.each(trans, function (val, key) {
var regex = new RegExp(key);
var regex = new RegExp(key, 'g');
id = id.replace(regex, val);
});
id = id.replace(/[\s]+/g, '-');

View file

@ -16,6 +16,17 @@ function checkPath(path) {
}
}
// Set defaults for config file stuff
kibana.port = kibana.port || 5601;
kibana.host = kibana.host || '0.0.0.0';
kibana.elasticsearch_url = kibana.elasticsearch_url || 'http://localhost:9200';
kibana.maxSockets = kibana.maxSockets || Infinity;
kibana.log_file = kibana.log_file || null;
kibana.request_timeout = kibana.startup_timeout == null ? 0 : kibana.request_timeout;
kibana.ping_timeout = kibana.ping_timeout == null ? kibana.request_timeout : kibana.ping_timeout;
kibana.startup_timeout = kibana.startup_timeout == null ? 5000 : kibana.startup_timeout;
// Check if the local public folder is present. This means we are running in
// the NPM module. If it's not there then we are running in the git root.
var public_folder = path.resolve(__dirname, '..', 'public');
@ -33,13 +44,10 @@ try {
packagePath = path.resolve(__dirname, '..', '..', '..', 'package.json');
}
var requestTimeout = kibana.request_timeout || 0;
var pingTimeout = kibana.ping_timeout == null ? requestTimeout : kibana.ping_timeout;
var config = module.exports = {
port : kibana.port || 5601,
host : kibana.host || '0.0.0.0',
elasticsearch : kibana.elasticsearch_url || 'http://localhost:9200',
port : kibana.port,
host : kibana.host,
elasticsearch : kibana.elasticsearch_url,
root : path.normalize(path.join(__dirname, '..')),
quiet : false,
public_folder : public_folder,
@ -49,10 +57,10 @@ var config = module.exports = {
package : require(packagePath),
htpasswd : htpasswdPath,
buildNum : '@@buildNum',
maxSockets : kibana.maxSockets || Infinity,
log_file : kibana.log_file || null,
request_timeout : requestTimeout,
ping_timeout : pingTimeout
maxSockets : kibana.maxSockets,
log_file : kibana.log_file,
request_timeout : kibana.request_timeout,
ping_timeout : kibana.ping_timeout
};
config.plugins = listPlugins(config);

View file

@ -45,6 +45,9 @@ request_timeout: 300000
# Set to 0 to disable.
shard_timeout: 0
# Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying
# startup_timeout: 5000
# Set to false to have a complete disregard for the validity of the SSL
# certificate.
verify_ssl: true

View file

@ -6,7 +6,7 @@ var logger = require('./logger');
var config = require('../config');
function waitForPong() {
return client.ping()
return client.ping({requestTimeout: config.kibana.startup_timeout})
.catch(function (err) {
if (!(err instanceof NoConnections)) throw err;

View file

@ -1,12 +1,5 @@
module.exports = function (grunt) {
var config = {
test: {
files: [
'<%= unitTestDir %>/**/*.js'
],
tasks: ['mocha:unit']
},
less: {
files: [
'<%= app %>/**/styles/**/*.less',

View file

@ -1,31 +1,33 @@
define(function (require) {
describe('PercentList directive', function () {
describe('NumberList directive', function () {
var $ = require('jquery');
var _ = require('lodash');
var simulateKeys = require('test_utils/simulate_keys');
require('components/agg_types/controls/_values_list');
require('components/number_list/number_list');
var $el;
var $scope;
var compile;
function onlyValidValues() {
return $el.find('[ng-model]').toArray().map(function (el) {
var ngModel = $(el).controller('ngModel');
return ngModel.$valid ? ngModel.$modelValue : undefined;
});
}
beforeEach(module('kibana'));
beforeEach(inject(function ($injector) {
var $compile = $injector.get('$compile');
var $rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
$el = $('<div>').append(
$('<input>')
.attr('ng-model', 'vals[$index]')
.attr('ng-repeat', 'val in vals')
.attr('values-list', 'vals')
.attr('values-list-min', '0')
.attr('values-list-max', '100')
);
compile = function (vals) {
$el = $('<kbn-number-list ng-model="vals">');
compile = function (vals, range) {
$scope.vals = vals || [];
$el.attr('range', range);
$compile($el)($scope);
$scope.$apply();
};
@ -38,51 +40,22 @@ define(function (require) {
it('fails on invalid numbers', function () {
compile([1, 'foo']);
expect($scope.vals).to.eql([1, undefined]);
expect($el.find('.ng-invalid').size()).to.be(1);
expect(onlyValidValues()).to.eql([1, undefined]);
});
it('supports decimals', function () {
compile(['1.2', '000001.6', '99.10']);
expect($scope.vals).to.eql([1.2, 1.6, 99.1]);
expect(onlyValidValues()).to.eql([1.2, 1.6, 99.1]);
});
it('ensures that the values are in order', function () {
compile([1, 2, 3, 10, 4, 5]);
expect($scope.vals).to.eql([1, 2, 3, undefined, 4, 5]);
expect($el.find('.ng-invalid').size()).to.be(1);
expect(onlyValidValues()).to.eql([1, 2, 3, 10, undefined, 5]);
});
describe('ensures that the values are between 0 and 100', function () {
it(': -1', function () {
compile([-1, 1]);
expect($scope.vals).to.eql([undefined, 1]);
expect($el.find('.ng-invalid').size()).to.be(1);
});
it(': 0', function () {
compile([0, 1]);
expect($scope.vals).to.eql([undefined, 1]);
expect($el.find('.ng-invalid').size()).to.be(1);
});
it(': 0.0001', function () {
compile([0.0001, 1]);
expect($scope.vals).to.eql([0.0001, 1]);
expect($el.find('.ng-invalid').size()).to.be(0);
});
it(': 99.9999999', function () {
compile([1, 99.9999999]);
expect($scope.vals).to.eql([1, 99.9999999]);
expect($el.find('.ng-invalid').size()).to.be(0);
});
it(': 101', function () {
compile([1, 101]);
expect($scope.vals).to.eql([1, undefined]);
expect($el.find('.ng-invalid').size()).to.be(1);
});
it('requires that values are within a range', function () {
compile([50, 100, 200, 250], '[100, 200)');
expect(onlyValidValues()).to.eql([undefined, 100, undefined, undefined]);
});
describe('listens for keyboard events', function () {
@ -94,7 +67,7 @@ define(function (require) {
['up', 'up', 'up']
)
.then(function () {
expect($scope.vals).to.eql([4]);
expect(onlyValidValues()).to.eql([4]);
});
});
@ -118,7 +91,7 @@ define(function (require) {
seq
)
.then(function () {
expect($scope.vals).to.eql([5.1]);
expect(onlyValidValues()).to.eql([5.1]);
});
});
@ -130,7 +103,7 @@ define(function (require) {
['down', 'down', 'down']
)
.then(function () {
expect($scope.vals).to.eql([2]);
expect(onlyValidValues()).to.eql([2]);
});
});
@ -154,7 +127,7 @@ define(function (require) {
seq
)
.then(function () {
expect($scope.vals).to.eql([4.8]);
expect(onlyValidValues()).to.eql([4.8]);
});
});
@ -170,9 +143,9 @@ define(function (require) {
return simulateKeys(getEl, seq)
.then(function () {
expect($scope.vals).to.eql([9, 10, 13]);
expect(onlyValidValues()).to.eql([9, 10, 13]);
});
});
});
});
});
});

View file

@ -0,0 +1,74 @@
define(function (require) {
describe('config component', function () {
var $scope;
var config;
var defaults;
var configFile;
beforeEach(module('kibana'));
beforeEach(inject(function ($injector, Private) {
config = $injector.get('config');
$scope = $injector.get('$rootScope');
configFile = $injector.get('configFile');
defaults = Private(require('components/config/defaults'));
}));
it('exposes the configFile', function () {
expect(config.file).to.be(configFile);
});
describe('#get', function () {
it('gives access to config values', function () {
expect(config.get('dateFormat')).to.be.a('string');
});
it('reads from the defaults', function () {
var initial = config.get('dateFormat');
var newDefault = initial + '- new';
defaults.dateFormat.value = newDefault;
expect(config.get('dateFormat')).to.be(newDefault);
});
});
describe('#set', function () {
it('stores a value in the config val set', function () {
var initial = config.get('dateFormat');
config.set('dateFormat', 'notaformat');
expect(config.get('dateFormat')).to.be('notaformat');
});
});
describe('#$bind', function () {
it('binds a config key to a $scope property', function () {
var dateFormat = config.get('dateFormat');
config.$bind($scope, 'dateFormat');
expect($scope).to.have.property('dateFormat', dateFormat);
});
it('alows overriding the property name', function () {
var dateFormat = config.get('dateFormat');
config.$bind($scope, 'dateFormat', 'defaultDateFormat');
expect($scope).to.not.have.property('dateFormat');
expect($scope).to.have.property('defaultDateFormat', dateFormat);
});
it('keeps the property up to date', function () {
var dateFormat = config.get('dateFormat');
var newDateFormat = dateFormat + ' NEW NEW NEW!';
config.$bind($scope, 'dateFormat');
expect($scope).to.have.property('dateFormat', dateFormat);
config.set('dateFormat', newDateFormat);
expect($scope).to.have.property('dateFormat', newDateFormat);
});
});
});
});

View file

@ -64,7 +64,7 @@ define(function (require) {
// make the element
$elem = angular.element(
'<vis-editor-agg></vis-editor-agg>'
'<ng-form vis-editor-agg></ng-form>'
);
// compile the html
@ -77,7 +77,7 @@ define(function (require) {
$scope = $elem.isolateScope();
}));
it('should only add the close button only if there is more than the minimum', function () {
it('should only add the close button if there is more than the minimum', function () {
expect($parentScope.canRemove($parentScope.agg)).to.be(false);
$parentScope.group.push({
id: '3',

View file

@ -8,11 +8,11 @@ define(function (require) {
['aaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaa<wbr>aaaaaaaaa'],
['aaaa aaaaaaaaaaaaaaa', 'aaaa aaaaaaaaaaa<wbr>aaaa'],
['aaaa;aaaaaaaaaaaaaaa', 'aaaa;aaaaaaaaaaa<wbr>aaaa'],
['aaaa&aaaaaaaaaaaaaaa', 'aaaa&aaaaaa<wbr>aaaaaaaaa'],
['aaaa:aaaaaaaaaaaaaaa', 'aaaa:aaaaaaaaaaa<wbr>aaaa'],
['aaaa,aaaaaaaaaaaaaaa', 'aaaa,aaaaaaaaaaa<wbr>aaaa'],
['aaaa aaaa', 'aaaa aaaa'],
['aaaa <mark>aaaa</mark>aaaaaaaaaaaa', 'aaaa <mark>aaaa</mark>aaaaaaaaaaa<wbr>a']
['aaaa <mark>aaaa</mark>aaaaaaaaaaaa', 'aaaa <mark>aaaa</mark>aaaaaaaaaaa<wbr>a'],
['aaaa&quot;aaaaaaaaaaaa', 'aaaa&quot;aaaaaaaaaaa<wbr>a']
];
_.each(fixtures, function (fixture) {

View file

@ -0,0 +1,110 @@
define(function (require) {
describe('Range parsing utility', function () {
var _ = require('lodash');
var parse = require('utils/range');
it('throws an error for inputs that are not formatted properly', function () {
expect(function () {
parse('');
}).to.throwException(TypeError);
expect(function () {
parse('p10202');
}).to.throwException(TypeError);
expect(function () {
parse('{0,100}');
}).to.throwException(TypeError);
expect(function () {
parse('[0,100');
}).to.throwException(TypeError);
expect(function () {
parse(')0,100(');
}).to.throwException(TypeError);
});
var tests = {
'[ 0 , 100 ]': {
props: {
min: 0,
max: 100,
minInclusive: true,
maxInclusive: true
},
within: [
[0, true],
[0.0000001, true],
[1, true],
[99.99999, true],
[100, true]
]
},
'(26.3 , 42]': {
props: {
min: 26.3,
max: 42,
minInclusive: false,
maxInclusive: true
},
within: [
[26.2999999, false],
[26.3000001, true],
[30, true],
[41, true],
[42, true]
]
},
'(-50,50)': {
props: {
min: -50,
max: 50,
minInclusive: false,
maxInclusive: false
},
within: [
[-50, false],
[-49.99999, true],
[0, true],
[49.99999, true],
[50, false]
]
},
'(Infinity, -Infinity)': {
props: {
min: -Infinity,
max: Infinity,
minInclusive: false,
maxInclusive: false
},
within: [
[0, true],
[-0.0000001, true],
[-1, true],
[-10000000000, true],
[-Infinity, false],
[Infinity, false],
]
}
};
_.forOwn(tests, function (spec, str) {
describe(str, function () {
var range = parse(str);
it('creation', function () {
expect(range).to.eql(spec.props);
});
spec.within.forEach(function (tup) {
it('#within(' + tup[0] + ')', function () {
expect(range.within(tup[0])).to.be(tup[1]);
});
});
});
});
});
});

View file

@ -16,7 +16,11 @@ define(function (require) {
['test / ^test', 'test-slash-^test'],
['test ? test', 'test-questionmark-test'],
['test = test', 'test-equal-test'],
['test & test', 'test-ampersand-test']
['test & test', 'test-ampersand-test'],
['test/test/test', 'test-slash-test-slash-test'],
['test?test?test', 'test-questionmark-test-questionmark-test'],
['test&test&test', 'test-ampersand-test-ampersand-test'],
['test=test=test', 'test-equal-test-equal-test']
];
_.each(fixtures, function (fixture) {

View file

@ -5,17 +5,20 @@ define(function (require) {
var slices = require('vislib_fixtures/mock_data/histogram/_slices');
var stackedSeries = require('vislib_fixtures/mock_data/date_histogram/_stacked_series');
var histogramSlices = require('vislib_fixtures/mock_data/histogram/_slices');
var dataArray = [
stackedSeries,
slices,
histogramSlices,
stackedSeries,
stackedSeries,
stackedSeries
];
var chartTypes = [
'histogram',
'pie',
'pie',
'area',
'line'
];
@ -24,7 +27,7 @@ define(function (require) {
histogram: '.chart rect',
pie: '.chart path',
area: '.chart path',
line: '.chart circle',
line: '.chart circle'
};
angular.module('LegendFactory', ['kibana']);

View file

@ -0,0 +1,127 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var $ = require('jquery');
var fixtures = require('fixtures/fake_hierarchical_data');
var series = require('vislib_fixtures/mock_data/date_histogram/_series');
var terms = require('vislib_fixtures/mock_data/terms/_columns');
angular.module('TimeMarkerFactory', ['kibana']);
describe('VisLib Time Marker Test Suite', function () {
var height = 50;
var color = '#ff0000';
var opacity = 0.5;
var width = 3;
var customClass = 'custom-time-marker';
var dateMathTimes = ['now-1m', 'now-5m', 'now-15m'];
var myTimes = dateMathTimes.map(function (dateMathString) {
return {
time: dateMathString,
class: customClass,
color: color,
opacity: opacity,
width: width
};
});
var getExtent = function (dataArray, func) {
return func(dataArray, function (obj) {
return func(obj.values, function (d) {
return d.x;
});
});
};
var times = [];
var TimeMarker;
var defaultMarker;
var customMarker;
var selection;
var xScale;
var minDomain;
var maxDomain;
var domain;
beforeEach(function () {
module('TimeMarkerFactory');
});
beforeEach(function () {
inject(function (d3, Private) {
TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
minDomain = getExtent(series.series, d3.min);
maxDomain = getExtent(series.series, d3.max);
domain = [minDomain, maxDomain];
xScale = d3.time.scale().domain(domain).range([0, 500]);
defaultMarker = new TimeMarker(times, xScale, height);
customMarker = new TimeMarker(myTimes, xScale, height);
selection = d3.select('body').append('div').attr('class', 'marker');
selection.datum(series);
});
});
afterEach(function () {
selection.remove('*');
selection = null;
defaultMarker = null;
});
describe('_isTimeBaseChart method', function () {
var boolean;
var newSelection;
it('should return true when data is time based', function () {
boolean = defaultMarker._isTimeBasedChart(selection);
expect(boolean).to.be(true);
});
it('should return false when data is not time based', function () {
newSelection = selection.datum(terms);
boolean = defaultMarker._isTimeBasedChart(newSelection);
expect(boolean).to.be(false);
});
});
describe('render method', function () {
var lineArray;
beforeEach(function () {
defaultMarker.render(selection);
customMarker.render(selection);
lineArray = document.getElementsByClassName('custom-time-marker');
});
it('should render the default line', function () {
expect(!!$('line.time-marker').length).to.be(true);
});
it('should render the custom (user defined) lines', function () {
expect($('line.custom-time-marker').length).to.be(myTimes.length);
});
it('should set the class', function () {
Array.prototype.forEach.call(lineArray, function (line) {
expect(line.getAttribute('class')).to.be(customClass);
});
});
it('should set the stroke', function () {
Array.prototype.forEach.call(lineArray, function (line) {
expect(line.getAttribute('stroke')).to.be(color);
});
});
it('should set the stroke-opacity', function () {
Array.prototype.forEach.call(lineArray, function (line) {
expect(+line.getAttribute('stroke-opacity')).to.be(opacity);
});
});
it('should set the stroke-width', function () {
Array.prototype.forEach.call(lineArray, function (line) {
expect(+line.getAttribute('stroke-width')).to.be(width);
});
});
});
});
});