summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js.es667
-rw-r--r--app/assets/javascripts/smart_interval.js.es6130
-rw-r--r--app/assets/javascripts/subbable_resource.js.es654
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 = {}));