diff --git a/Gemfile.lock b/Gemfile.lock index 6f5dce38b..975954b24 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,6 @@ PATH active_model_serializers (~> 0.9.3) ansi_stream (~> 0.0.6) autoprefixer-rails (~> 6.4.1) - coffee-rails (~> 5.0) explicit-parameters (~> 0.4.0) faraday (~> 1.3) faraday-http-cache (~> 2.2) @@ -126,13 +125,6 @@ GEM coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (5.0.0) - coffee-script (>= 2.2.0) - railties (>= 5.2.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) concurrent-ruby (1.3.5) connection_pool (2.5.3) crack (1.0.0) @@ -176,13 +168,13 @@ GEM faraday-http-cache (2.5.0) faraday (>= 0.8) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday-retry (1.0.3) + faraday-retry (1.0.4) gemoji (2.1.0) globalid (1.2.1) activesupport (>= 6.1) @@ -215,7 +207,10 @@ GEM marcel (1.0.4) method_source (1.0.0) mini_mime (1.1.5) - minitest (5.25.5) + mini_portile2 (2.8.9) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) mocha (2.4.5) ruby2_keywords (>= 0.0.5) msgpack (1.7.1) @@ -232,6 +227,9 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) + nokogiri (1.18.9) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.18.9-arm64-darwin) racc (~> 1.4) nokogiri (1.18.9-x86_64-linux-gnu) @@ -263,7 +261,7 @@ GEM ast (~> 2.4.1) racc pg (1.3.3) - prism (1.4.0) + prism (1.9.0) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) @@ -373,6 +371,8 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) spy (1.0.2) + sqlite3 (2.6.0) + mini_portile2 (~> 2.8.0) sqlite3 (2.6.0-arm64-darwin) sqlite3 (2.6.0-x86_64-linux-gnu) state_machines (0.5.0) diff --git a/app/assets/javascripts/merge_status.coffee b/app/assets/javascripts/merge_status.coffee deleted file mode 100644 index da00694c8..000000000 --- a/app/assets/javascripts/merge_status.coffee +++ /dev/null @@ -1,83 +0,0 @@ -class MergeStatusPoller - POLL_INTERVAL = 3000 - - constructor: -> - @request = {abort: ->} - @previousLastModified = null - @timeoutId = null - - start: -> - @timeoutId = setTimeout(@refreshPage, POLL_INTERVAL) - @ - - stop: -> - @request.abort() - clearTimeout(@timeoutId) - @ - - onPageChange: => - window.parent.postMessage({event: 'hctw:height:change', height: document.body.clientHeight, service: 'shipit'}, '*') - window.parent.postMessage({event: 'hctw:stack:info', queue_enabled: @isMergeQueueEnabled(), status: @mergeStatus(), service: 'shipit'}, '*') - - fetchPage: (url, callback) -> - request = @request = new XMLHttpRequest() - request.onreadystatechange = -> - if request.readyState == XMLHttpRequest.DONE - callback(request.status == 200 && request.responseText, request) - request.open('GET', url, true) - request.setRequestHeader('X-Requested-With', 'XMLHttpRequest') - request.send() - - previousLastModified = null - refreshPage: => - @fetchPage window.location.toString(), (html, response) => - @updateDocument(html, response) - setTimeout(@refreshPage, POLL_INTERVAL) - - updateDocument: (html, response) => - lastModified = response.getResponseHeader('last-modified') - if !lastModified || lastModified != @previousLastModified - @previousLastModified = lastModified - if html && container = document.querySelector('[data-layout-content]') - container.innerHTML = html - @onPageChange() - - isMergeQueueEnabled: => - document.querySelector('.merge-status-container .js-details-container')?.hasAttribute('data-queue-enabled') - - mergeStatus: => - document.querySelector('.merge-status-container .js-details-container')?.getAttribute('data-merge-status') || 'unknown' - -class AjaxAction - constructor: (@poller) -> - document.addEventListener('submit', @submit, false) - - submit: (event) => - return unless event.target.getAttribute('data-remote') == 'true' - - event.preventDefault() - - @poller.stop() - @disableButtons(event.target) - @submitFormAsynchronously event.target, (html, request) => - @poller.updateDocument(html, request) - @poller.start() - - submitFormAsynchronously: (form, callback) -> - request = new XMLHttpRequest() - request.onreadystatechange = -> - if request.readyState == XMLHttpRequest.DONE - callback(request.status == 200 && request.responseText, request) - request.open(form.method.toLocaleUpperCase(), form.action, true) - request.setRequestHeader('X-Requested-With', 'XMLHttpRequest') - request.send(new FormData(form)) - - disableButtons: (form) -> - for button in form.querySelectorAll('[data-disable-with]') - button.disabled = true - button.textContent = button.getAttribute('data-disable-with') - -poller = new MergeStatusPoller -poller.onPageChange() -poller.start() -new AjaxAction(poller) diff --git a/app/assets/javascripts/merge_status.js b/app/assets/javascripts/merge_status.js new file mode 100644 index 000000000..0a2b844e7 --- /dev/null +++ b/app/assets/javascripts/merge_status.js @@ -0,0 +1,155 @@ +// Generated by CoffeeScript 1.12.7 +var AjaxAction, MergeStatusPoller, poller, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +MergeStatusPoller = (function() { + var POLL_INTERVAL, previousLastModified; + + POLL_INTERVAL = 3000; + + function MergeStatusPoller() { + this.mergeStatus = bind(this.mergeStatus, this); + this.isMergeQueueEnabled = bind(this.isMergeQueueEnabled, this); + this.updateDocument = bind(this.updateDocument, this); + this.refreshPage = bind(this.refreshPage, this); + this.onPageChange = bind(this.onPageChange, this); + this.request = { + abort: function() {} + }; + this.previousLastModified = null; + this.timeoutId = null; + } + + MergeStatusPoller.prototype.start = function() { + this.timeoutId = setTimeout(this.refreshPage, POLL_INTERVAL); + return this; + }; + + MergeStatusPoller.prototype.stop = function() { + this.request.abort(); + clearTimeout(this.timeoutId); + return this; + }; + + MergeStatusPoller.prototype.onPageChange = function() { + window.parent.postMessage({ + event: 'hctw:height:change', + height: document.body.clientHeight, + service: 'shipit' + }, '*'); + return window.parent.postMessage({ + event: 'hctw:stack:info', + queue_enabled: this.isMergeQueueEnabled(), + status: this.mergeStatus(), + service: 'shipit' + }, '*'); + }; + + MergeStatusPoller.prototype.fetchPage = function(url, callback) { + var request; + request = this.request = new XMLHttpRequest(); + request.onreadystatechange = function() { + if (request.readyState === XMLHttpRequest.DONE) { + return callback(request.status === 200 && request.responseText, request); + } + }; + request.open('GET', url, true); + request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + return request.send(); + }; + + previousLastModified = null; + + MergeStatusPoller.prototype.refreshPage = function() { + return this.fetchPage(window.location.toString(), (function(_this) { + return function(html, response) { + _this.updateDocument(html, response); + return setTimeout(_this.refreshPage, POLL_INTERVAL); + }; + })(this)); + }; + + MergeStatusPoller.prototype.updateDocument = function(html, response) { + var container, lastModified; + lastModified = response.getResponseHeader('last-modified'); + if (!lastModified || lastModified !== this.previousLastModified) { + this.previousLastModified = lastModified; + if (html && (container = document.querySelector('[data-layout-content]'))) { + container.innerHTML = html; + return this.onPageChange(); + } + } + }; + + MergeStatusPoller.prototype.isMergeQueueEnabled = function() { + var ref; + return (ref = document.querySelector('.merge-status-container .js-details-container')) != null ? ref.hasAttribute('data-queue-enabled') : void 0; + }; + + MergeStatusPoller.prototype.mergeStatus = function() { + var ref; + return ((ref = document.querySelector('.merge-status-container .js-details-container')) != null ? ref.getAttribute('data-merge-status') : void 0) || 'unknown'; + }; + + return MergeStatusPoller; + +})(); + +AjaxAction = (function() { + function AjaxAction(poller1) { + this.poller = poller1; + this.submit = bind(this.submit, this); + document.addEventListener('submit', this.submit, false); + } + + AjaxAction.prototype.submit = function(event) { + if (event.target.getAttribute('data-remote') !== 'true') { + return; + } + event.preventDefault(); + this.poller.stop(); + this.disableButtons(event.target); + return this.submitFormAsynchronously(event.target, (function(_this) { + return function(html, request) { + _this.poller.updateDocument(html, request); + return _this.poller.start(); + }; + })(this)); + }; + + AjaxAction.prototype.submitFormAsynchronously = function(form, callback) { + var request; + request = new XMLHttpRequest(); + request.onreadystatechange = function() { + if (request.readyState === XMLHttpRequest.DONE) { + return callback(request.status === 200 && request.responseText, request); + } + }; + request.open(form.method.toLocaleUpperCase(), form.action, true); + request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + return request.send(new FormData(form)); + }; + + AjaxAction.prototype.disableButtons = function(form) { + var button, i, len, ref, results; + ref = form.querySelectorAll('[data-disable-with]'); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + button = ref[i]; + button.disabled = true; + results.push(button.textContent = button.getAttribute('data-disable-with')); + } + return results; + }; + + return AjaxAction; + +})(); + +poller = new MergeStatusPoller; + +poller.onPageChange(); + +poller.start(); + +new AjaxAction(poller); diff --git a/app/assets/javascripts/shipit.js b/app/assets/javascripts/shipit.js new file mode 100644 index 000000000..bf701e806 --- /dev/null +++ b/app/assets/javascripts/shipit.js @@ -0,0 +1,52 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. +// +// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require rails-timeago +//= require jquery-notify +//= require_tree ./shipit +//= require_self + +$(document).on('click', '.disabled, .btn--disabled', function(event) { + event.preventDefault(); +}); + +$(document).on('click', '.banner__dismiss', function(event) { + $(event.target).closest('.banner').addClass('hidden'); +}); + +$(document).on('click', '.enable-notifications .banner__dismiss', function(event) { + localStorage.setItem("dismissed-enable-notifications", true); +}); + +$(document).on('click', '.github-status .banner__dismiss', function(event) { + localStorage.setItem("dismissed-github-status", true); +}); + +jQuery(function() { + var $button, $notificationNotice; + if (!(localStorage.getItem("dismissed-enable-notifications"))) { + $notificationNotice = $('.enable-notifications'); + if ($.notifyCheck() === $.NOTIFY_NOT_ALLOWED) { + $button = $notificationNotice.find('button'); + $button.on('click', function() { + $.notifyRequest(); + $notificationNotice.addClass('hidden'); + }); + $notificationNotice.removeClass('hidden'); + } + } + if (!(localStorage.getItem("dismissed-github-status"))) { + $('.github-status').removeClass('hidden'); + } +}); diff --git a/app/assets/javascripts/shipit.js.coffee b/app/assets/javascripts/shipit.js.coffee deleted file mode 100644 index 0caa0aab1..000000000 --- a/app/assets/javascripts/shipit.js.coffee +++ /dev/null @@ -1,45 +0,0 @@ -# This is a manifest file that'll be compiled into application.js, which will include all the files -# listed below. -# -# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -# -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# compiled file. -# -# Read Sprockets README (https:#github.com/sstephenson/sprockets#sprockets-directives) for details -# about supported directives. -# -#= require jquery -#= require jquery_ujs -#= require rails-timeago -#= require jquery-notify -#= require_tree ./shipit -#= require_self - -$(document).on 'click', '.disabled, .btn--disabled', (event) -> - event.preventDefault() - -$(document).on 'click', '.banner__dismiss', (event) -> - $(event.target).closest('.banner').addClass('hidden') - -$(document).on 'click', '.enable-notifications .banner__dismiss', (event) -> - localStorage.setItem("dismissed-enable-notifications", true) - -$(document).on 'click', '.github-status .banner__dismiss', (event) -> - localStorage.setItem("dismissed-github-status", true) - -jQuery -> - unless(localStorage.getItem("dismissed-enable-notifications")) - $notificationNotice = $('.enable-notifications') - - if $.notifyCheck() == $.NOTIFY_NOT_ALLOWED - $button = $notificationNotice.find('button') - $button.on 'click', -> - $.notifyRequest() - $notificationNotice.addClass('hidden') - $notificationNotice.removeClass('hidden') - - unless(localStorage.getItem("dismissed-github-status")) - $('.github-status').removeClass('hidden') - diff --git a/app/assets/javascripts/shipit/checklist.js b/app/assets/javascripts/shipit/checklist.js new file mode 100644 index 000000000..f782990bc --- /dev/null +++ b/app/assets/javascripts/shipit/checklist.js @@ -0,0 +1,13 @@ +var $document, toggleDeployButton; + +$document = $(document); + +toggleDeployButton = function() { + $('.trigger-deploy').toggleClass('disabled btn--disabled', !!$(':checkbox.required:not(:checked)').length); +}; + +$document.on('change', ':checkbox.required', toggleDeployButton); + +jQuery(function($) { + toggleDeployButton(); +}); diff --git a/app/assets/javascripts/shipit/checklist.js.coffee b/app/assets/javascripts/shipit/checklist.js.coffee deleted file mode 100644 index db7394f36..000000000 --- a/app/assets/javascripts/shipit/checklist.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -$document = $(document) - -toggleDeployButton = -> - $('.trigger-deploy').toggleClass('disabled btn--disabled', !!$(':checkbox.required:not(:checked)').length) - -$document.on('change', ':checkbox.required', toggleDeployButton) - -jQuery ($) -> - toggleDeployButton() diff --git a/app/assets/javascripts/shipit/continuous_delivery_schedule.js b/app/assets/javascripts/shipit/continuous_delivery_schedule.js new file mode 100644 index 000000000..c54dade24 --- /dev/null +++ b/app/assets/javascripts/shipit/continuous_delivery_schedule.js @@ -0,0 +1,17 @@ +$(document).on("click", ".continuous-delivery-schedule [data-action='copy-to-all']", function(event) { + var form, mondayEnd, mondayStart; + form = event.target.closest("form"); + mondayStart = form.elements.namedItem("continuous_delivery_schedule[monday_start]").value; + mondayEnd = form.elements.namedItem("continuous_delivery_schedule[monday_end]").value; + Array.from(form.elements).forEach(function(formElement) { + if (formElement.type !== "time") { + return; + } + if (formElement.name.endsWith("_start]")) { + formElement.value = mondayStart; + } + if (formElement.name.endsWith("_end]")) { + formElement.value = mondayEnd; + } + }); +}); diff --git a/app/assets/javascripts/shipit/continuous_delivery_schedule.js.coffee b/app/assets/javascripts/shipit/continuous_delivery_schedule.js.coffee deleted file mode 100644 index b8750ffb0..000000000 --- a/app/assets/javascripts/shipit/continuous_delivery_schedule.js.coffee +++ /dev/null @@ -1,15 +0,0 @@ -$(document) - .on "click", ".continuous-delivery-schedule [data-action='copy-to-all']", (event) -> - form = event.target.closest("form"); - - mondayStart = form.elements.namedItem("continuous_delivery_schedule[monday_start]").value - mondayEnd = form.elements.namedItem("continuous_delivery_schedule[monday_end]").value - - Array.from(form.elements).forEach (formElement) -> - return unless formElement.type == "time" - - if formElement.name.endsWith("_start]") - formElement.value = mondayStart - - if formElement.name.endsWith("_end]") - formElement.value = mondayEnd diff --git a/app/assets/javascripts/shipit/deploy.js b/app/assets/javascripts/shipit/deploy.js new file mode 100644 index 000000000..0e3ceff37 --- /dev/null +++ b/app/assets/javascripts/shipit/deploy.js @@ -0,0 +1,59 @@ +var AbortButton, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +AbortButton = (function() { + var SELECTOR; + + SELECTOR = '[data-action="abort"]'; + + AbortButton.listen = function() { + return $(document).on('click', SELECTOR, this.handle); + }; + + AbortButton.handle = function(event) { + var button; + event.preventDefault(); + button = new AbortButton($(event.currentTarget)); + button.trigger(); + }; + + function AbortButton($button) { + this.$button = $button; + this.reenable = bind(this.reenable, this); + this.waitForCompletion = bind(this.waitForCompletion, this); + this.url = this.$button.attr('href'); + this.shouldRollback = this.$button.data('rollback'); + } + + AbortButton.prototype.trigger = function() { + if (this.isDisabled()) { + return false; + } + this.disable(); + this.waitForCompletion(); + $.post(this.url).success(this.waitForCompletion).error(this.reenable); + }; + + AbortButton.prototype.waitForCompletion = function() { + setTimeout(this.reenable, 3000); + }; + + AbortButton.prototype.reenable = function() { + this.$button.removeClass('pending btn-disabled'); + this.$button.siblings(SELECTOR).removeClass('btn-disabled'); + }; + + AbortButton.prototype.disable = function() { + this.$button.addClass('pending btn-disabled'); + this.$button.siblings(SELECTOR).addClass('btn-disabled'); + }; + + AbortButton.prototype.isDisabled = function() { + return this.$button.hasClass('btn-disabled'); + }; + + return AbortButton; + +})(); + +AbortButton.listen(); diff --git a/app/assets/javascripts/shipit/deploy.js.coffee b/app/assets/javascripts/shipit/deploy.js.coffee deleted file mode 100644 index b7317d04a..000000000 --- a/app/assets/javascripts/shipit/deploy.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -class AbortButton - SELECTOR = '[data-action="abort"]' - - @listen: -> - $(document).on('click', SELECTOR, @handle) - - @handle: (event) => - event.preventDefault() - button = new this($(event.currentTarget)) - button.trigger() - - constructor: (@$button) -> - @url = @$button.attr('href') - @shouldRollback = @$button.data('rollback') - - trigger: -> - return false if @isDisabled() - - @disable() - @waitForCompletion() - $.post(@url).success(@waitForCompletion).error(@reenable) - - waitForCompletion: => - setTimeout(@reenable, 3000) - - reenable: => - @$button.removeClass('pending btn-disabled') - @$button.siblings(SELECTOR).removeClass('btn-disabled') - - disable: -> - @$button.addClass('pending btn-disabled') - @$button.siblings(SELECTOR).addClass('btn-disabled') - - isDisabled: -> - @$button.hasClass('btn-disabled') - -AbortButton.listen() diff --git a/app/assets/javascripts/shipit/flash.js b/app/assets/javascripts/shipit/flash.js new file mode 100644 index 000000000..66c6e4e6a --- /dev/null +++ b/app/assets/javascripts/shipit/flash.js @@ -0,0 +1,5 @@ +jQuery(function($) { + setTimeout((function() { + $('.flash-success').remove(); + }), 3000); +}); diff --git a/app/assets/javascripts/shipit/flash.js.coffee b/app/assets/javascripts/shipit/flash.js.coffee deleted file mode 100644 index 6751bee97..000000000 --- a/app/assets/javascripts/shipit/flash.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -jQuery ($) -> - setTimeout((-> $('.flash-success').remove()), 3000) diff --git a/app/assets/javascripts/shipit/page_updater.js b/app/assets/javascripts/shipit/page_updater.js new file mode 100644 index 000000000..9a138700e --- /dev/null +++ b/app/assets/javascripts/shipit/page_updater.js @@ -0,0 +1,120 @@ +var PageUpdater, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +PageUpdater = (function() { + var DEBOUNCE, MAX_RETRIES, RETRY_DELAY; + + DEBOUNCE = 100; + + RETRY_DELAY = 5000; + + MAX_RETRIES = 5; + + PageUpdater.callbacks = []; + + PageUpdater.afterUpdate = function(callback) { + this.callbacks.push(callback); + }; + + function PageUpdater(channel, selectors) { + this.channel = channel; + this.selectors = selectors; + this.updatePage = bind(this.updatePage, this); + this.fetchPage = bind(this.fetchPage, this); + this.scheduleUpdate = bind(this.scheduleUpdate, this); + this.requestUpdate = bind(this.requestUpdate, this); + this.parser = new DOMParser(); + this.source = this.listen(); + this.previousLastModified = null; + } + + PageUpdater.prototype.requestUpdate = function() { + this.updateRequested = true; + this.scheduleUpdate(); + }; + + PageUpdater.prototype.scheduleUpdate = function() { + if (this.updateScheduled) { + return; + } + if (!this.updateRequested) { + return; + } + setTimeout(this.fetchPage, DEBOUNCE); + this.updateScheduled = true; + }; + + PageUpdater.prototype.fetchPage = function(message) { + this.updateRequested = false; + jQuery.get(window.location.toString()).done(this.updatePage).fail((function(_this) { + return function() { + _this.updateScheduled = false; + }; + })(this)); + }; + + PageUpdater.prototype.updatePage = function(html, status, response) { + var callback, i, j, lastModified, len, len1, newDocument, ref, ref1, selector; + lastModified = response.getResponseHeader('last-modified'); + if ((lastModified != null) && lastModified !== this.previousLastModified) { + this.previousLastModified = lastModified; + newDocument = this.parser.parseFromString(html, 'text/html'); + ref = this.selectors; + for (i = 0, len = ref.length; i < len; i++) { + selector = ref[i]; + $(selector).html(newDocument.querySelectorAll(selector + " > *")); + } + ref1 = PageUpdater.callbacks; + for (j = 0, len1 = ref1.length; j < len1; j++) { + callback = ref1[j]; + callback(); + } + } + this.updateScheduled = false; + }; + + PageUpdater.prototype.listen = function() { + this.source = new EventSource(this.channel); + this.source.addEventListener('update', this.requestUpdate); + this.retries = MAX_RETRIES; + this.interval = setInterval((function(_this) { + return function() { + switch (_this.source.readyState) { + case _this.source.CLOSED: + clearInterval(_this.interval); + if (_this.retries > 0) { + _this.retries -= 1; + _this.listen(); + } + break; + default: + _this.retries = MAX_RETRIES; + } + }; + })(this), RETRY_DELAY); + }; + + return PageUpdater; + +})(); + +jQuery(function($) { + var channel, e, selectors; + PageUpdater.afterUpdate(function() { + $('time[data-time-ago]').timeago(); + }); + channel = $('meta[name=subscription-channel]').attr('content'); + selectors = (function() { + var i, len, ref, results; + ref = $('meta[name=subscription-selector]'); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + e = ref[i]; + results.push(e.content); + } + return results; + })(); + if (channel && selectors) { + new PageUpdater(channel, selectors); + } +}); diff --git a/app/assets/javascripts/shipit/page_updater.js.coffee b/app/assets/javascripts/shipit/page_updater.js.coffee deleted file mode 100644 index 26f60635c..000000000 --- a/app/assets/javascripts/shipit/page_updater.js.coffee +++ /dev/null @@ -1,63 +0,0 @@ -class PageUpdater - DEBOUNCE = 100 - RETRY_DELAY = 5000 - MAX_RETRIES = 5 - - @callbacks: [] - @afterUpdate: (callback) -> - @callbacks.push(callback) - - constructor: (@channel, @selectors) -> - @parser = new DOMParser() - @source = @listen() - @previousLastModified = null - - requestUpdate: => - @updateRequested = true - @scheduleUpdate() - - scheduleUpdate: => - return if @updateScheduled - return unless @updateRequested - setTimeout(@fetchPage, DEBOUNCE) - @updateScheduled = true - - fetchPage: (message) => - @updateRequested = false - jQuery.get(window.location.toString()).done(@updatePage).fail(=> @updateScheduled = false) - - updatePage: (html, status, response) => - lastModified = response.getResponseHeader('last-modified') - if lastModified? and lastModified != @previousLastModified - @previousLastModified = lastModified - - newDocument = @parser.parseFromString(html, 'text/html') - for selector in @selectors - $(selector).html(newDocument.querySelectorAll("#{selector} > *")) - for callback in PageUpdater.callbacks - callback() - - @updateScheduled = false - - listen: -> - @source = new EventSource(@channel) - @source.addEventListener('update', @requestUpdate) - @retries = MAX_RETRIES - @interval = setInterval => - switch @source.readyState - when @source.CLOSED - clearInterval(@interval) - if @retries > 0 - @retries -= 1 - @listen() - else - @retries = MAX_RETRIES - , RETRY_DELAY - -jQuery ($) -> - PageUpdater.afterUpdate -> $('time[data-time-ago]').timeago() - - channel = $('meta[name=subscription-channel]').attr('content') - selectors = (e.content for e in $('meta[name=subscription-selector]')) - if channel and selectors - new PageUpdater(channel, selectors) diff --git a/app/assets/javascripts/shipit/repositories_search.js b/app/assets/javascripts/shipit/repositories_search.js new file mode 100644 index 000000000..689fcd967 --- /dev/null +++ b/app/assets/javascripts/shipit/repositories_search.js @@ -0,0 +1,96 @@ +var KEY, RepositorySearch, search, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +KEY = { + UP: 38, + DOWN: 40, + ENTER: 13 +}; + +RepositorySearch = (function() { + function RepositorySearch(root) { + this.onKeyUp = bind(this.onKeyUp, this); + this.$root = $(root); + this.$root.on('keyup', '.repository-search', this.onKeyUp); + this.$root.on('click', '.show-all-repositories', (function(_this) { + return function(event) { + _this.$root.find('.not-matching').removeClass('not-matching'); + event.preventDefault(); + }; + })(this)); + } + + RepositorySearch.prototype.onKeyUp = function(event) { + this.$items = this.$root.find('[data-search]'); + switch (event.keyCode) { + case KEY.ENTER: + event.preventDefault(); + this.goToSelectedRepository(); + break; + case KEY.UP: + event.preventDefault(); + this.selectPrevious(); + break; + case KEY.DOWN: + event.preventDefault(); + this.selectNext(); + break; + default: + this.filterResults($.trim($(event.target).val()).toLowerCase()); + } + }; + + RepositorySearch.prototype.filterResults = function(query) { + var $item, i, item, len, ref; + if (query) { + ref = this.$items; + for (i = 0, len = ref.length; i < len; i++) { + item = ref[i]; + $item = $(item); + $item.toggleClass('not-matching', indexOf.call($item.attr('data-search').toLowerCase(), query) < 0); + } + this.selectFirst(); + } else { + this.$items.removeClass('not-matching'); + } + }; + + RepositorySearch.prototype.selectFirst = function() { + this.$items.removeClass('selected').first(':not(.not-matching)').addClass('selected'); + }; + + RepositorySearch.prototype.selectNext = function() { + var $next; + $next = this.$items.filter('.selected').removeClass('selected').nextAll(':not(.not-matching)').first(); + if (!$next.length) { + $next = this.$items.filter(':not(.not-matching)').first(); + } + $next.addClass('selected'); + }; + + RepositorySearch.prototype.selectPrevious = function() { + var $previous; + $previous = this.$items.filter('.selected').removeClass('selected').prevAll(':not(.not-matching)').first(); + if (!$previous.length) { + $previous = this.$items.filter(':not(.not-matching)').last(); + } + $previous.addClass('selected'); + }; + + RepositorySearch.prototype.goToSelectedRepository = function() { + var repository; + if (repository = this.$items.filter('.selected').filter(':not(.not-matching)').find('.commits-path').attr('href')) { + window.location = repository; + } + }; + + return RepositorySearch; + +})(); + +search = new RepositorySearch(document); + +jQuery(function() { + $('.repository-search').focus(); +}); diff --git a/app/assets/javascripts/shipit/repositories_search.js.coffee b/app/assets/javascripts/shipit/repositories_search.js.coffee deleted file mode 100644 index c16cdb12d..000000000 --- a/app/assets/javascripts/shipit/repositories_search.js.coffee +++ /dev/null @@ -1,60 +0,0 @@ -KEY = - UP: 38 - DOWN: 40 - ENTER: 13 - -class RepositorySearch - - constructor: (root) -> - @$root = $(root) - @$root.on('keyup', '.repository-search', @onKeyUp) - @$root.on('click', '.show-all-repositories', (event) => - @$root.find('.not-matching').removeClass('not-matching') - event.preventDefault() - ) - - onKeyUp: (event) => - @$items = @$root.find('[data-search]') - switch event.keyCode - when KEY.ENTER - event.preventDefault() - @goToSelectedRepository() - when KEY.UP - event.preventDefault() - @selectPrevious() - when KEY.DOWN - event.preventDefault() - @selectNext() - else - @filterResults($.trim($(event.target).val()).toLowerCase()) - - filterResults: (query) -> - if query - for item in @$items - $item = $(item) - $item.toggleClass('not-matching', query not in $item.attr('data-search').toLowerCase()) - @selectFirst() - else - @$items.removeClass('not-matching') - - selectFirst: -> - @$items.removeClass('selected').first(':not(.not-matching)').addClass('selected') - - selectNext: -> - $next = @$items.filter('.selected').removeClass('selected').nextAll(':not(.not-matching)').first() - $next = @$items.filter(':not(.not-matching)').first() unless $next.length - $next.addClass('selected') - - selectPrevious: -> - $previous = @$items.filter('.selected').removeClass('selected').prevAll(':not(.not-matching)').first() - $previous = @$items.filter(':not(.not-matching)').last() unless $previous.length - $previous.addClass('selected') - - goToSelectedRepository: -> - if repository = @$items.filter('.selected').filter(':not(.not-matching)').find('.commits-path').attr('href') - window.location = repository - -search = new RepositorySearch(document) - -jQuery -> - $('.repository-search').focus() diff --git a/app/assets/javascripts/shipit/stack_search.js b/app/assets/javascripts/shipit/stack_search.js new file mode 100644 index 000000000..8a9c62af6 --- /dev/null +++ b/app/assets/javascripts/shipit/stack_search.js @@ -0,0 +1,104 @@ +var KEY, StackSearch, search, + slice = [].slice, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +if (!String.prototype.contains) { + String.prototype.contains = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return this.indexOf.apply(this, args) !== -1; + }; +} + +KEY = { + UP: 38, + DOWN: 40, + ENTER: 13 +}; + +StackSearch = (function() { + function StackSearch(root) { + this.onKeyUp = bind(this.onKeyUp, this); + this.$root = $(root); + this.$root.on('keyup', '.stack-search', this.onKeyUp); + this.$root.on('click', '.show-all-stacks', (function(_this) { + return function(event) { + _this.$root.find('.not-matching').removeClass('not-matching'); + event.preventDefault(); + }; + })(this)); + } + + StackSearch.prototype.onKeyUp = function(event) { + this.$items = this.$root.find('[data-search]'); + switch (event.keyCode) { + case KEY.ENTER: + event.preventDefault(); + this.goToSelectedStack(); + break; + case KEY.UP: + event.preventDefault(); + this.selectPrevious(); + break; + case KEY.DOWN: + event.preventDefault(); + this.selectNext(); + break; + default: + this.filterResults($.trim($(event.target).val()).toLowerCase()); + } + }; + + StackSearch.prototype.filterResults = function(query) { + var $item, i, item, len, ref; + if (query) { + ref = this.$items; + for (i = 0, len = ref.length; i < len; i++) { + item = ref[i]; + $item = $(item); + $item.toggleClass('not-matching', !$item.attr('data-search').toLowerCase().contains(query)); + } + this.selectFirst(); + } else { + this.$items.removeClass('not-matching'); + } + }; + + StackSearch.prototype.selectFirst = function() { + this.$items.removeClass('selected').first(':not(.not-matching)').addClass('selected'); + }; + + StackSearch.prototype.selectNext = function() { + var $next; + $next = this.$items.filter('.selected').removeClass('selected').nextAll(':not(.not-matching)').first(); + if (!$next.length) { + $next = this.$items.filter(':not(.not-matching)').first(); + } + $next.addClass('selected'); + }; + + StackSearch.prototype.selectPrevious = function() { + var $previous; + $previous = this.$items.filter('.selected').removeClass('selected').prevAll(':not(.not-matching)').first(); + if (!$previous.length) { + $previous = this.$items.filter(':not(.not-matching)').last(); + } + $previous.addClass('selected'); + }; + + StackSearch.prototype.goToSelectedStack = function() { + var stack; + if (stack = this.$items.filter('.selected').filter(':not(.not-matching)').find('.commits-path').attr('href')) { + window.location = stack; + } + }; + + return StackSearch; + +})(); + +search = new StackSearch(document); + +jQuery(function() { + $('.stack-search').focus(); +}); diff --git a/app/assets/javascripts/shipit/stack_search.js.coffee b/app/assets/javascripts/shipit/stack_search.js.coffee deleted file mode 100644 index 2baf24409..000000000 --- a/app/assets/javascripts/shipit/stack_search.js.coffee +++ /dev/null @@ -1,64 +0,0 @@ -unless String::contains - String::contains = (args...) -> - @indexOf(args...) != -1 - -KEY = - UP: 38 - DOWN: 40 - ENTER: 13 - -class StackSearch - - constructor: (root) -> - @$root = $(root) - @$root.on('keyup', '.stack-search', @onKeyUp) - @$root.on('click', '.show-all-stacks', (event) => - @$root.find('.not-matching').removeClass('not-matching') - event.preventDefault() - ) - - onKeyUp: (event) => - @$items = @$root.find('[data-search]') - switch event.keyCode - when KEY.ENTER - event.preventDefault() - @goToSelectedStack() - when KEY.UP - event.preventDefault() - @selectPrevious() - when KEY.DOWN - event.preventDefault() - @selectNext() - else - @filterResults($.trim($(event.target).val()).toLowerCase()) - - filterResults: (query) -> - if query - for item in @$items - $item = $(item) - $item.toggleClass('not-matching', !$item.attr('data-search').toLowerCase().contains(query)) - @selectFirst() - else - @$items.removeClass('not-matching') - - selectFirst: -> - @$items.removeClass('selected').first(':not(.not-matching)').addClass('selected') - - selectNext: -> - $next = @$items.filter('.selected').removeClass('selected').nextAll(':not(.not-matching)').first() - $next = @$items.filter(':not(.not-matching)').first() unless $next.length - $next.addClass('selected') - - selectPrevious: -> - $previous = @$items.filter('.selected').removeClass('selected').prevAll(':not(.not-matching)').first() - $previous = @$items.filter(':not(.not-matching)').last() unless $previous.length - $previous.addClass('selected') - - goToSelectedStack: -> - if stack = @$items.filter('.selected').filter(':not(.not-matching)').find('.commits-path').attr('href') - window.location = stack - -search = new StackSearch(document) - -jQuery -> - $('.stack-search').focus() diff --git a/app/assets/javascripts/shipit/stacks.js b/app/assets/javascripts/shipit/stacks.js new file mode 100644 index 000000000..f263cd1a3 --- /dev/null +++ b/app/assets/javascripts/shipit/stacks.js @@ -0,0 +1,81 @@ +var $document; + +$document = $(document); + +$document.on('click', '.commit-lock a', function(event) { + var $commit, $link, locked; + event.preventDefault(); + $commit = $(event.target).closest('.commit'); + $link = $(event.target).closest('a'); + locked = $commit.hasClass('locked'); + $commit.toggleClass('locked'); + $.ajax($link.attr('href'), { + method: 'PATCH', + data: { + commit: { + locked: !locked + } + } + }); +}); + +$document.on('click', '.action-set-release-status', function(event) { + var $deploy, $link, newStatus; + event.preventDefault(); + $link = $(event.target).closest('a'); + $deploy = $link.closest('.deploy'); + newStatus = $link.data('status'); + if ($deploy.attr('data-release-status') === newStatus) { + return; + } + $.ajax($link.attr('href'), { + method: 'POST', + data: { + status: newStatus + } + }).success(function(last_status) { + $deploy.attr('data-release-status', last_status.state); + }); +}); + +jQuery(function($) { + var dismissIgnoreCiMessage, displayIgnoreCiMessage, getLocalStorageKey; + displayIgnoreCiMessage = function() { + var ignoreCiMessage; + ignoreCiMessage = $(".ignoring-ci"); + if (!ignoreCiMessage) { + return; + } + $('.dismiss-ignore-ci-warning').click(function(event) { + event.preventDefault(); + dismissIgnoreCiMessage(); + }); + if (localStorage.getItem(getLocalStorageKey())) { + ignoreCiMessage.hide(); + } + }; + dismissIgnoreCiMessage = function() { + var ignoreCiMessage; + localStorage.setItem(getLocalStorageKey(), true); + ignoreCiMessage = $(".ignoring-ci"); + if (ignoreCiMessage) { + ignoreCiMessage.hide(); + } + }; + getLocalStorageKey = function() { + var stackName; + stackName = $('.repo-name').data('repo-full-name'); + return "ignoreCIDismissed" + stackName; + }; + displayIgnoreCiMessage(); + $(document).on('click', '.setting-ccmenu input[type=submit]', function(event) { + event.preventDefault(); + $(event.target).prop('disabled', true); + $.get(event.target.dataset.remote).done(function(data) { + $('#ccmenu-url').val(data.ccmenu_url).removeClass('hidden'); + $(event.target).addClass('hidden'); + }).fail(function() { + $(event.target).prop('disabled', false); + }); + }); +}); diff --git a/app/assets/javascripts/shipit/stacks.js.coffee b/app/assets/javascripts/shipit/stacks.js.coffee deleted file mode 100644 index 84bcc55be..000000000 --- a/app/assets/javascripts/shipit/stacks.js.coffee +++ /dev/null @@ -1,55 +0,0 @@ -$document = $(document) - -$document.on 'click', '.commit-lock a', (event) -> - event.preventDefault() - $commit = $(event.target).closest('.commit') - $link = $(event.target).closest('a') - - locked = $commit.hasClass('locked') - $commit.toggleClass('locked') - - $.ajax($link.attr('href'), method: 'PATCH', data: {commit: {locked: !locked}}) - -$document.on 'click', '.action-set-release-status', (event) -> - event.preventDefault() - $link = $(event.target).closest('a') - $deploy = $link.closest('.deploy') - newStatus = $link.data('status') - - return if $deploy.attr('data-release-status') == newStatus - - $.ajax($link.attr('href'), method: 'POST', data: {status: newStatus}).success((last_status) -> - $deploy.attr('data-release-status', last_status.state) - ) - -jQuery ($) -> - displayIgnoreCiMessage = -> - ignoreCiMessage = $(".ignoring-ci") - return unless ignoreCiMessage - $('.dismiss-ignore-ci-warning').click (event) -> - event.preventDefault() - dismissIgnoreCiMessage() - - if localStorage.getItem(getLocalStorageKey()) - ignoreCiMessage.hide() - - dismissIgnoreCiMessage = -> - localStorage.setItem(getLocalStorageKey(), true) - ignoreCiMessage = $(".ignoring-ci") - ignoreCiMessage.hide() if ignoreCiMessage - - getLocalStorageKey = -> - stackName = $('.repo-name').data('repo-full-name') - "ignoreCIDismissed" + stackName - - displayIgnoreCiMessage() - - $(document).on 'click', '.setting-ccmenu input[type=submit]', (event) -> - event.preventDefault() - $(event.target).prop('disabled', true) - $.get(event.target.dataset.remote).done((data) -> - $('#ccmenu-url').val(data.ccmenu_url).removeClass('hidden') - $(event.target).addClass('hidden') - ).fail(-> - $(event.target).prop('disabled', false) - ) diff --git a/app/assets/javascripts/task.js b/app/assets/javascripts/task.js new file mode 100644 index 000000000..0e9dce08e --- /dev/null +++ b/app/assets/javascripts/task.js @@ -0,0 +1,35 @@ +//= require string_includes +//= require mousetrap +//= require mousetrap-global-bind +//= require lodash +//= require clusterize +//= require_tree ./task +//= require_self + +this.OutputStream = new Stream; + +jQuery(function() { + var $code, initialOutput, search, task, tty; + $code = $('code'); + initialOutput = $code.attr('data-output'); + $code.removeAttr('data-output'); + search = new SearchBar($('.search-bar')); + OutputStream.addEventListener('status', function(status, response) { + $('[data-status]').attr('data-status', status); + if (status === 'aborted' && response.rollback_url) { + window.location = response.rollback_url; + } + }); + tty = new TTY($('body')); + search.addEventListener('query', tty.filterOutput); + search.immediateBroadcastQueryChange(); + OutputStream.addEventListener('chunk', tty.appendChunk); + if (task = $('[data-task]').data('task')) { + Notifications.init(OutputStream, task); + } + OutputStream.init({ + status: $code.closest('[data-status]').data('status'), + url: $code.data('next-chunks-url'), + text: initialOutput + }); +}); diff --git a/app/assets/javascripts/task.js.coffee b/app/assets/javascripts/task.js.coffee deleted file mode 100644 index 1874635c4..000000000 --- a/app/assets/javascripts/task.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -#= require string_includes -#= require mousetrap -#= require mousetrap-global-bind -#= require lodash -#= require clusterize -#= require_tree ./task -#= require_self - -@OutputStream = new Stream - -jQuery -> - $code = $('code') - initialOutput = $code.attr('data-output') - $code.removeAttr('data-output') - - search = new SearchBar($('.search-bar')) - - OutputStream.addEventListener 'status', (status, response) -> - $('[data-status]').attr('data-status', status) - - if status == 'aborted' && response.rollback_url - window.location = response.rollback_url - - tty = new TTY($('body')) - search.addEventListener('query', tty.filterOutput) - search.immediateBroadcastQueryChange() - OutputStream.addEventListener('chunk', tty.appendChunk) - - if task = $('[data-task]').data('task') - Notifications.init(OutputStream, task) - - OutputStream.init - status: $code.closest('[data-status]').data('status') - url: $code.data('next-chunks-url') - text: initialOutput diff --git a/app/assets/javascripts/task/ansi_colors.js b/app/assets/javascripts/task/ansi_colors.js new file mode 100644 index 000000000..8dc62a6a1 --- /dev/null +++ b/app/assets/javascripts/task/ansi_colors.js @@ -0,0 +1,10 @@ +//= require ansi_stream +//= require ./tty + +var stream; + +stream = new AnsiStream(); + +TTY.appendFormatter(function(chunk) { + return stream.process(chunk); +}); diff --git a/app/assets/javascripts/task/ansi_colors.js.coffee b/app/assets/javascripts/task/ansi_colors.js.coffee deleted file mode 100644 index 0b54c8214..000000000 --- a/app/assets/javascripts/task/ansi_colors.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -#= require ansi_stream -#= require ./tty - -stream = new AnsiStream() -TTY.appendFormatter (chunk) -> - stream.process(chunk) diff --git a/app/assets/javascripts/task/notifications.js.coffee.erb b/app/assets/javascripts/task/notifications.js.coffee.erb deleted file mode 100644 index 05dbddd84..000000000 --- a/app/assets/javascripts/task/notifications.js.coffee.erb +++ /dev/null @@ -1,27 +0,0 @@ -class @Notifications - IMAGES = - success: '<%= image_path "deploy_success.jpg" %>' - failed: '<%= image_path "deploy_failed.jpg" %>' - error: '<%= image_path "deploy_error.jpg" %>' - - @init: (outputStream, task) -> - outputStream.addEventListener('status', new this(task).statusUpdated) - - constructor: ({@repo, @description}) -> - - statusUpdated: (status) => - return unless status of IMAGES - return unless $.notifyCheck() == $.NOTIFY_ALLOWED - $.notify(IMAGES[status], @repo, @message(status)) - - message: (status) -> - deployShortSha = $('.short-sha').text() - switch status - when 'success' - "Your #{@description} was successful!" - when 'failed' - "Your #{@description} failed." - when 'error' - "Error during #{@description}." - else - "Your #{@description} ended with status: #{status}" diff --git a/app/assets/javascripts/task/notifications.js.erb b/app/assets/javascripts/task/notifications.js.erb new file mode 100644 index 000000000..5d826278d --- /dev/null +++ b/app/assets/javascripts/task/notifications.js.erb @@ -0,0 +1,48 @@ +var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +this.Notifications = (function() { + var IMAGES; + + IMAGES = { + success: '<%= image_path "deploy_success.jpg" %>', + failed: '<%= image_path "deploy_failed.jpg" %>', + error: '<%= image_path "deploy_error.jpg" %>' + }; + + Notifications.init = function(outputStream, task) { + outputStream.addEventListener('status', new this(task).statusUpdated); + }; + + function Notifications(arg) { + this.repo = arg.repo, this.description = arg.description; + this.statusUpdated = bind(this.statusUpdated, this); + } + + Notifications.prototype.statusUpdated = function(status) { + if (!(status in IMAGES)) { + return; + } + if ($.notifyCheck() !== $.NOTIFY_ALLOWED) { + return; + } + $.notify(IMAGES[status], this.repo, this.message(status)); + }; + + Notifications.prototype.message = function(status) { + var deployShortSha; + deployShortSha = $('.short-sha').text(); + switch (status) { + case 'success': + return "Your " + this.description + " was successful!"; + case 'failed': + return "Your " + this.description + " failed."; + case 'error': + return "Error during " + this.description + "."; + default: + return "Your " + this.description + " ended with status: " + status; + } + }; + + return Notifications; + +})(); diff --git a/app/assets/javascripts/task/plugins.js b/app/assets/javascripts/task/plugins.js new file mode 100644 index 000000000..666d1f266 --- /dev/null +++ b/app/assets/javascripts/task/plugins.js @@ -0,0 +1,13 @@ +this.Shipit || (this.Shipit = {}); + +this.Shipit.Plugins = { + config: function(name) { + var config; + config = $("meta[name=\"" + name + "-config\"]").attr('content'); + try { + return JSON.parse(config); + } catch (error) { + return null; + } + } +}; diff --git a/app/assets/javascripts/task/plugins.js.coffee b/app/assets/javascripts/task/plugins.js.coffee deleted file mode 100644 index 3bd573969..000000000 --- a/app/assets/javascripts/task/plugins.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -@Shipit ||= {} -@Shipit.Plugins = - config: (name) -> - config = $("""meta[name="#{name}-config"]""").attr('content') - try - JSON.parse(config) - catch - null diff --git a/app/assets/javascripts/task/search_bar.js b/app/assets/javascripts/task/search_bar.js new file mode 100644 index 000000000..05aa3c161 --- /dev/null +++ b/app/assets/javascripts/task/search_bar.js @@ -0,0 +1,88 @@ +var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +this.SearchBar = (function() { + var DEBOUNCE; + + DEBOUNCE = 300; + + function SearchBar($bar) { + this.$bar = $bar; + this.closeIfEmpty = bind(this.closeIfEmpty, this); + this.open = bind(this.open, this); + this.immediateBroadcastQueryChange = bind(this.immediateBroadcastQueryChange, this); + this.updateQuery = bind(this.updateQuery, this); + this.eventListeners = {}; + this.query = window.location.hash.replace(/^#/, ''); + this.$input = this.$bar.find('.search-input'); + this.$input.on('blur', this.closeIfEmpty); + this.$input.on('input', this.updateQuery); + this.broadcastQueryChange = _.debounce(this.immediateBroadcastQueryChange, DEBOUNCE); + Mousetrap.bindGlobal(['command+f', 'ctrl+f'], this.open); + if (this.query) { + this.open(); + this.setQuery(this.query); + } + } + + SearchBar.prototype.addEventListener = function(type, handler) { + this.listeners(type).push(handler); + }; + + SearchBar.prototype.listeners = function(type) { + var base; + return (base = this.eventListeners)[type] || (base[type] = []); + }; + + SearchBar.prototype.setQuery = function(query) { + this.$input.val(query); + this.updateQuery(); + }; + + SearchBar.prototype.updateQuery = function() { + var oldQuery; + oldQuery = this.query; + this.query = this.$input.val(); + if (this.query !== oldQuery) { + this.broadcastQueryChange(); + } + }; + + SearchBar.prototype.immediateBroadcastQueryChange = function() { + var handler, i, len, ref; + this.updateHash(); + ref = this.listeners('query'); + for (i = 0, len = ref.length; i < len; i++) { + handler = ref[i]; + handler(this.query); + } + }; + + SearchBar.prototype.updateHash = function() { + window.location.hash = "#" + this.query; + }; + + SearchBar.prototype.open = function(event) { + if (event != null) { + event.preventDefault(); + } + this.$bar.removeClass('hidden'); + this.focus(); + }; + + SearchBar.prototype.focus = function() { + this.$input.focus()[0].select(); + }; + + SearchBar.prototype.closeIfEmpty = function(event) { + if (!this.query.length) { + this.close(); + } + }; + + SearchBar.prototype.close = function() { + this.$bar.addClass('hidden'); + }; + + return SearchBar; + +})(); diff --git a/app/assets/javascripts/task/search_bar.js.coffee b/app/assets/javascripts/task/search_bar.js.coffee deleted file mode 100644 index e74b12239..000000000 --- a/app/assets/javascripts/task/search_bar.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ -class @SearchBar - DEBOUNCE = 300 - - constructor: (@$bar) -> - @eventListeners = {} - @query = window.location.hash.replace(/^#/, '') - @$input = @$bar.find('.search-input') - @$input.on('blur', @closeIfEmpty) - @$input.on('input', @updateQuery) - @broadcastQueryChange = _.debounce(@immediateBroadcastQueryChange, DEBOUNCE) - Mousetrap.bindGlobal(['command+f', 'ctrl+f'], @open) - - if @query - @open() - @setQuery(@query) - - addEventListener: (type, handler) -> - @listeners(type).push(handler) - - listeners: (type) -> - @eventListeners[type] ||= [] - - setQuery: (query) -> - @$input.val(query) - @updateQuery() - - updateQuery: => - oldQuery = @query - @query = @$input.val() - @broadcastQueryChange() unless @query == oldQuery - - immediateBroadcastQueryChange: => - @updateHash() - for handler in @listeners('query') - handler(@query) - - updateHash: -> - window.location.hash = "##{@query}" - - open: (event) => - event?.preventDefault() - @$bar.removeClass('hidden') - @focus() - - focus: -> - @$input.focus()[0].select() - - closeIfEmpty: (event) => - @close() unless @query.length - - close: -> - @$bar.addClass('hidden') diff --git a/app/assets/javascripts/task/sidebar.js b/app/assets/javascripts/task/sidebar.js new file mode 100644 index 000000000..653e14e4f --- /dev/null +++ b/app/assets/javascripts/task/sidebar.js @@ -0,0 +1,26 @@ +this.Sidebar = (function() { + var INSTANCE; + + INSTANCE = null; + + Sidebar.instance = function() { + return INSTANCE || (INSTANCE = new this($('.sidebar'), $('.sidebar-plugins'))); + }; + + Sidebar.newWidgetContainer = function() { + return Sidebar.instance().newWidgetContainer(); + }; + + function Sidebar($sidebar, $container) { + this.$sidebar = $sidebar; + this.$container = $container; + } + + Sidebar.prototype.newWidgetContainer = function() { + this.$sidebar.addClass('enabled'); + return $(document.createElement('div')).addClass('sidebar-plugin').prependTo(this.$container); + }; + + return Sidebar; + +})(); diff --git a/app/assets/javascripts/task/sidebar.js.coffee b/app/assets/javascripts/task/sidebar.js.coffee deleted file mode 100644 index 7c2a3383a..000000000 --- a/app/assets/javascripts/task/sidebar.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -class @Sidebar - INSTANCE = null - - @instance: -> - INSTANCE ||= new this($('.sidebar'), $('.sidebar-plugins')) - - @newWidgetContainer: -> - Sidebar.instance().newWidgetContainer() - - constructor: (@$sidebar, @$container) -> - - newWidgetContainer: -> - @$sidebar.addClass('enabled') - $(document.createElement('div')).addClass('sidebar-plugin').prependTo(@$container) diff --git a/app/assets/javascripts/task/stream.js b/app/assets/javascripts/task/stream.js new file mode 100644 index 000000000..ae3c2e215 --- /dev/null +++ b/app/assets/javascripts/task/stream.js @@ -0,0 +1,146 @@ +var Chunk, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + slice = [].slice; + +Chunk = (function() { + function Chunk(raw) { + this.raw = raw; + } + + Chunk.prototype.rawText = function() { + return this.raw; + }; + + Chunk.prototype.text = function() { + return this._text || (this._text = AnsiStream.strip(this.raw)); + }; + + Chunk.prototype.rawLines = function() { + return this._rawLines || (this._rawLines = this.splitLines(this.raw)); + }; + + Chunk.prototype.lines = function() { + return this._lines || (this._lines = this.splitLines(this.text())); + }; + + Chunk.prototype.splitLines = function(text) { + var lines; + lines = text.split(/\r?\n/); + if (!lines[lines.length - 1]) { + lines.pop(); + } + return lines; + }; + + return Chunk; + +})(); + +this.Stream = (function() { + var INTERVAL, MAX_RETRIES; + + INTERVAL = 1000; + + MAX_RETRIES = 15; + + function Stream() { + this.error = bind(this.error, this); + this.success = bind(this.success, this); + this.poll = bind(this.poll, this); + this.url = null; + this.eventListeners = {}; + this.retries = 0; + this.status = 'running'; + } + + Stream.prototype.init = function(arg) { + var status, text, url; + url = arg.url, text = arg.text, status = arg.status; + this.status = status; + this.broadcastOutput(text); + this.start(url); + }; + + Stream.prototype.poll = function() { + jQuery.ajax(this.url, { + success: this.success, + error: this.error + }); + }; + + Stream.prototype.success = function(response) { + this.retries = 0; + this.broadcastOutput(response.output, response); + this.broadcastStatus(response.status, response); + this.start(response.url || false); + }; + + Stream.prototype.broadcastStatus = function() { + var args, error, handler, i, len, ref, status; + status = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + if (status !== this.status) { + this.status = status; + ref = this.listeners('status'); + for (i = 0, len = ref.length; i < len; i++) { + handler = ref[i]; + try { + handler.apply(null, [status].concat(slice.call(args))); + } catch (error1) { + error = error1; + if (typeof console !== "undefined" && console !== null) { + console.log("Plugin error: " + error); + } + } + } + } + }; + + Stream.prototype.broadcastOutput = function() { + var args, chunk, error, handler, i, len, raw, ref; + raw = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + if (!raw) { + return; + } + chunk = new Chunk(raw); + ref = this.listeners('chunk'); + for (i = 0, len = ref.length; i < len; i++) { + handler = ref[i]; + try { + handler.apply(null, [chunk].concat(slice.call(args))); + } catch (error1) { + error = error1; + if (typeof console !== "undefined" && console !== null) { + console.log("Plugin error: " + error); + } + } + } + }; + + Stream.prototype.error = function(response) { + var ref; + if ((600 > (ref = response.status) && ref >= 500) && (this.retries += 1) < MAX_RETRIES) { + this.start(); + } + }; + + Stream.prototype.start = function(url) { + if (url == null) { + url = this.url; + } + if (this.url = url) { + setTimeout(this.poll, INTERVAL); + } + }; + + Stream.prototype.addEventListener = function(type, handler) { + this.listeners(type).push(handler); + }; + + Stream.prototype.listeners = function(type) { + var base; + return (base = this.eventListeners)[type] || (base[type] = []); + }; + + return Stream; + +})(); diff --git a/app/assets/javascripts/task/stream.js.coffee b/app/assets/javascripts/task/stream.js.coffee deleted file mode 100644 index 7b48d1109..000000000 --- a/app/assets/javascripts/task/stream.js.coffee +++ /dev/null @@ -1,77 +0,0 @@ -class Chunk - constructor: (@raw) -> - - rawText: -> - @raw - - text: -> - @_text ||= AnsiStream.strip(@raw) - - rawLines: -> - @_rawLines ||= @splitLines(@raw) - - lines: -> - @_lines ||= @splitLines(@text()) - - splitLines: (text) -> - lines = text.split(/\r?\n/) - lines.pop() unless lines[lines.length - 1] - lines - -class @Stream - INTERVAL = 1000 - MAX_RETRIES = 15 - - constructor: -> - @url = null - @eventListeners = {} - @retries = 0 - @status = 'running' - - init: ({url, text, status}) -> - @status = status - @broadcastOutput(text) - @start(url) - - poll: => - jQuery.ajax @url, - success: @success - error: @error - - success: (response) => - @retries = 0 - @broadcastOutput(response.output, response) - @broadcastStatus(response.status, response) - @start(response.url || false) - - broadcastStatus: (status, args...) -> - if status != @status - @status = status - for handler in @listeners('status') - try - handler(status, args...) - catch error - console?.log("Plugin error: #{error}") - - broadcastOutput: (raw, args...) -> - return unless raw - - chunk = new Chunk(raw) - for handler in @listeners('chunk') - try - handler(chunk, args...) - catch error - console?.log("Plugin error: #{error}") - - error: (response) => - @start() if 600 > response.status >= 500 && (@retries += 1) < MAX_RETRIES - - start: (url = @url) -> - if @url = url - setTimeout(@poll, INTERVAL) - - addEventListener: (type, handler) -> - @listeners(type).push(handler) - - listeners: (type) -> - @eventListeners[type] ||= [] diff --git a/app/assets/javascripts/task/tty.js b/app/assets/javascripts/task/tty.js new file mode 100644 index 000000000..52871bf59 --- /dev/null +++ b/app/assets/javascripts/task/tty.js @@ -0,0 +1,212 @@ +var OutputLines, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +OutputLines = (function() { + function OutputLines(screen, render) { + this.screen = screen; + this.render = render; + this.renderingCache = {}; + } + + OutputLines.prototype.append = function(lines) { + this.screen.append(this.renderLines(this.filter(lines))); + }; + + OutputLines.prototype.setFilter = function() { + return true; + }; + + OutputLines.prototype.filter = function(lines) { + return lines; + }; + + OutputLines.prototype.highlight = function(line) { + return line; + }; + + OutputLines.prototype.renderLines = function(lines) { + var base, i, len, line, results; + results = []; + for (i = 0, len = lines.length; i < len; i++) { + line = lines[i]; + results.push(this.highlight((base = this.renderingCache)[line] || (base[line] = this.render(line)))); + } + return results; + }; + + return OutputLines; + +})(); + +this.ClusterizeOutputLines = (function(superClass) { + extend(ClusterizeOutputLines, superClass); + + function ClusterizeOutputLines(screen, render) { + this.screen = screen; + this.render = render; + ClusterizeOutputLines.__super__.constructor.apply(this, arguments); + this.raw = []; + this.query = ''; + this.highlightRegexp = null; + this.stripCache = {}; + } + + ClusterizeOutputLines.prototype.append = function(lines) { + this.raw = this.raw.concat(lines); + ClusterizeOutputLines.__super__.append.apply(this, arguments); + }; + + ClusterizeOutputLines.prototype.setFilter = function(query) { + if (this.query = query) { + this.screen.options.no_data_text = 'No matches'; + } else { + this.screen.options.no_data_text = 'Loading...'; + } + this.highlightRegexp = this.buildHighlightRegexp(this.query); + this.reset(); + }; + + ClusterizeOutputLines.prototype.reset = function() { + this.screen.update(this.renderLines(this.filter(this.raw))); + }; + + ClusterizeOutputLines.prototype.strip = function(line) { + var base; + return (base = this.stripCache)[line] || (base[line] = AnsiStream.strip(line)); + }; + + ClusterizeOutputLines.prototype.filter = function(lines) { + var i, len, line, results; + if (!this.query) { + return lines; + } + results = []; + for (i = 0, len = lines.length; i < len; i++) { + line = lines[i]; + if (this.strip(line).includes(this.query)) { + results.push(line); + } + } + return results; + }; + + ClusterizeOutputLines.prototype.buildHighlightRegexp = function(query) { + var pattern; + pattern = query.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/(\s+)/g, '(<[^>]+>)*$1(<[^>]+>)*'); + return new RegExp("(" + pattern + ")", 'g'); + }; + + ClusterizeOutputLines.prototype.highlight = function(renderedLine) { + if (!this.query) { + return renderedLine; + } + return renderedLine.replace(this.highlightRegexp, '$1').replace(/([^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, '$1$2$4'); + }; + + return ClusterizeOutputLines; + +})(OutputLines); + +this.TTY = (function() { + var FORMATTERS, STICKY_SCROLL_TOLERENCE; + + FORMATTERS = []; + + STICKY_SCROLL_TOLERENCE = 200; + + TTY.appendFormatter = function(formatter) { + FORMATTERS.push(formatter); + }; + + TTY.prependFormatter = function(formatter) { + FORMATTERS.unshift(formatter); + }; + + function TTY($body) { + this.appendChunk = bind(this.appendChunk, this); + this.filterOutput = bind(this.filterOutput, this); + var scroller; + this.outputLines = []; + this.$code = $body.find('code'); + this.$container = this.$code.closest('.task-output-container'); + if (this.$container.hasClass('clusterize-scroll')) { + scroller = new Clusterize({ + no_data_text: 'Loading...', + tag: 'div', + contentElem: this.$code[0], + scrollElem: this.$container[0] + }); + this.output = new ClusterizeOutputLines(scroller, (function(_this) { + return function(line) { + return _this.createLine(_this.formatChunks(line)); + }; + })(this)); + } else { + this.output = new OutputLines(this.$code, (function(_this) { + return function(line) { + return _this.createLine(_this.formatChunks(line)); + }; + })(this)); + } + } + + TTY.prototype.filterOutput = function(query) { + this.output.setFilter(query); + }; + + TTY.prototype.formatChunks = function(chunk) { + var formatter, i, len; + for (i = 0, len = FORMATTERS.length; i < len; i++) { + formatter = FORMATTERS[i]; + chunk = formatter(chunk) || chunk; + } + return chunk; + }; + + TTY.prototype.appendChunk = function(chunk) { + var lines; + lines = chunk.rawLines(); + if (!lines.length) { + return; + } + this.preserveScroll((function(_this) { + return function() { + _this.output.append(lines); + }; + })(this)); + }; + + TTY.prototype.createLine = function(fragment) { + var div; + div = document.createElement('div'); + div.appendChild(fragment); + div.className = 'output-line'; + return div.outerHTML; + }; + + TTY.prototype.isScrolledToBottom = function() { + return (this.getMaxScroll() - this.$container.scrollTop()) < 1; + }; + + TTY.prototype.scrollToBottom = function() { + this.$container.scrollTop(this.getMaxScroll()); + }; + + TTY.prototype.getMaxScroll = function() { + return this.$code.parent().outerHeight(true) - this.$container.outerHeight(true); + }; + + TTY.prototype.preserveScroll = function(callback) { + var wasScrolledToBottom; + wasScrolledToBottom = this.isScrolledToBottom(); + callback(); + if (wasScrolledToBottom) { + this.scrollToBottom(); + } + }; + + return TTY; + +})(); diff --git a/app/assets/javascripts/task/tty.js.coffee b/app/assets/javascripts/task/tty.js.coffee deleted file mode 100644 index c8fb55e3f..000000000 --- a/app/assets/javascripts/task/tty.js.coffee +++ /dev/null @@ -1,119 +0,0 @@ -class OutputLines - constructor: (@screen, @render) -> - @renderingCache = {} - - append: (lines) -> - @screen.append(@renderLines(@filter(lines))) - - setFilter: -> - true - - filter: (lines) -> - lines - - highlight: (line) -> - line - - renderLines: (lines) -> - for line in lines - @highlight(@renderingCache[line] ||= @render(line)) - -class @ClusterizeOutputLines extends OutputLines - constructor: (@screen, @render) -> - super - @raw = [] - @query = '' - @highlightRegexp = null - @stripCache = {} - - append: (lines) -> - @raw = @raw.concat(lines) - super - - setFilter: (query) -> - if @query = query - @screen.options.no_data_text = 'No matches' - else - @screen.options.no_data_text = 'Loading...' - @highlightRegexp = @buildHighlightRegexp(@query) - @reset() - - reset: -> - @screen.update(@renderLines(@filter(@raw))) - - strip: (line) -> - @stripCache[line] ||= AnsiStream.strip(line) - - filter: (lines) -> - return lines unless @query - line for line in lines when @strip(line).includes(@query) - - buildHighlightRegexp: (query) -> - pattern = query.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/(\s+)/g, '(<[^>]+>)*$1(<[^>]+>)*') - new RegExp("(#{pattern})", 'g') - - highlight: (renderedLine) -> - return renderedLine unless @query - - renderedLine.replace(@highlightRegexp, '$1').replace(/([^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, '$1$2$4'); - -class @TTY - FORMATTERS = [] - STICKY_SCROLL_TOLERENCE = 200 - - @appendFormatter: (formatter) -> - FORMATTERS.push(formatter) - - @prependFormatter: (formatter) -> - FORMATTERS.unshift(formatter) - - constructor: ($body) -> - @outputLines = [] - @$code = $body.find('code') - @$container = @$code.closest('.task-output-container') - if @$container.hasClass('clusterize-scroll') - scroller = new Clusterize( - no_data_text: 'Loading...' - tag: 'div' - contentElem: @$code[0] - scrollElem: @$container[0] - ) - @output = new ClusterizeOutputLines(scroller, (line) => @createLine(@formatChunks(line))) - else - @output = new OutputLines(@$code, (line) => @createLine(@formatChunks(line))) - - filterOutput: (query) => - @output.setFilter(query) - - formatChunks: (chunk) -> - for formatter in FORMATTERS - chunk = formatter(chunk) || chunk - chunk - - appendChunk: (chunk) => - lines = chunk.rawLines() - return unless lines.length - - @preserveScroll => - @output.append(lines) - - createLine: (fragment) -> - div = document.createElement('div') - div.appendChild(fragment) - div.className = 'output-line' - div.outerHTML - - isScrolledToBottom: -> - (@getMaxScroll() - @$container.scrollTop()) < 1 - - scrollToBottom: -> - @$container.scrollTop(@getMaxScroll()) - - getMaxScroll: -> - @$code.parent().outerHeight(true) - @$container.outerHeight(true) - - preserveScroll: (callback) -> - wasScrolledToBottom = @isScrolledToBottom() - callback() - @scrollToBottom() if wasScrolledToBottom - diff --git a/lib/shipit.rb b/lib/shipit.rb index 1b6f738f2..04fe3660f 100644 --- a/lib/shipit.rb +++ b/lib/shipit.rb @@ -8,7 +8,6 @@ require 'explicit-parameters' require 'paquito' -require 'coffee-rails' require 'jquery-rails' require 'rails-timeago' require 'lodash-rails' diff --git a/shipit-engine.gemspec b/shipit-engine.gemspec index edee8ec2a..a955c5b13 100644 --- a/shipit-engine.gemspec +++ b/shipit-engine.gemspec @@ -23,7 +23,6 @@ Gem::Specification.new do |s| s.add_dependency('active_model_serializers', '~> 0.9.3') s.add_dependency('ansi_stream', '~> 0.0.6') s.add_dependency('autoprefixer-rails', '~> 6.4.1') - s.add_dependency('coffee-rails', '~> 5.0') s.add_dependency('explicit-parameters', '~> 0.4.0') s.add_dependency('faraday', '~> 1.3') s.add_dependency('faraday-http-cache', '~> 2.2') diff --git a/vendor/assets/javascripts/ansi_stream.js b/vendor/assets/javascripts/ansi_stream.js new file mode 100644 index 000000000..47381f1b9 --- /dev/null +++ b/vendor/assets/javascripts/ansi_stream.js @@ -0,0 +1,161 @@ +// Compiled from ansi_stream gem (v0.0.6) ansi_stream.coffee using CoffeeScript 1.12.7 +// +// Copyright (c) 2014 Guillaume Malette +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +var AnsiSpan, AnsiStyle; + +this.AnsiStream = (function() { + var ANSI_CODE; + + ANSI_CODE = /((\u001b\[)|\u009b)(\d{0,3}(;\d{0,3})*[A-M|f-m])|\u001b[A-M]/g; + + AnsiStream.strip = function(text) { + return text.replace(ANSI_CODE, ''); + }; + + function AnsiStream() { + this.style = new AnsiStyle(); + this.span = new AnsiSpan(); + } + + AnsiStream.prototype.process = function(text) { + var first_part, i, len, part, partText, parts, ref, span, spans, styles; + parts = text.split(/\033\[/); + spans = document.createDocumentFragment(); + first_part = parts.shift(); + if (first_part) { + spans.appendChild(this.span.create(first_part, this.style)); + } + for (i = 0, len = parts.length; i < len; i++) { + part = parts[i]; + ref = this._extractTextAndStyles(part), partText = ref[0], styles = ref[1]; + this.style.apply(styles); + span = this.span.create(partText, this.style); + spans.appendChild(span); + } + return spans; + }; + + AnsiStream.prototype._extractTextAndStyles = function(originalText) { + var matches, numbers, ref, text; + matches = originalText.match(/^([\d;]*)m([^]*)$/); + if (!matches) { + return [originalText, null]; + } + ref = matches, matches = ref[0], numbers = ref[1], text = ref[2]; + return [text, numbers.split(";")]; + }; + + return AnsiStream; + +})(); + +AnsiStyle = (function() { + var COLORS; + + COLORS = { + 0: 'black', + 1: 'red', + 2: 'green', + 3: 'yellow', + 4: 'blue', + 5: 'magenta', + 6: 'cyan', + 7: 'white', + 8: null, + 9: 'default' + }; + + function AnsiStyle() { + this.reset(); + } + + AnsiStyle.prototype.apply = function(newStyles) { + var i, len, style; + if (!newStyles) { + return; + } + for (i = 0, len = newStyles.length; i < len; i++) { + style = newStyles[i]; + style = parseInt(style); + if (style === 0) { + this.reset(); + } else if (style === 1) { + this.bright = true; + } else if ((30 <= style && style <= 39) && style !== 38) { + this._applyStyle('foreground', style); + } else if ((40 <= style && style <= 49) && style !== 48) { + this._applyStyle('background', style); + } else if (style === 4) { + this.underline = true; + } else if (style === 24) { + this.underline = false; + } + } + }; + + AnsiStyle.prototype.reset = function() { + this.background = this.foreground = 'default'; + this.underline = this.bright = false; + }; + + AnsiStyle.prototype.toClass = function() { + var classes; + classes = []; + if (this.background) { + classes.push("ansi-background-" + this.background); + } + if (this.foreground) { + classes.push("ansi-foreground-" + this.foreground); + } + if (this.bright) { + classes.push("ansi-bright"); + } + if (this.underline) { + classes.push("ansi-underline"); + } + return classes.join(" "); + }; + + AnsiStyle.prototype._applyStyle = function(layer, number) { + this[layer] = COLORS[number % 10]; + }; + + return AnsiStyle; + +})(); + +AnsiSpan = (function() { + function AnsiSpan() {} + + AnsiSpan.prototype.create = function(text, style) { + var span; + span = document.createElement('span'); + span.textContent = text; + span.className = style.toClass(); + return span; + }; + + return AnsiSpan; + +})();