mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Merge branch 'develop' into vislib/refactor1
Merging develop
This commit is contained in:
commit
6f6e054fda
21 changed files with 553 additions and 113 deletions
34
TODOS.md
Normal file
34
TODOS.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# TODO items
|
||||
> Automatically extracted
|
||||
|
||||
- **src/kibana/apps/dashboard/directives/grid.js**
|
||||
- change this from event based to calling a method on dashboardApp – [L68](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/dashboard/directives/grid.js#L68)
|
||||
- **src/kibana/apps/discover/controllers/discover.js**
|
||||
- Switch this to watching time.string when we implement it – [L148](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L148)
|
||||
- On array fields, negating does not negate the combination, rather all terms – [L431](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L431)
|
||||
- Move to utility class – [L496](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L496)
|
||||
- Move to utility class – [L506](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js#L506)
|
||||
- **src/kibana/apps/settings/sections/indices/_create.js**
|
||||
- we should probably display a message of some kind – [L111](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/settings/sections/indices/_create.js#L111)
|
||||
- **src/kibana/apps/visualize/controllers/editor.js**
|
||||
- Switch this to watching time.string when we implement it – [L189](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/controllers/editor.js#L189)
|
||||
- **src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js**
|
||||
- Should we abtract out the agg building stuff? – [L58](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js#L58)
|
||||
- Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js – [L89](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js#L89)
|
||||
- **src/kibana/apps/visualize/saved_visualizations/_type_defs.js**
|
||||
- We need to be able to get ahold of angular services here – [L16](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js#L16)
|
||||
- We need to be able to get ahold of angular services here – [L88](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js#L88)
|
||||
- **src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js**
|
||||
- We need more just _count here. – [L26](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js#L26)
|
||||
- **src/kibana/components/index_patterns/_mapper.js**
|
||||
- Change index to be the resolved in some way, last three months, last hour, last year, whatever – [L49](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/index_patterns/_mapper.js#L49)
|
||||
- **src/kibana/components/state_management/state.js**
|
||||
- Change all the references to onUpdate to the actual fetch_with_changes event – [L72](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/state_management/state.js#L72)
|
||||
- **src/kibana/directives/rows.js**
|
||||
- It would be better to actually check the type of the field, but we don't have – [L38](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/directives/rows.js#L38)
|
||||
- **src/kibana/services/timefilter.js**
|
||||
- This should be disabled on route change, apps need to enable it explicitly – [L12](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/services/timefilter.js#L12)
|
||||
- **test/unit/specs/apps/dashboard/directives/panel.js**
|
||||
- This should not be needed, timefilter is only included here – [L14](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/apps/dashboard/directives/panel.js#L14)
|
||||
- **test/unit/specs/directives/timepicker.js**
|
||||
- This should not be needed, timefilter is only included here, it should move – [L17](https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/directives/timepicker.js#L17)
|
|
@ -33,7 +33,7 @@
|
|||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"server": "grunt server",
|
||||
"precommit": "grunt jshint"
|
||||
"precommit": "grunt jshint todos"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -77,7 +77,8 @@ define(function (require) {
|
|||
query: initialQuery || '',
|
||||
columns: ['_source'],
|
||||
index: config.get('defaultIndex'),
|
||||
interval: 'auto'
|
||||
interval: 'auto',
|
||||
filters: _.cloneDeep($scope.searchSource.get('filter'))
|
||||
};
|
||||
|
||||
var metaFields = config.get('metaFields');
|
||||
|
@ -93,7 +94,7 @@ define(function (require) {
|
|||
'year'
|
||||
];
|
||||
|
||||
var $state = $scope.state = new appStateFactory.create(stateDefaults);
|
||||
var $state = $scope.state = appStateFactory.create(stateDefaults);
|
||||
|
||||
if (!_.contains(indexPatternList, $state.index)) {
|
||||
var reason = 'The index specified in the URL is not a configured pattern. ';
|
||||
|
@ -161,6 +162,10 @@ define(function (require) {
|
|||
if (!angular.equals(sort, currentSort)) $scope.fetch();
|
||||
});
|
||||
|
||||
$scope.$watch('state.filters', function (filters) {
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
$scope.$watch('opts.timefield', function (timefield) {
|
||||
timefilter.enabled(!!timefield);
|
||||
});
|
||||
|
@ -199,6 +204,8 @@ define(function (require) {
|
|||
if (!init.complete) return;
|
||||
|
||||
$scope.updateTime();
|
||||
if (_.isEmpty($state.columns)) refreshColumns();
|
||||
$state.save();
|
||||
$scope.updateDataSource()
|
||||
.then(setupVisualization)
|
||||
.then(function () {
|
||||
|
@ -340,7 +347,6 @@ define(function (require) {
|
|||
|
||||
$scope.updateDataSource = function () {
|
||||
var chartOptions;
|
||||
|
||||
$scope.searchSource
|
||||
.size($scope.opts.sampleSize)
|
||||
.sort(function () {
|
||||
|
@ -354,7 +360,8 @@ define(function (require) {
|
|||
}
|
||||
return sort;
|
||||
})
|
||||
.query(!$state.query ? null : $state.query);
|
||||
.query(!$state.query ? null : $state.query)
|
||||
.set('filter', $state.filters || []);
|
||||
|
||||
// get the current indexPattern
|
||||
var indexPattern = $scope.searchSource.get('index');
|
||||
|
@ -432,20 +439,26 @@ define(function (require) {
|
|||
// TODO: On array fields, negating does not negate the combination, rather all terms
|
||||
$scope.filterQuery = function (field, value, operation) {
|
||||
value = _.isArray(value) ? value : [value];
|
||||
operation = operation || '+';
|
||||
|
||||
var indexPattern = $scope.searchSource.get('index');
|
||||
indexPattern.popularizeField(field, 1);
|
||||
|
||||
_.each(value, function (clause) {
|
||||
var filter = field + ':"' + addSlashes(clause) + '"';
|
||||
var regex = '[\\+-]' + regexEscape(filter) + '\\s*';
|
||||
// Grap the filters from the searchSource and ensure it's an array
|
||||
var filters = _.flatten([$state.filters], true);
|
||||
|
||||
$state.query = $state.query.replace(new RegExp(regex), '') +
|
||||
' ' + operation + filter;
|
||||
_.each(value, function (clause) {
|
||||
var previous = _.find(filters, function (item) {
|
||||
return item && item.query.match[field] === clause;
|
||||
});
|
||||
if (!previous) {
|
||||
var filter = { query: { match: {} } };
|
||||
filter.negate = operation === '-';
|
||||
filter.query.match[field] = clause;
|
||||
filters.push(filter);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.fetch();
|
||||
$state.filters = filters;
|
||||
};
|
||||
|
||||
$scope.toggleField = function (name) {
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
<config config-template="configTemplate" config-object="opts" config-close="configClose" config-submit="fetch"></config>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<filter-bar state="state"></filter-bar>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="discover-hits"><strong>{{hits || 0}}</strong> hits</div>
|
||||
|
||||
|
|
|
@ -19,6 +19,19 @@ define(function (require) {
|
|||
var serviceObj = registry.get($routeParams.service);
|
||||
var service = $injector.get(serviceObj.service);
|
||||
|
||||
/**
|
||||
* Creates a field definition and pushes it to the memo stack. This function
|
||||
* is designed to be used in conjunction with _.reduce(). If the
|
||||
* values is plain object it will recurse through all the keys till it hits
|
||||
* a string, number or an array.
|
||||
*
|
||||
* @param {array} memo The stack of fields
|
||||
* @param {mixed} value The value of the field
|
||||
* @param {stirng} 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}
|
||||
*/
|
||||
var createField = function (memo, val, key, collection, parents) {
|
||||
if (_.isArray(parents)) {
|
||||
parents.push(key);
|
||||
|
@ -92,6 +105,11 @@ define(function (require) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an object and sets the notification
|
||||
* @param {type} name description
|
||||
* @returns {type} description
|
||||
*/
|
||||
$scope.delete = function () {
|
||||
$scope.obj.delete().then(function (resp) {
|
||||
$location.path('/settings/objects').search({ _a: rison.encode({
|
||||
|
@ -132,4 +150,4 @@ define(function (require) {
|
|||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ define(function (require) {
|
|||
$scope.fields = _.sortBy(indexPattern.fields, 'name');
|
||||
$scope.fields.byName = indexPattern.fieldsByName;
|
||||
|
||||
var $state = $scope.state = new appStateFactory.create(vis.getState());
|
||||
var $state = $scope.state = appStateFactory.create(vis.getState());
|
||||
|
||||
if ($state.query) {
|
||||
vis.searchSource.set('query', $state.query);
|
||||
|
|
|
@ -247,6 +247,29 @@ define(function (require) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter that can be reversed for filters with negate set
|
||||
* @param {boolean} reverse This will reverse the filter. If true then
|
||||
* anything where negate is set will come
|
||||
* through otherwise it will filter out
|
||||
* @returns {function}
|
||||
*/
|
||||
var filterNegate = function (reverse) {
|
||||
return function (filter) {
|
||||
if (_.isUndefined(filter.negate)) return !reverse;
|
||||
return filter.negate === reverse;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean out any invalid attributes from the filters
|
||||
* @param {object} filter The filter to clean
|
||||
* @returns {object}
|
||||
*/
|
||||
var cleanFilter = function (filter) {
|
||||
return _.omit(filter, ['negate', 'disabled']);
|
||||
};
|
||||
|
||||
// switch to filtered query if there are filters
|
||||
if (flatState.filters) {
|
||||
if (flatState.filters.length) {
|
||||
|
@ -255,7 +278,8 @@ define(function (require) {
|
|||
query: flatState.body.query,
|
||||
filter: {
|
||||
bool: {
|
||||
must: flatState.filters
|
||||
must: _(flatState.filters).filter(filterNegate(false)).map(cleanFilter).value(),
|
||||
must_not: _(flatState.filters).filter(filterNegate(true)).map(cleanFilter).value()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,7 +157,16 @@ define(function (require) {
|
|||
switch (key) {
|
||||
case 'filter':
|
||||
// user a shallow flatten to detect if val is an array, and pull the values out if it is
|
||||
state.filters = _.flatten([ state.filters || [], val ], true);
|
||||
state.filters = _([ state.filters || [], val ])
|
||||
.flatten(true)
|
||||
// Yo Dawg! I heard you needed to filter out your filters
|
||||
.filter(function (filter) {
|
||||
if (!filter) return false;
|
||||
// return true for anything that is either empty or false
|
||||
// return false for anything that is explicitly set to true
|
||||
return !filter.disabled;
|
||||
})
|
||||
.value();
|
||||
return;
|
||||
case 'index':
|
||||
case 'type':
|
||||
|
|
30
src/kibana/components/filter_bar/filter_bar.css
Normal file
30
src/kibana/components/filter_bar/filter_bar.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
filter-bar .bar {
|
||||
padding: 6px 6px 4px 6px;
|
||||
background: #dde4e6;
|
||||
}
|
||||
filter-bar .bar .title {
|
||||
display: inline;
|
||||
color: #748287;
|
||||
margin: 0 6px;
|
||||
}
|
||||
filter-bar .bar .filter {
|
||||
font-size: 12px;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
background-color: #83949C;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
filter-bar .bar .filter .value {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.4);
|
||||
padding-right: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
filter-bar .bar .filter.negate {
|
||||
background-color: #D18282;
|
||||
}
|
||||
filter-bar .bar .filter a {
|
||||
color: #FFF;
|
||||
}
|
9
src/kibana/components/filter_bar/filter_bar.html
Normal file
9
src/kibana/components/filter_bar/filter_bar.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="bar" ng-show="filters.length">
|
||||
<!-- <div class="title">Filters</div> -->
|
||||
<div class="filter" ng-class="{ negate: filter.negate }" ng-repeat="filter in filters">
|
||||
<span class="key">{{ filter.key }}:</span>
|
||||
<span class="value">"{{ filter.value }}"</span>
|
||||
<a class="fa" tooltip="Toggle" tooltip-placement="top" ng-class="{ 'fa-eye-slash': filter.disabled, 'fa-eye': !filter.disabled }" ng-click="toggleFilter(filter)"><a>
|
||||
<a class="fa fa-times" tooltip="Remove" tooltip-placement="top" ng-click="removeFilter(filter)"><a>
|
||||
</div>
|
||||
</div>
|
88
src/kibana/components/filter_bar/filter_bar.js
Normal file
88
src/kibana/components/filter_bar/filter_bar.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
define(function (require) {
|
||||
'use strict';
|
||||
var _ = require('lodash');
|
||||
var module = require('modules').get('kibana');
|
||||
var template = require('text!components/filter_bar/filter_bar.html');
|
||||
|
||||
module.directive('filterBar', function (courier) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
state: '='
|
||||
},
|
||||
link: function ($scope, $el, attrs) {
|
||||
|
||||
/**
|
||||
* Map the filter into an object with the key and value exposed so it's
|
||||
* easier to work with in the template
|
||||
* @param {object} fitler The filter the map
|
||||
* @returns {object}
|
||||
*/
|
||||
var mapFilter = function (filter) {
|
||||
var key = _.keys(filter.query.match)[0];
|
||||
return {
|
||||
key: key,
|
||||
value: filter.query.match[key],
|
||||
disabled: !!(filter.disabled),
|
||||
negate: !!(filter.negate),
|
||||
filter: filter
|
||||
};
|
||||
};
|
||||
|
||||
$scope.$watch('state.filters', function (filters) {
|
||||
// Get the filters from the searchSource
|
||||
$scope.filters = _(filters)
|
||||
.filter(function (filter) {
|
||||
return filter;
|
||||
})
|
||||
.flatten(true)
|
||||
.map(mapFilter)
|
||||
.value();
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Remap the filter from the intermediary back to it's original.
|
||||
* @param {object} filter The original filter
|
||||
* @returns {object}
|
||||
*/
|
||||
var remapFilters = function (filter) {
|
||||
return filter.filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the filter between enabled/disabled.
|
||||
* @param {object} filter The filter to toggle
|
||||
* @returns {void}
|
||||
*/
|
||||
$scope.toggleFilter = function (filter) {
|
||||
// Toggle the disabled flag
|
||||
var disabled = !filter.disabled;
|
||||
filter.disabled = disabled;
|
||||
filter.filter.disabled = disabled;
|
||||
|
||||
// Save the filters back to the searchSource
|
||||
$scope.state.filters = _.map($scope.filters, remapFilters);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the filter from the searchSource
|
||||
* @param {object} filter The filter to remove
|
||||
* @returns {void}
|
||||
*/
|
||||
$scope.removeFilter = function (invalidFilter) {
|
||||
// Remove the filter from the the scope $filters and map it back
|
||||
// to the original format to save in searchSource
|
||||
$scope.state.filters = _($scope.filters)
|
||||
.filter(function (filter) {
|
||||
return filter.filter !== invalidFilter.filter;
|
||||
})
|
||||
.map(remapFilters)
|
||||
.value();
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
35
src/kibana/components/filter_bar/filter_bar.less
Normal file
35
src/kibana/components/filter_bar/filter_bar.less
Normal file
|
@ -0,0 +1,35 @@
|
|||
filter-bar .bar {
|
||||
padding: 6px 6px 4px 6px;
|
||||
background: #dde4e6;
|
||||
|
||||
.title {
|
||||
display: inline;
|
||||
color: #748287;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
font-size: 12px;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
background-color: #83949C;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.value {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.4);
|
||||
padding-right: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.negate {
|
||||
background-color: #D18282;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ define(function (require) {
|
|||
var getAppStash = function (search) {
|
||||
var appStash = search._a && rison.decode(search._a);
|
||||
if (app.current) {
|
||||
// Apply the defaults to appStash
|
||||
appStash = _.defaults(appStash || {}, app.defaults);
|
||||
}
|
||||
return appStash;
|
||||
|
@ -137,4 +138,4 @@ define(function (require) {
|
|||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ define(function (require) {
|
|||
AppState.Super.call(this, '_a', defaults);
|
||||
}
|
||||
|
||||
|
||||
return AppState;
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ define(function (require) {
|
|||
require('components/courier/courier');
|
||||
require('components/notify/notify');
|
||||
require('components/state_management/app_state_factory');
|
||||
require('components/filter_bar/filter_bar');
|
||||
require('directives/info');
|
||||
require('directives/spinner');
|
||||
require('directives/paginate');
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
|
||||
return function EventsProvider(Private, PromiseEmitter) {
|
||||
return function EventsProvider(Private, Promise, Notifier) {
|
||||
var BaseObject = Private(require('factories/base_object'));
|
||||
var notify = new Notifier({ location: 'EventEmitter' });
|
||||
|
||||
_.inherits(Events, BaseObject);
|
||||
function Events() {
|
||||
|
@ -12,28 +13,42 @@ define(function (require) {
|
|||
|
||||
/**
|
||||
* Listens for events
|
||||
* @param {string} name The name of the event
|
||||
* @param {function} handler The handler for the event
|
||||
* @returns {PromiseEmitter}
|
||||
* @param {string} name - The name of the event
|
||||
* @param {function} handler - The function to call when the event is triggered
|
||||
* @returns {undefined}
|
||||
*/
|
||||
Events.prototype.on = function (name, handler) {
|
||||
var self = this;
|
||||
|
||||
if (!_.isArray(this._listeners[name])) {
|
||||
this._listeners[name] = [];
|
||||
}
|
||||
|
||||
return new PromiseEmitter(function (resolve, reject, defer) {
|
||||
defer._handler = handler;
|
||||
self._listeners[name].push(defer);
|
||||
}, handler);
|
||||
var listener = { handler: handler };
|
||||
|
||||
// capture the promise that is resolved when listener.defer is "fresh"/new
|
||||
// and attach it to the listener
|
||||
(function buildDefer(value) {
|
||||
|
||||
// we will execute the handler on each re-build, but not the initial build
|
||||
var rebuilding = listener.defer != null;
|
||||
|
||||
listener.defer = Promise.defer();
|
||||
listener.deferResolved = false;
|
||||
listener.newDeferPromise = listener.defer.promise.then(buildDefer);
|
||||
|
||||
if (!rebuilding) return;
|
||||
|
||||
// we ignore the completion of handlers, just watch for unhandled errors
|
||||
Promise.try(handler, [value]).catch(notify.fatal);
|
||||
}());
|
||||
|
||||
this._listeners[name].push(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a event listner
|
||||
* @param {string} name The name of the event
|
||||
* @param {function} [handler] The handler to remove
|
||||
* @return {void}
|
||||
* Removes an event listener
|
||||
* @param {string} [name] - The name of the event
|
||||
* @param {function} [handler] - The handler to remove
|
||||
* @return {undefined}
|
||||
*/
|
||||
Events.prototype.off = function (name, handler) {
|
||||
if (!name && !handler) {
|
||||
|
@ -47,31 +62,38 @@ define(function (require) {
|
|||
if (!handler) {
|
||||
delete this._listeners[name];
|
||||
} else {
|
||||
this._listeners[name] = _.filter(this._listeners[name], function (defer) {
|
||||
return handler !== defer._handler;
|
||||
this._listeners[name] = _.filter(this._listeners[name], function (listener) {
|
||||
return handler !== listener.handler;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits and event using the PromiseEmitter
|
||||
* @param {string} name The name of the event
|
||||
* @param {mixed} args The args to pass along to the handers
|
||||
* @returns {void}
|
||||
* Emits the event to all listeners
|
||||
*
|
||||
* @param {string} name - The name of the event.
|
||||
* @param {any} [value] - The value that will be passed to all event handlers.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
Events.prototype.emit = function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var name = args.shift();
|
||||
if (this._listeners[name]) {
|
||||
// We need to empty the array when we resolve the listners. PromiseEmitter
|
||||
// will regenerate the listners array with new promises.
|
||||
_.each(this._listeners[name].splice(0), function (defer) {
|
||||
defer.resolve.apply(defer, args);
|
||||
});
|
||||
Events.prototype.emit = function (name, value) {
|
||||
if (!this._listeners[name]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.map(this._listeners[name], function resolveListener(listener) {
|
||||
if (listener.deferResolved) {
|
||||
// this listener has already been resolved by another call to events#emit()
|
||||
// so we wait for listener.defer to be recreated and try again
|
||||
return listener.newDeferPromise.then(function () {
|
||||
return resolveListener(listener);
|
||||
});
|
||||
} else {
|
||||
listener.deferResolved = true;
|
||||
listener.defer.resolve(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return Events;
|
||||
|
||||
};
|
||||
});
|
||||
|
|
|
@ -347,3 +347,4 @@ input[type="checkbox"],
|
|||
}
|
||||
}
|
||||
|
||||
@import '../components/filter_bar/filter_bar.less';
|
||||
|
|
|
@ -9,7 +9,8 @@ module.exports = {
|
|||
'<%= src %>/kibana/apps/settings/styles/main.less',
|
||||
'<%= src %>/kibana/apps/visualize/styles/main.less',
|
||||
'<%= src %>/kibana/apps/visualize/styles/visualization.less',
|
||||
'<%= src %>/kibana/styles/main.less'
|
||||
'<%= src %>/kibana/styles/main.less',
|
||||
'<%= src %>/kibana/components/**/*.less'
|
||||
],
|
||||
expand: true,
|
||||
ext: '.css',
|
||||
|
@ -18,4 +19,4 @@ module.exports = {
|
|||
paths: [bc + '/lesshat/build/']
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,8 @@ module.exports = function (grunt) {
|
|||
},
|
||||
less: {
|
||||
files: [
|
||||
'<%= app %>/**/styles/**/*.less'
|
||||
'<%= app %>/**/styles/**/*.less',
|
||||
'<%= app %>/**/components/**/*.less'
|
||||
],
|
||||
tasks: ['less']
|
||||
},
|
||||
|
|
103
tasks/todos.js
Normal file
103
tasks/todos.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
module.exports = function (grunt) {
|
||||
var _ = require('lodash');
|
||||
var Promise = require('bluebird');
|
||||
var readFileAsync = Promise.promisify(require('fs').readFile);
|
||||
var spawnAsync = Promise.promisify(grunt.util.spawn);
|
||||
var path = require('path');
|
||||
var absolute = _.partial(path.join, path.join(__dirname, '..'));
|
||||
|
||||
var TODO_RE = /[\s\/\*]+(TODO|FIXME):?\s*(.+)/;
|
||||
var NEWLINE_RE = /\r?\n/;
|
||||
var TODO_FILENAME = 'TODOS.md';
|
||||
var TYPE_PRIORITIES = {
|
||||
'FIXME': 1
|
||||
};
|
||||
|
||||
grunt.registerTask('todos', function () {
|
||||
var files = grunt.file.expand([
|
||||
'src/kibana/**/*.js',
|
||||
'test/unit/specs/**/*.js'
|
||||
]);
|
||||
var matches = [];
|
||||
|
||||
var currentFile = null;
|
||||
if (grunt.file.exists(TODO_FILENAME)) {
|
||||
currentFile = grunt.file.read(TODO_FILENAME);
|
||||
}
|
||||
|
||||
Promise.map(files, function (path) {
|
||||
// grunt passes back these file names relative to the root... not
|
||||
// what we want when we are calling fs.readFile
|
||||
var absPath = absolute(path);
|
||||
|
||||
return readFileAsync(absPath, 'utf8')
|
||||
.then(function (file) {
|
||||
return file.split(NEWLINE_RE);
|
||||
})
|
||||
.each(function (line, i) {
|
||||
var match = line.match(TODO_RE);
|
||||
if (!match) return;
|
||||
|
||||
matches.push({
|
||||
type: match[1],
|
||||
msg: match[2],
|
||||
path: path,
|
||||
line: i + 1
|
||||
});
|
||||
});
|
||||
}, { concurrency: 50 })
|
||||
.then(function () {
|
||||
var newFileLines = [
|
||||
'# TODO items',
|
||||
'> Automatically extracted',
|
||||
''
|
||||
];
|
||||
|
||||
var groupedByPath = _.groupBy(matches, 'path');
|
||||
|
||||
Object.keys(groupedByPath)
|
||||
.sort(function (a, b) {
|
||||
var aChunks = a.split(path.sep);
|
||||
var bChunks = b.split(path.sep);
|
||||
|
||||
// compare the paths chunk by chunk
|
||||
for (var i = 0; i < aChunks.length; i++) {
|
||||
if (aChunks[i] === bChunks[i]) continue;
|
||||
return aChunks[i].localeCompare(bChunks[i] || '');
|
||||
}
|
||||
})
|
||||
.forEach(function (path) {
|
||||
newFileLines.push(' - **' + path + '**');
|
||||
|
||||
_(groupedByPath[path])
|
||||
.sortBy(function (match) {
|
||||
return TYPE_PRIORITIES[match.type] || 0;
|
||||
})
|
||||
.each(function (match) {
|
||||
var priority = TYPE_PRIORITIES[match.type] || 0;
|
||||
|
||||
newFileLines.push(
|
||||
' - ' + (priority ? match.type + ' – ' : '') +
|
||||
match.msg + ' – ' +
|
||||
'[L' + match.line + ']' +
|
||||
'(https://github.com/elasticsearch/kibana4/blob/master/' + match.path + '#L' + match.line + ')'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
var newFile = newFileLines.join('\n');
|
||||
|
||||
if (newFile !== currentFile) {
|
||||
grunt.log.ok('Committing updated TODO.md');
|
||||
grunt.file.write(TODO_FILENAME, newFile);
|
||||
return spawnAsync({
|
||||
cmd: 'git',
|
||||
args: ['add', absolute(TODO_FILENAME)]
|
||||
});
|
||||
} else {
|
||||
grunt.log.ok('No changes to commit to TODO.md');
|
||||
}
|
||||
})
|
||||
.nodeify(this.async());
|
||||
});
|
||||
};
|
|
@ -1,86 +1,132 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var sinon = require('sinon/sinon');
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
require('services/private');
|
||||
|
||||
|
||||
// Load kibana
|
||||
require('index');
|
||||
|
||||
describe('State Management', function () {
|
||||
describe('Events', function () {
|
||||
var $rootScope;
|
||||
var Events;
|
||||
describe('Events', function () {
|
||||
require('test_utils/no_digest_promises').activateForSuite();
|
||||
|
||||
beforeEach(function () {
|
||||
module('kibana');
|
||||
var $rootScope;
|
||||
var Events;
|
||||
var Notifier;
|
||||
var Promise;
|
||||
|
||||
inject(function (_$rootScope_, Private) {
|
||||
$rootScope = _$rootScope_;
|
||||
Events = Private(require('factories/events'));
|
||||
});
|
||||
beforeEach(function () {
|
||||
module('kibana');
|
||||
|
||||
inject(function ($injector, Private) {
|
||||
$rootScope = $injector.get('$rootScope');
|
||||
Notifier = $injector.get('Notifier');
|
||||
Promise = $injector.get('Promise');
|
||||
Events = Private(require('factories/events'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle on events', function (done) {
|
||||
var obj = new Events();
|
||||
obj.on('test', function (message) {
|
||||
expect(message).to.equal('Hello World');
|
||||
done();
|
||||
});
|
||||
obj.emit('test', 'Hello World');
|
||||
$rootScope.$apply();
|
||||
it('should handle on events', function (done) {
|
||||
var obj = new Events();
|
||||
obj.on('test', function (message) {
|
||||
expect(message).to.equal('Hello World');
|
||||
done();
|
||||
});
|
||||
obj.emit('test', 'Hello World');
|
||||
});
|
||||
|
||||
it('should work with inherited objects', function (done) {
|
||||
_.inherits(MyEventedObject, Events);
|
||||
function MyEventedObject() {
|
||||
MyEventedObject.Super.call(this);
|
||||
}
|
||||
var obj = new MyEventedObject();
|
||||
obj.on('test', function (message) {
|
||||
expect(message).to.equal('Hello World');
|
||||
done();
|
||||
});
|
||||
obj.emit('test', 'Hello World');
|
||||
$rootScope.$apply();
|
||||
it('should work with inherited objects', function (done) {
|
||||
_.inherits(MyEventedObject, Events);
|
||||
function MyEventedObject() {
|
||||
MyEventedObject.Super.call(this);
|
||||
}
|
||||
var obj = new MyEventedObject();
|
||||
obj.on('test', function (message) {
|
||||
expect(message).to.equal('Hello World');
|
||||
done();
|
||||
});
|
||||
obj.emit('test', 'Hello World');
|
||||
});
|
||||
|
||||
it('should clear events when off is called', function () {
|
||||
var obj = new Events();
|
||||
obj.on('test', _.noop);
|
||||
expect(obj._listeners).to.have.property('test');
|
||||
expect(obj._listeners['test']).to.have.length(1);
|
||||
obj.off();
|
||||
expect(obj._listeners).to.not.have.property('test');
|
||||
});
|
||||
it('should clear events when off is called', function () {
|
||||
var obj = new Events();
|
||||
obj.on('test', _.noop);
|
||||
expect(obj._listeners).to.have.property('test');
|
||||
expect(obj._listeners['test']).to.have.length(1);
|
||||
obj.off();
|
||||
expect(obj._listeners).to.not.have.property('test');
|
||||
});
|
||||
|
||||
it('should clear a specific handler when off is called for an event', function () {
|
||||
var obj = new Events();
|
||||
var handler1 = sinon.stub();
|
||||
var handler2 = sinon.stub();
|
||||
obj.on('test', handler1);
|
||||
obj.on('test', handler2);
|
||||
expect(obj._listeners).to.have.property('test');
|
||||
obj.off('test', handler1);
|
||||
obj.emit('test', 'Hello World');
|
||||
$rootScope.$apply();
|
||||
it('should clear a specific handler when off is called for an event', function (done) {
|
||||
var obj = new Events();
|
||||
var handler1 = sinon.stub();
|
||||
var handler2 = sinon.stub();
|
||||
obj.on('test', handler1);
|
||||
obj.on('test', handler2);
|
||||
expect(obj._listeners).to.have.property('test');
|
||||
obj.off('test', handler1);
|
||||
obj.emit('test', 'Hello World').then(function () {
|
||||
sinon.assert.calledOnce(handler2);
|
||||
sinon.assert.notCalled(handler1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should clear a all handlers when off is called for an event', function () {
|
||||
var obj = new Events();
|
||||
var handler1 = sinon.stub();
|
||||
obj.on('test', handler1);
|
||||
expect(obj._listeners).to.have.property('test');
|
||||
obj.off('test');
|
||||
expect(obj._listeners).to.not.have.property('test');
|
||||
obj.emit('test', 'Hello World');
|
||||
$rootScope.$apply();
|
||||
sinon.assert.notCalled(handler1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear a all handlers when off is called for an event', function (done) {
|
||||
var obj = new Events();
|
||||
var handler1 = sinon.stub();
|
||||
obj.on('test', handler1);
|
||||
expect(obj._listeners).to.have.property('test');
|
||||
obj.off('test');
|
||||
expect(obj._listeners).to.not.have.property('test');
|
||||
obj.emit('test', 'Hello World').then(function () {
|
||||
sinon.assert.notCalled(handler1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mulitple identical emits in the same tick', function (done) {
|
||||
var obj = new Events();
|
||||
var handler1 = sinon.stub();
|
||||
|
||||
obj.on('test', handler1);
|
||||
var emits = [
|
||||
obj.emit('test', 'one'),
|
||||
obj.emit('test', 'two'),
|
||||
obj.emit('test', 'three')
|
||||
];
|
||||
|
||||
Promise.all(emits).then(function () {
|
||||
expect(handler1.callCount).to.be(3);
|
||||
expect(handler1.getCall(0).calledWith('one')).to.be(true);
|
||||
expect(handler1.getCall(1).calledWith('two')).to.be(true);
|
||||
expect(handler1.getCall(2).calledWith('three')).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle emits from the handler', function (done) {
|
||||
var obj = new Events();
|
||||
var secondEmit = Promise.defer();
|
||||
var handler1 = sinon.spy(function () {
|
||||
if (handler1.calledTwice) {
|
||||
return;
|
||||
}
|
||||
obj.emit('test').then(_.bindKey(secondEmit, 'resolve'));
|
||||
});
|
||||
|
||||
obj.on('test', handler1);
|
||||
|
||||
Promise.all([
|
||||
obj.emit('test'),
|
||||
secondEmit.promise
|
||||
]).then(function () {
|
||||
expect(handler1.callCount).to.be(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should only emit to handlers registered before emit is called');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue