Merge remote-tracking branch 'upstream/master' into tweat/visualize_button_position

This commit is contained in:
Rashid Khan 2015-04-23 16:39:36 -07:00
commit 94085f9e02
34 changed files with 833 additions and 398 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

@ -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
@ -119,20 +120,7 @@ define(function (require) {
_.assign(self, self._source);
return Promise.try(function () {
// if we have a searchSource, set it's state based on the searchSourceJSON field
if (self.searchSource) {
var state = {};
try {
state = JSON.parse(meta.searchSourceJSON);
} catch (e) {}
var oldState = self.searchSource.toJSON();
var fnProps = _.transform(oldState, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
}, {});
self.searchSource.set(_.defaults(state, fnProps));
}
parseSearchSource(meta.searchSourceJSON);
})
.then(hydrateIndexPattern)
.then(function () {
@ -153,6 +141,23 @@ define(function (require) {
});
});
function parseSearchSource(searchSourceJson) {
if (!self.searchSource) return;
// if we have a searchSource, set its state based on the searchSourceJSON field
var state = {};
try {
state = JSON.parse(searchSourceJson);
} catch (e) {}
var oldState = self.searchSource.toJSON();
var fnProps = _.transform(oldState, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
}, {});
self.searchSource.set(_.defaults(state, fnProps));
}
/**
* After creation or fetching from ES, ensure that the searchSources index indexPattern
* is an bonafide IndexPattern object.
@ -181,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) {
@ -205,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);
@ -229,7 +244,7 @@ define(function (require) {
return docSource.doCreate(source)
.then(finish)
.catch(function (err) {
var confirmMessage = 'Are you sure you want to overwrite this?';
var confirmMessage = 'Are you sure you want to overwrite ' + self.title + '?';
if (_.deepGet(err, 'origError.status') === 409 && window.confirm(confirmMessage)) {
return docSource.doIndex(source).then(finish);
}
@ -264,7 +279,6 @@ define(function (require) {
});
});
};
}
return SavedObject;

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

@ -49,6 +49,11 @@ define(function (require) {
throw new Error('No valid data!');
}
if (this.handler) {
this.data = null;
this._runOnHandler('destroy');
}
this.data = data;
this.handler = handlerTypes[chartType](this) || handlerTypes.column(this);
this._runOnHandler('render');

View file

@ -0,0 +1,32 @@
define(function (require) {
var module = require('modules').get('kibana');
var $ = require('jquery');
module.directive('fileUpload', function ($parse) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
var onUpload = $parse(attrs.fileUpload);
var $fileInput = $('<input type="file" style="opacity: 0" id="testfile" />');
$elem.after($fileInput);
$fileInput.on('change', function (e) {
var reader = new FileReader();
reader.onload = function (e) {
$scope.$apply(function () {
onUpload($scope, {fileContents: e.target.result});
});
};
var target = e.srcElement || e.target;
if (target && target.files && target.files.length) reader.readAsText(target.files[0]);
});
$elem.on('click', function (e) {
$fileInput.trigger('click');
});
}
};
});
});

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

@ -8,11 +8,15 @@
<span class="smaller">{{conf.description}}</span>
</td>
<td class="value">
<!-- Settings editors -->
<form
name="forms.configEdit"
ng-if="conf.editting"
ng-submit="save(conf)"
role="form">
<input
ng-show="conf.normal"
ng-model="conf.unsavedValue"
@ -20,14 +24,7 @@
placeholder="{{conf.value || conf.defVal}}"
type="text"
class="form-control">
<input
ng-show="conf.array"
ng-list=","
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
placeholder="{{(conf.value || conf.defVal).join(', ')}}"
type="text"
class="form-control">
<textarea
ng-if="conf.json"
type="text"
@ -36,16 +33,38 @@
ng-keyup="maybeCancel($event, conf)"
validate-json
></textarea>
<small ng-show="forms.configEdit.$error.jsonInput">Invalid JSON syntax</small>
<small ng-show="forms.configEdit.$error.jsonInput">Invalid JSON syntax</small>
<input
ng-show="conf.array"
ng-list=","
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
placeholder="{{(conf.value || conf.defVal).join(', ')}}"
type="text"
class="form-control">
<input
ng-show="conf.bool"
ng-model="conf.unsavedValue"
type="checkbox"
class="form-control">
<select
ng-show="conf.select"
name="conf.name"
ng-model="conf.unsavedValue"
ng-options="option as option for option in conf.options"
class="form-control">
</select>
</form>
<span ng-if="!conf.editting && (conf.normal || conf.json)">{{conf.value || conf.defVal}}</span>
<span ng-if="!conf.editting && conf.bool">{{conf.value === undefined ? conf.defVal : conf.value}}</span>
<span ng-if="!conf.editting && conf.array">{{(conf.value || conf.defVal).join(', ')}}</span>
<!-- Setting display formats -->
<span ng-if="!conf.editting">
<span ng-show="(conf.normal || conf.json || conf.select)">{{conf.value || conf.defVal}}</span>
<span ng-show="conf.array">{{(conf.value || conf.defVal).join(', ')}}</span>
<span ng-show="conf.bool">{{conf.value === undefined ? conf.defVal : conf.value}}</span>
</span>
</td>
<td class="actions">

View file

@ -20,7 +20,7 @@ define(function (require) {
ESC: 27
};
var NAMED_EDITORS = ['json', 'array', 'boolean'];
var NAMED_EDITORS = ['json', 'array', 'boolean', 'select'];
var NORMAL_EDITOR = ['number', 'string', 'null', 'undefined'];
function getEditorType(conf) {
@ -42,11 +42,13 @@ define(function (require) {
defVal: def.value,
type: getValType(def, val),
description: def.description,
options: def.options,
value: val,
};
var editor = getEditorType(conf);
conf.json = editor === 'json';
conf.select = editor === 'select';
conf.bool = editor === 'boolean';
conf.array = editor === 'array';
conf.normal = editor === 'normal';

View file

@ -1,6 +1,10 @@
<kbn-settings-app section="objects">
<kbn-settings-objects class="container">
<h2>Edit Saved Objects</h2>
<div class="header">
<h2 class="title">Edit Saved Objects</h2>
<button class="btn btn-default controls" ng-click="exportAll()"><i aria-hidden="true" class="fa fa-download"></i> Export</button>
<button file-upload="importAll(fileContents)" class="btn btn-default controls" ng-click><i aria-hidden="true" class="fa fa-upload"></i> Import</button>
</div>
<p>
From here you can delete saved objects, such as saved searches. You can also edit the raw data of saved objects. Typically objects are only modified via their associated application, which is probably what you should use instead of this screen. Each tab is limited to 100 results. You can use the filter to find objects not in the default list.
</p>
@ -20,13 +24,16 @@
<div class="tab-content">
<div class="action-bar">
<label>
<input type="checkbox" ng-model="deleteAll">
<input type="checkbox" ng-checked="currentTab.data.length > 0 && selectedItems.length == currentTab.data.length" ng-click="toggleAll()" />
Select All
</label>
<a ng-disabled="!deleteAllBtn"
<a ng-disabled="selectedItems.length == 0"
confirm-click="bulkDelete()"
confirmation="Are you sure want to delete the selected {{service.title}}? This action is irreversible!"
class="delete-all btn btn-danger btn-xs" aria-label="Delete Selected"><i aria-hidden="true" class="fa fa-trash"></i> Delete Selected</a>
confirmation="Are you sure want to delete the selected {{currentTab.title}}? This action is irreversible!"
class="btn btn-xs btn-danger" aria-label="Delete"><i aria-hidden="true" class="fa fa-trash"></i> Delete</a>
<a ng-disabled="selectedItems.length == 0"
ng-click="bulkExport()"
class="btn btn-xs btn-default" aria-label="Export"><i aria-hidden="true" class="fa fa-download"></i> Export</a>
</div>
<div ng-repeat="service in services" ng-class="{ active: state.tab === service.title }" class="tab-pane">
<ul class="list-unstyled">
@ -51,8 +58,8 @@
<div class="pull-left">
<input
ng-click="item.checked = !item.checked; toggleDeleteBtn(service)"
ng-checked="item.checked"
ng-click="toggleItem(item)"
ng-checked="selectedItems.indexOf(item) >= 0"
type="checkbox" >
</div>

View file

@ -1,8 +1,12 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
var saveAs = require('file_saver');
var registry = require('plugins/settings/saved_object_registry');
var objectIndexHTML = require('text!plugins/settings/sections/objects/_objects.html');
require('directives/file_upload');
require('routes')
.when('/settings/objects', {
template: objectIndexHTML
@ -12,42 +16,52 @@ define(function (require) {
.directive('kbnSettingsObjects', function (config, Notifier, Private, kbnUrl) {
return {
restrict: 'E',
controller: function ($scope, $injector, $q, AppState) {
controller: function ($scope, $injector, $q, AppState, es) {
var notify = new Notifier({ location: 'Saved Objects' });
var $state = $scope.state = new AppState();
var resetCheckBoxes = function () {
$scope.deleteAll = false;
_.each($scope.services, function (service) {
_.each(service.data, function (item) {
item.checked = false;
});
});
};
$scope.currentTab = null;
$scope.selectedItems = [];
var getData = function (filter) {
var services = registry.all().map(function (obj) {
var service = $injector.get(obj.service);
return service.find(filter).then(function (data) {
return { service: obj.service, title: obj.title, data: data.hits, total: data.total };
return {
service: service,
serviceName: obj.service,
title: obj.title,
type: service.type,
data: data.hits,
total: data.total
};
});
});
$q.all(services).then(function (data) {
$scope.services = _.sortBy(data, 'title');
if (!$state.tab) {
$scope.changeTab($scope.services[0]);
}
var tab = $scope.services[0];
if ($state.tab) tab = _.find($scope.services, {title: $state.tab});
$scope.changeTab(tab);
});
};
$scope.$watch('deleteAll', function (checked) {
var service = _.find($scope.services, { title: $state.tab });
if (!service) return;
_.each(service.data, function (item) {
item.checked = checked;
});
$scope.toggleDeleteBtn(service);
});
$scope.toggleAll = function () {
if ($scope.selectedItems.length === $scope.currentTab.data.length) {
$scope.selectedItems.length = 0;
} else {
$scope.selectedItems = [].concat($scope.currentTab.data);
}
};
$scope.toggleItem = function (item) {
var i = $scope.selectedItems.indexOf(item);
if (i >= 0) {
$scope.selectedItems.splice(i, 1);
} else {
$scope.selectedItems.push(item);
}
};
$scope.open = function (item) {
kbnUrl.change(item.url.substr(1));
@ -55,43 +69,103 @@ define(function (require) {
$scope.edit = function (service, item) {
var params = {
service: service.service,
service: service.serviceName,
id: item.id
};
kbnUrl.change('/settings/objects/{{ service }}/{{ id }}', params);
};
$scope.toggleDeleteBtn = function (service) {
$scope.deleteAllBtn = _.some(service.data, { checked: true});
$scope.bulkDelete = function () {
$scope.currentTab.service.delete(_.pluck($scope.selectedItems, 'id')).then(refreshData);
};
$scope.bulkDelete = function () {
var serviceObj = _.find($scope.services, { title: $state.tab });
if (!serviceObj) return;
var service = $injector.get(serviceObj.service);
var ids = _(serviceObj.data)
.filter({ checked: true})
.pluck('id')
.value();
service.delete(ids).then(function (resp) {
serviceObj.data = _.filter(serviceObj.data, function (obj) {
return !obj.checked;
});
resetCheckBoxes();
$scope.bulkExport = function () {
var objs = $scope.selectedItems.map(_.partialRight(_.extend, {type: $scope.currentTab.type}));
retrieveAndExportDocs(objs);
};
$scope.exportAll = function () {
var objs = $scope.services.map(function (service) {
return service.data.map(_.partialRight(_.extend, {type: service.type}));
});
retrieveAndExportDocs(_.flatten(objs));
};
function retrieveAndExportDocs(objs) {
es.mget({
index: config.file.kibana_index,
body: {docs: objs.map(transformToMget)}
})
.then(function (response) {
saveToFile(response.docs.map(_.partialRight(_.pick, '_id', '_type', '_source')));
});
}
// Takes an object and returns the associated data needed for an mget API request
function transformToMget(obj) {
return {_id: obj.id, _type: obj.type};
}
function saveToFile(results) {
var blob = new Blob([angular.toJson(results, true)], {type: 'application/json'});
saveAs(blob, 'export.json');
}
$scope.importAll = function (fileContents) {
var docs;
try {
docs = JSON.parse(fileContents);
} catch (e) {
notify.error('The file could not be processed.');
}
return es.mget({
index: config.file.kibana_index,
body: {docs: docs.map(_.partialRight(_.pick, '_id', '_type'))}
})
.then(function (response) {
var existingDocs = _.where(response.docs, {found: true});
var confirmMessage = 'The following objects will be overwritten:\n\n';
if (existingDocs.length === 0 || window.confirm(confirmMessage + _.pluck(existingDocs, '_id').join('\n'))) {
return es.bulk({
index: config.file.kibana_index,
body: _.flatten(docs.map(transformToBulk))
})
.then(refreshIndex)
.then(refreshData, notify.error);
}
});
};
$scope.changeTab = function (obj) {
$state.tab = obj.title;
// Takes a doc and returns the associated two entries for an index bulk API request
function transformToBulk(doc) {
return [
{index: _.pick(doc, '_id', '_type')},
doc._source
];
}
function refreshIndex() {
return es.indices.refresh({
index: config.file.kibana_index
});
}
function refreshData() {
return getData($scope.advancedFilter);
}
$scope.changeTab = function (tab) {
$scope.currentTab = tab;
$scope.selectedItems.length = 0;
$state.tab = tab.title;
$state.save();
resetCheckBoxes();
};
$scope.$watch('advancedFilter', function (filter) {
getData(filter);
});
}
};
});

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

@ -29,7 +29,7 @@ define(function (require) {
*
* @param {array} memo The stack of fields
* @param {mixed} value The value of the field
* @param {stirng} key The key of the field
* @param {string} key The key of the field
* @param {object} collection This is a reference the collection being reduced
* @param {array} parents The parent keys to the field
* @returns {array}
@ -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

@ -48,12 +48,18 @@ kbn-settings-objects {
font-weight: normal;
}
.delete-all {
.btn {
font-size: 10px;
margin-left: 20px;
}
}
.header {
.title, .controls {
padding-right: 1em;
display: inline-block;
}
}
}
kbn-settings-advanced {

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,18 +9,22 @@
<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>
@ -45,7 +49,7 @@
</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

@ -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

@ -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]);
});
});
});
});
});
});