diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2016-11-22 02:46:19 +0000 |
---|---|---|
committer | Jacob Schatz <jschatz@gitlab.com> | 2016-11-22 02:46:19 +0000 |
commit | 56b420ae10aa91807b5be2b8e4c18d67313d27dc (patch) | |
tree | a81a8e8ca8d67121483386489112ccf93feff64e /app | |
parent | 2579cd7590e4da2fda5926e9ff2f923bc0c76452 (diff) | |
parent | 9c67b320a7adacc3a777bf7dc2fabd0b9a31caa8 (diff) | |
download | gitlab-ce-56b420ae10aa91807b5be2b8e4c18d67313d27dc.tar.gz |
Merge branch 'backport-tt' into 'master'
Backport SmartInterval, PrettyTime, SubbableResource from EE.
## What does this MR do?
Backports infrastructure used for EE-only Timetracking so it can be used and improved upon in CE.
This doesn't really need review... it was already reviewed and merged in EE.
There are no side effects or conflicts, just three new classes added:
1. `SubbableResource` -- pubsub for ajax resources
2. `SmartInterval`-- for configurable polling
3. `PrettyTime` -- time parsing and formatting utility methods
- [x] Added for this feature/bug
- [x] All builds are passing
- [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if it does - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
## What are the relevant issue numbers?
https://gitlab.com/gitlab-org/gitlab-ee/issues/985
https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/870
cc: @jschatz1
See merge request !7573
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/lib/utils/pretty_time.js.es6 | 67 | ||||
-rw-r--r-- | app/assets/javascripts/smart_interval.js.es6 | 130 | ||||
-rw-r--r-- | app/assets/javascripts/subbable_resource.js.es6 | 54 |
3 files changed, 251 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6 new file mode 100644 index 00000000000..ccaf447eb0b --- /dev/null +++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6 @@ -0,0 +1,67 @@ +(() => { + /* + * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints, + * stringifyTime condensed or non-condensed, abbreviateTimelengths) + * */ + + class PrettyTime { + + /* + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. + */ + static parseSeconds(seconds) { + const DAYS_PER_WEEK = 5; + const HOURS_PER_DAY = 8; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; + + let unorderedMinutes = PrettyTime.secondsToMinutes(seconds); + + return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + + unorderedMinutes -= (periodCount * minutesPerPeriod); + + return periodCount; + }); + } + + /* + * Accepts a timeObject and returns a condensed string representation of it + * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. + */ + + static stringifyTime(timeObject) { + const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, '').trim(); + return reducedTime.length ? reducedTime : '0m'; + } + + /* + * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns + * the first non-zero unit/value pair. + */ + + static abbreviateTime(timeStr) { + return timeStr.split(' ') + .filter(unitStr => unitStr.charAt(0) !== '0')[0]; + } + + static secondsToMinutes(seconds) { + return Math.abs(seconds / 60); + } + } + + gl.PrettyTime = PrettyTime; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 new file mode 100644 index 00000000000..5eb15dba79b --- /dev/null +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -0,0 +1,130 @@ +/* +* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable +* and controllable by a public API. +* +* */ + +(() => { + class SmartInterval { + /** + * @param { function } callback Function to be called on each iteration (required) + * @param { milliseconds } startingInterval `currentInterval` is set to this initially + * @param { milliseconds } maxInterval `currentInterval` will be incremented to this + * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily + */ + constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { + this.cfg = { + callback, + startingInterval, + maxInterval, + incrementByFactorOf, + lazyStart, + }; + + this.state = { + intervalId: null, + currentInterval: startingInterval, + pageVisibility: 'visible', + }; + + this.initInterval(); + } + /* public */ + + start() { + const cfg = this.cfg; + const state = this.state; + + state.intervalId = window.setInterval(() => { + cfg.callback(); + + if (this.getCurrentInterval() === cfg.maxInterval) { + return; + } + + this.incrementInterval(); + this.resume(); + }, this.getCurrentInterval()); + } + + // cancel the existing timer, setting the currentInterval back to startingInterval + cancel() { + this.setCurrentInterval(this.cfg.startingInterval); + this.stopTimer(); + } + + // start a timer, using the existing interval + resume() { + this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped + this.start(); + } + + destroy() { + this.cancel(); + $(document).off('visibilitychange').off('page:before-unload'); + } + + /* private */ + + initInterval() { + const cfg = this.cfg; + + if (!cfg.lazyStart) { + this.start(); + } + + this.initVisibilityChangeHandling(); + this.initPageUnloadHandling(); + } + + initVisibilityChangeHandling() { + // cancel interval when tab no longer shown (prevents cached pages from polling) + $(document) + .off('visibilitychange').on('visibilitychange', (e) => { + this.state.pageVisibility = e.target.visibilityState; + this.handleVisibilityChange(); + }); + } + + initPageUnloadHandling() { + // prevent interval continuing after page change, when kept in cache by Turbolinks + $(document).on('page:before-unload', () => this.cancel()); + } + + handleVisibilityChange() { + const state = this.state; + + const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; + + intervalAction.apply(this); + } + + getCurrentInterval() { + return this.state.currentInterval; + } + + setCurrentInterval(newInterval) { + this.state.currentInterval = newInterval; + } + + incrementInterval() { + const cfg = this.cfg; + const currentInterval = this.getCurrentInterval(); + let nextInterval = currentInterval * cfg.incrementByFactorOf; + + if (nextInterval > cfg.maxInterval) { + nextInterval = cfg.maxInterval; + } + + this.setCurrentInterval(nextInterval); + } + + stopTimer() { + const state = this.state; + + state.intervalId = window.clearInterval(state.intervalId); + } + } + gl.SmartInterval = SmartInterval; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 new file mode 100644 index 00000000000..932120157a3 --- /dev/null +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -0,0 +1,54 @@ +//= require vue +//= require vue-resource + +(() => { +/* +* SubbableResource can be extended to provide a pubsub-style service for one-off REST +* calls. Subscribe by passing a callback or render method you will use to handle responses. + * +* */ + + class SubbableResource { + constructor(resourcePath) { + this.endpoint = resourcePath; + + // TODO: Switch to axios.create + this.resource = $.ajax; + this.subscribers = []; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + publish(newResponse) { + const responseCopy = _.extend({}, newResponse); + this.subscribers.forEach((fn) => { + fn(responseCopy); + }); + return newResponse; + } + + get(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + post(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + put(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + delete(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + } + + gl.SubbableResource = SubbableResource; +})(window.gl || (window.gl = {})); |