diff --git a/src/plugins/kibana/public/visualize/editor/sidebar.html b/src/plugins/kibana/public/visualize/editor/sidebar.html
index d2de10a1d6c13..0587389a805b8 100644
--- a/src/plugins/kibana/public/visualize/editor/sidebar.html
+++ b/src/plugins/kibana/public/visualize/editor/sidebar.html
@@ -19,6 +19,9 @@
Options
+
+ Timefilter
+
@@ -63,7 +66,9 @@
+
+
+
-
diff --git a/src/plugins/kibana/public/visualize/editor/sidebar.js b/src/plugins/kibana/public/visualize/editor/sidebar.js
index b88b6537a85f8..671dc043d9c79 100644
--- a/src/plugins/kibana/public/visualize/editor/sidebar.js
+++ b/src/plugins/kibana/public/visualize/editor/sidebar.js
@@ -1,6 +1,7 @@
import _ from 'lodash';
import 'plugins/kibana/visualize/editor/agg_group';
import 'plugins/kibana/visualize/editor/vis_options';
+import 'plugins/vis_timefilter/vis_timefilter_params';
import uiModules from 'ui/modules';
import sidebarTemplate from 'plugins/kibana/visualize/editor/sidebar.html';
uiModules
diff --git a/src/plugins/vis_timefilter/index.js b/src/plugins/vis_timefilter/index.js
new file mode 100644
index 0000000000000..f0e949ec6d756
--- /dev/null
+++ b/src/plugins/vis_timefilter/index.js
@@ -0,0 +1,13 @@
+module.exports = function (kibana) {
+ let utils = require('requirefrom')('src/utils');
+ let fromRoot = utils('fromRoot');
+
+ return new kibana.Plugin({
+ uiExports: {
+ modules: {
+ VisTimefilter: fromRoot('src/plugins/vis_timefilter/vis_timefilter'),
+ }
+ }
+ });
+
+};
diff --git a/src/plugins/vis_timefilter/package.json b/src/plugins/vis_timefilter/package.json
new file mode 100644
index 0000000000000..a15e1df853ae3
--- /dev/null
+++ b/src/plugins/vis_timefilter/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "vis_timefilter",
+ "version": "1.0.0"
+}
diff --git a/src/plugins/vis_timefilter/public/vis_timefilter.less b/src/plugins/vis_timefilter/public/vis_timefilter.less
new file mode 100644
index 0000000000000..a43d82034c6d8
--- /dev/null
+++ b/src/plugins/vis_timefilter/public/vis_timefilter.less
@@ -0,0 +1,12 @@
+.vis-timefilter-selection {
+ margin: 0 5px 5px 5px;
+
+ a {
+ margin-right: 11px;
+ }
+}
+
+.editor-vis-timefilter-timeset {
+ padding: 5px;
+ border-bottom: 1px solid #ecf0f1;
+}
diff --git a/src/plugins/vis_timefilter/public/vis_timefilter_handler.js b/src/plugins/vis_timefilter/public/vis_timefilter_handler.js
new file mode 100644
index 0000000000000..385edac72f8dc
--- /dev/null
+++ b/src/plugins/vis_timefilter/public/vis_timefilter_handler.js
@@ -0,0 +1,259 @@
+import _ from 'lodash';
+import moment from 'moment';
+import dateMath from 'ui/utils/dateMath';
+
+export default function VisTimefilterHandlerFactory(timefilter, config) {
+
+ /**
+ * The VisTimefilterHandler encapsulates all functionality that is needed to
+ * support time sets in visualization and fetch stage.
+ *
+ * An instance of the Handler is assigned to a visualization and is
+ * referenced by all SearchSources that are derived from this visualization.
+ *
+ * @indexPattern the index pattern used by the visualization
+ * @params the params object of the visualization
+ *
+ */
+ function VisTimefilterHandler(indexPattern) {
+ this.indexPattern = indexPattern;
+ this.from = null;
+ this.to = null;
+ this.interval = null;
+ this.dateField = indexPattern.timeFieldName;
+ this.watchCounter = 0;
+ }
+
+ /**
+ * Sets the local time of this handler.
+ */
+ VisTimefilterHandler.prototype._setTime = function (from, to, interval) {
+ if (this.from !== from || this.to !== to || this.interval !== interval) {
+ this.from = this._toTicks(from);
+ this.to = this._toTicks(to);
+ this.interval = interval;
+ }
+ };
+
+ /**
+ * Clears the local time of this handler.
+ */
+ VisTimefilterHandler.prototype._clearTime = function () {
+ if (this.hasTime()) {
+ this.from = null;
+ this.to = null;
+ this.interval = null;
+ }
+ };
+
+ /**
+ * @return the time of this handler in form of a bounds array (min, max).
+ * The "time of the handler" is either a local time or the global
+ * timefilter time.
+ */
+ VisTimefilterHandler.prototype.getBounds = function () {
+ if (this.hasTime()) {
+ return {
+ min : moment(this.from),
+ max : moment(this.to)
+ };
+ } else {
+ return timefilter.getBounds();
+ }
+ };
+
+ /**
+ * @return the time of this handler in form of a bounds array (min, max).
+ * The "time of the handler" is either a local time or the global
+ * timefilter time. If no local time is set and the time filter is
+ * not enabled, this method returns undefined
+ */
+ VisTimefilterHandler.prototype.getActiveBounds = function () {
+ if (this.hasTime()) {
+ return {
+ min : moment(this.from),
+ max : moment(this.to)
+ };
+ } else {
+ return timefilter.getActiveBounds();
+ }
+ };
+
+ /**
+ * @return the time of this handler in form of a range.
+ *
+ * {range: {[TIMEFIELD_NAME]: {gte: [TICKS_FROM], lte: [TICKS_TO]}}
+ *
+ * The time of the handler is either a local time or the global timefilter
+ * time.
+ */
+ VisTimefilterHandler.prototype.getTimeRange = function () {
+ if (this.hasTime()) {
+ var obj = {
+ range : {}
+ };
+ obj.range[this.dateField] = {
+ 'gte' : this.from,
+ 'lte' : this.to
+ };
+ return obj;
+ } else {
+ return timefilter.get(this.indexPattern);
+ }
+
+ };
+
+ /**
+ * @return true, if a local time is set.
+ */
+ VisTimefilterHandler.prototype.hasTime = function () {
+ return this.from != null;
+ };
+
+ /**
+ * @return from formatted or empty string if from is not set
+ */
+ VisTimefilterHandler.prototype.getFromFormatted = function () {
+ return this.from != null ? moment(this.from).format(config.get('dateFormat')) : '';
+ };
+
+ /**
+ * @return to formatted or empty string if to is not set
+ */
+ VisTimefilterHandler.prototype.getToFormatted = function () {
+ return this.to != null ? moment(this.to).format(config.get('dateFormat')) : '';
+ };
+
+ /**
+ * Adds a new blank timeset to the list of local timesets available.
+ */
+ VisTimefilterHandler.prototype.addNewTimeset = function (visParams) {
+ if (!visParams.timeSets) {
+ visParams.timeSets = {
+ available : []
+ };
+ } else if (!visParams.timeSets.available) {
+ visParams.timeSets.available = [];
+ }
+
+ visParams.timeSets.available.push({
+ showInitially : false,
+ from : null,
+ to : null,
+ label : '',
+ interval : ''
+ });
+ };
+
+ /**
+ * Init with visualization params.
+ *
+ * Searches for a time set with "showInitially == true".
+ * If one is found, the vis time is set to the values from this set.
+ */
+ VisTimefilterHandler.prototype.initWithParams = function (visParams) {
+ var self = this;
+ var sets = visParams.timeSets;
+
+ if (sets && sets.available) {
+ sets.available.some(function (item) {
+ if (item.showInitially) {
+ self._setTime(item.from, item.to, item.interval);
+ sets.selected = item;
+ return true;
+ }
+ });
+
+ }
+
+ };
+
+ /**
+ * @return true, if buttons to select localtime shall be shown
+ */
+ VisTimefilterHandler.prototype.isShowVisTimefilterSelection = function (visParams) {
+ var self = this;
+ var sets = visParams.timeSets;
+
+ return sets && sets.showUi && sets.available && sets.available.length > 0;
+
+ };
+
+
+ VisTimefilterHandler.prototype.getAvailable = function (visParams) {
+ var sets = visParams.timeSets;
+
+ if (sets && sets.available) {
+ return sets.available;
+ } else {
+ return [];
+ }
+ };
+
+ VisTimefilterHandler.prototype.isSelected = function (timeset, visParams) {
+ var sets = visParams.timeSets;
+ if (sets && sets.selected) {
+ return _.isEqual(timeset, sets.selected);
+ } else {
+ return false;
+ }
+ };
+
+ VisTimefilterHandler.prototype.remove = function (ix, visParams) {
+ var avail = this.getAvailable(visParams);
+ var set = avail[ix];
+ if (set) {
+ if (this.isSelected(set, visParams)) {
+ this._clearTime();
+ var sets = visParams.timeSets;
+ sets.selected = null;
+ }
+ avail.splice(ix, 1);
+ }
+ };
+
+ VisTimefilterHandler.prototype.toggleSelection = function (timeset, visParams) {
+ var sets = visParams.timeSets;
+ if (sets) {
+ if (this.isSelected(timeset, visParams)) {
+ this._clearTime();
+ sets.selected = null;
+ } else {
+ this._setTime(timeset.from, timeset.to, timeset.interval);
+ sets.selected = timeset;
+ }
+
+ this.watchCounter++; // trigger watcher
+ }
+ };
+
+
+ /**
+ * Sets the local time of this handler.
+ */
+ VisTimefilterHandler.prototype._toTicks = function (text) {
+ if (!text) return undefined;
+
+ if (moment.isMoment(text)) {
+ return text.format();
+ }
+ if (_.isDate(text)) {
+ return text.format();
+ }
+
+ // parse ISO format
+ var m = moment(text);
+ if (m.isValid()) {
+ return m.format();
+ }
+
+ // parse dateMath
+ m = dateMath.parse(text);
+ if (m && m.isValid()) {
+ return m.format();
+ }
+
+ };
+
+ return VisTimefilterHandler;
+};
diff --git a/src/plugins/vis_timefilter/public/vis_timefilter_params.html b/src/plugins/vis_timefilter/public/vis_timefilter_params.html
new file mode 100644
index 0000000000000..0a4c9c9991f90
--- /dev/null
+++ b/src/plugins/vis_timefilter/public/vis_timefilter_params.html
@@ -0,0 +1,123 @@
+
diff --git a/src/plugins/vis_timefilter/public/vis_timefilter_params.js b/src/plugins/vis_timefilter/public/vis_timefilter_params.js
new file mode 100644
index 0000000000000..afc8cec39ee31
--- /dev/null
+++ b/src/plugins/vis_timefilter/public/vis_timefilter_params.js
@@ -0,0 +1,45 @@
+import uiModules from 'ui/modules';
+
+uiModules.get('visualize')
+.directive('visTimefilterParams',
+ function ($parse, $compile) {
+ return {
+ restrict: 'E',
+ template: require('plugins/vis_timefilter/vis_timefilter_params.html'),
+ scope: {
+ vis: '=',
+ },
+ link: function ($scope, $el) {
+ $scope.addTimeset = function () {
+ $scope.vis.vistime.addNewTimeset($scope.vis.params);
+ };
+
+ $scope.availableCount = function () {
+ return $scope.available().length;
+ };
+
+ $scope.available = function () {
+ return $scope.vis.vistime.getAvailable($scope.vis.params);
+ };
+
+ $scope.moveUp = function (ix) {
+ var av = $scope.available();
+ var tmp = av[ix];
+ av[ix] = av[ix - 1];
+ av[ix - 1] = tmp;
+ };
+
+ $scope.moveDown = function (ix) {
+ var av = $scope.available();
+ var tmp = av[ix];
+ av[ix] = av[ix + 1];
+ av[ix + 1] = tmp;
+ };
+
+ $scope.remove = function (ix) {
+ $scope.vis.vistime.remove(ix, $scope.vis.params);
+ };
+ }
+ };
+ }
+);
diff --git a/src/plugins/vis_timefilter/public/vis_timefilter_selection.html b/src/plugins/vis_timefilter/public/vis_timefilter_selection.html
new file mode 100644
index 0000000000000..3a222f6f84dce
--- /dev/null
+++ b/src/plugins/vis_timefilter/public/vis_timefilter_selection.html
@@ -0,0 +1,10 @@
+
diff --git a/src/plugins/vis_timefilter/public/vis_timefilter_selection.js b/src/plugins/vis_timefilter/public/vis_timefilter_selection.js
new file mode 100644
index 0000000000000..71b2f7263d9ee
--- /dev/null
+++ b/src/plugins/vis_timefilter/public/vis_timefilter_selection.js
@@ -0,0 +1,35 @@
+import uiModules from 'ui/modules';
+import 'plugins/vis_timefilter/vis_timefilter.less';
+
+uiModules.get('kibana')
+.directive('visTimefilterSelection',
+ function ($parse, $compile) {
+ return {
+ restrict: 'E',
+ template: require('plugins/vis_timefilter/vis_timefilter_selection.html'),
+ scope: {
+ vis: '=',
+ },
+ link: function ($scope, $el) {
+ var timefilterHandler = $scope.vis.vistime;
+
+ $scope.isShowVisTimefilterSelection = function () {
+ return timefilterHandler.isShowVisTimefilterSelection($scope.vis.params);
+ };
+
+ $scope.getAvailable = function () {
+ return timefilterHandler.getAvailable($scope.vis.params);
+ };
+
+ $scope.isSelected = function (timeset) {
+ return timefilterHandler.isSelected(timeset, $scope.vis.params);
+ };
+
+ $scope.toggle = function (timeset) {
+ timefilterHandler.toggleSelection(timeset, $scope.vis.params);
+ };
+
+ }
+ };
+ }
+);
diff --git a/src/ui/public/Vis/Vis.js b/src/ui/public/Vis/Vis.js
index c13fa144cbab3..3c66c4ef5f854 100644
--- a/src/ui/public/Vis/Vis.js
+++ b/src/ui/public/Vis/Vis.js
@@ -2,10 +2,12 @@ import _ from 'lodash';
import AggTypesIndexProvider from 'ui/agg_types/index';
import RegistryVisTypesProvider from 'ui/registry/vis_types';
import VisAggConfigsProvider from 'ui/Vis/AggConfigs';
+import VisTimefilterHandlerProvider from 'plugins/vis_timefilter/vis_timefilter_handler';
export default function VisFactory(Notifier, Private) {
var aggTypes = Private(AggTypesIndexProvider);
var visTypes = Private(RegistryVisTypesProvider);
var AggConfigs = Private(VisAggConfigsProvider);
+ var VisTimefilterHandler = Private(VisTimefilterHandlerProvider);
var notify = new Notifier({
location: 'Vis'
@@ -24,6 +26,9 @@ export default function VisFactory(Notifier, Private) {
// http://aphyr.com/data/posts/317/state.gif
this.setState(state);
+
+ this.vistime = new VisTimefilterHandler(this.indexPattern);
+ this.vistime.initWithParams(this.params);
}
Vis.convertOldState = function (type, oldState) {
@@ -78,6 +83,10 @@ export default function VisFactory(Notifier, Private) {
_.cloneDeep(this.type.params.defaults || {})
);
+ if (this.params.timeSets) {
+ this.params.timeSets = _.clone(this.params.timeSets, true);
+ }
+
this.aggs = new AggConfigs(this, state.aggs);
};
diff --git a/src/ui/public/agg_types/buckets/date_histogram.js b/src/ui/public/agg_types/buckets/date_histogram.js
index cd4c988a249d4..e94f18045f994 100644
--- a/src/ui/public/agg_types/buckets/date_histogram.js
+++ b/src/ui/public/agg_types/buckets/date_histogram.js
@@ -28,7 +28,7 @@ export default function DateHistogramAggType(timefilter, config, Private) {
function setBounds(agg, force) {
if (agg.buckets._alreadySet && !force) return;
agg.buckets._alreadySet = true;
- agg.buckets.setBounds(agg.fieldIsTimeField() && timefilter.getActiveBounds());
+ agg.buckets.setBounds(agg.fieldIsTimeField() && agg.vis.vistime.getActiveBounds());
}
diff --git a/src/ui/public/courier/data_source/_root_search_source.js b/src/ui/public/courier/data_source/_root_search_source.js
index 385cdea478472..d46a1bb90d7fc 100644
--- a/src/ui/public/courier/data_source/_root_search_source.js
+++ b/src/ui/public/courier/data_source/_root_search_source.js
@@ -9,7 +9,11 @@ export default function RootSearchSource(Private, $rootScope, timefilter, Notifi
globalSource.inherits(false); // this is the final source, it has no parents
globalSource.filter(function (globalSource) {
// dynamic time filter will be called in the _flatten phase of things
- return timefilter.get(globalSource.get('index'));
+ if (globalSource.vistime) {
+ return globalSource.vistime.getTimeRange();
+ } else {
+ return timefilter.get(globalSource.get('index'));
+ }
});
var appSource; // set in setAppSource()
diff --git a/src/ui/public/courier/fetch/request/request.js b/src/ui/public/courier/fetch/request/request.js
index 7d828e583adfd..1a3ae853faf8f 100644
--- a/src/ui/public/courier/fetch/request/request.js
+++ b/src/ui/public/courier/fetch/request/request.js
@@ -41,7 +41,16 @@ export default function AbstractReqProvider(Private, Promise) {
};
AbstractReq.prototype.getFetchParams = function () {
- return this.source._flatten();
+ var bounds = null;
+ if (typeof this.source.vistime === 'object') {
+ bounds = this.source.vistime.getBounds();
+ }
+ return this.source._flatten().then(function (fetchParams) {
+ if (bounds) {
+ fetchParams.bounds = bounds;
+ }
+ return fetchParams;
+ });
};
AbstractReq.prototype.transformResponse = function (resp) {
diff --git a/src/ui/public/courier/fetch/strategy/search.js b/src/ui/public/courier/fetch/strategy/search.js
index 3c2972e4ec359..19f70e093471c 100644
--- a/src/ui/public/courier/fetch/strategy/search.js
+++ b/src/ui/public/courier/fetch/strategy/search.js
@@ -21,6 +21,9 @@ export default function FetchStrategyForSearch(Private, Promise, timefilter) {
}
var timeBounds = timefilter.getBounds();
+ if (fetchParams.bounds) {
+ timeBounds = fetchParams.bounds;
+ }
return indexList.toIndexList(timeBounds.min, timeBounds.max);
})
.then(function (indexList) {
diff --git a/src/ui/public/visualize/visualize.html b/src/ui/public/visualize/visualize.html
index 5d8f7763033bb..be99da8697b52 100644
--- a/src/ui/public/visualize/visualize.html
+++ b/src/ui/public/visualize/visualize.html
@@ -14,4 +14,5 @@ No results found
+
diff --git a/src/ui/public/visualize/visualize.js b/src/ui/public/visualize/visualize.js
index 171c967810bde..39690444ec860 100644
--- a/src/ui/public/visualize/visualize.js
+++ b/src/ui/public/visualize/visualize.js
@@ -1,6 +1,7 @@
import 'ui/visualize/spy';
import 'ui/visualize/visualize.less';
import 'ui/visualize/visualize_legend';
+import 'plugins/vis_timefilter/vis_timefilter_selection';
import $ from 'jquery';
import _ from 'lodash';
import RegistryVisTypesProvider from 'ui/registry/vis_types';
@@ -115,6 +116,26 @@ uiModules
applyClassNames();
});
+ $scope.$watch('vis.vistime.watchCounter', function () {
+ // Pass reference of vis timehandler to searchSource.
+ // It would be probably enough to do this once when the
+ // searchSource is created but since I don't know where
+ // this happens we assign the same reference in every
+ // watch call.
+ // TODO assert that this does not trigger the watcher on
+ // 'searchSource'...
+ if ($scope.searchSource && $scope.vis) {
+ $scope.searchSource.vistime = $scope.vis.vistime;
+ }
+
+ if (typeof $scope.$parent.fetch === 'function') {
+ $scope.$parent.fetch();
+ } else if (typeof $scope.$parent.refresh === 'function') {
+ $scope.$parent.refresh();
+ }
+ });
+
+
$scope.$watch('vis', prereq(function (vis, oldVis) {
var $visEl = getVisEl();
if (!$visEl) return;