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 @@ +
    +
    + + {{timeset.label}} + +
    +
    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;