diff --git a/app/components/running-jobs-item.coffee b/app/components/running-jobs-item.coffee new file mode 100644 index 00000000..fb5fe7c7 --- /dev/null +++ b/app/components/running-jobs-item.coffee @@ -0,0 +1,8 @@ +`import Ember from 'ember'` +`import Polling from 'travis/mixins/polling'` + +RunningJobsItemComponent = Ember.Component.extend(Polling, + pollModels: 'job' +) + +`export default RunningJobsItemComponent` diff --git a/app/controllers/builds-item.coffee b/app/controllers/builds-item.coffee index 3db4ba54..50cc588a 100644 --- a/app/controllers/builds-item.coffee +++ b/app/controllers/builds-item.coffee @@ -6,15 +6,15 @@ Controller = Ember.ObjectController.extend(GithubUrlProperties, needs: ['builds'] isPullRequestsListBinding: 'controllers.builds.isPullRequestsList' - buildBinding: 'content' + buildBinding: 'model' color: (-> colorForState(@get('build.state')) ).property('build.state') urlAuthorGravatarImage: (-> - gravatarImage(@get('commit.authorEmail'), 40) - ).property('commit.authorEmail') + gravatarImage(@get('build.commit.authorEmail'), 40) + ).property('build.commit.authorEmail') ) `export default Controller` diff --git a/app/controllers/running-jobs.coffee b/app/controllers/running-jobs.coffee index 10b09e45..1058b304 100644 --- a/app/controllers/running-jobs.coffee +++ b/app/controllers/running-jobs.coffee @@ -12,7 +12,8 @@ Controller = Ember.ArrayController.extend isLoaded: false content: (-> - result = @store.filter('job', { state: 'started' }, (job) -> + # TODO: this should also query for received jobs + result = @store.filter('job', {}, (job) -> ['started', 'received'].indexOf(job.get('state')) != -1 ) result.then => diff --git a/app/mixins/polling.coffee b/app/mixins/polling.coffee new file mode 100644 index 00000000..83cdca8d --- /dev/null +++ b/app/mixins/polling.coffee @@ -0,0 +1,63 @@ +`import Ember from 'ember'` + +mixin = Ember.Mixin.create + polling: Ember.inject.service() + + didInsertElement: -> + @_super.apply(this, arguments) + + @startPolling() + + willDestroyElement: -> + @_super.apply(this, arguments) + + @stopPolling() + + pollModelDidChange: (sender, key, value) -> + @pollModel(key) + + pollModelWillChange: (sender, key, value) -> + @stopPollingModel(key) + + pollModel: (property) -> + addToPolling = (model) => + @get('polling').startPolling(model) + + if model = @get(property) + if model.then + model.then (resolved) -> + addToPolling(resolved) + else + addToPolling(model) + + stopPollingModel: (property) -> + if model = @get(property) + @get('polling').stopPolling(model) + + startPolling: -> + pollModels = @get('pollModels') + + if pollModels + pollModels = [pollModels] unless Ember.isArray(pollModels) + + pollModels.forEach (property) => + @pollModel(property) + @addObserver(property, this, 'pollModelDidChange') + Ember.addBeforeObserver(this, property, this, 'pollModelWillChange') + + @get('polling').startPollingHook(this) if @pollHook + + stopPolling: -> + pollModels = @get('pollModels') + return unless pollModels + + pollModels = [pollModels] unless Ember.isArray(pollModels) + + pollModels.forEach (property) => + @stopPollingModel(property) + @removeObserver(property, this, 'pollModelDidChange') + Ember.removeBeforeObserver(this, property, this, 'pollModelWillChange') + + @get('polling').stopPollingHook(this) + +`export default mixin` diff --git a/app/routes/abstract-builds.coffee b/app/routes/abstract-builds.coffee index 1a79b7ec..d64f93e3 100644 --- a/app/routes/abstract-builds.coffee +++ b/app/routes/abstract-builds.coffee @@ -11,6 +11,7 @@ Route = TravisRoute.extend @controllerFor('repo').activate(@get('contentType')) @contentDidChange() @controllerFor('repo').addObserver(@get('path'), this, 'contentDidChange') + @controllerFor('build').set('contentType', @get('contentType')) deactivate: -> @controllerFor('repo').removeObserver(@get('path'), this, 'contentDidChange') diff --git a/app/services/polling.coffee b/app/services/polling.coffee new file mode 100644 index 00000000..f4a3dd1c --- /dev/null +++ b/app/services/polling.coffee @@ -0,0 +1,51 @@ +`import Ember from 'ember'` + +service = Ember.Service.extend + pollingInterval: 30000 + + init: -> + @_super.apply(this, arguments) + + @set('watchedModels', []) + @set('sources', []) + + interval = setInterval => + @poll() + , @get('pollingInterval') + + @set('interval', interval) + + willDestroy: -> + @_super.apply(this, arguments) + + clearInterval(@get('interval')) + + startPollingHook: (source) -> + sources = @get('sources') + unless sources.contains(source) + sources.pushObject(source) + + stopPollingHook: (source) -> + sources = @get('sources') + sources.removeObject(source) + + startPolling: (model) -> + watchedModels = @get('watchedModels') + unless watchedModels.contains(model) + watchedModels.pushObject(model) + + stopPolling: (model) -> + watchedModels = @get('watchedModels') + watchedModels.removeObject(model) + + poll: -> + @get('watchedModels').forEach (model) -> + model.reload() + + @get('sources').forEach (source) => + if Ember.get(source, 'isDestroyed') + @get('sources').removeObject(source) + else + source.pollHook() + +`export default service` diff --git a/app/styles/app/layouts/profile.sass b/app/styles/app/layouts/profile.sass index afb6d0be..ceae5ea1 100644 --- a/app/styles/app/layouts/profile.sass +++ b/app/styles/app/layouts/profile.sass @@ -195,11 +195,15 @@ p.profile-user-last .hooks-error width: 100%; padding: 0 $column-gutter/2; + margin-top: 3.3rem; p position: relative padding: $column-gutter/2 $column-gutter*2 $column-gutter/2 $column-gutter/2; color: #de4248 background-color: #f1b6ad + a + color: #de4248 + text-decoration: underline &:after content: "" position: absolute diff --git a/app/templates/components/running-jobs-item.hbs b/app/templates/components/running-jobs-item.hbs new file mode 100644 index 00000000..889d9eea --- /dev/null +++ b/app/templates/components/running-jobs-item.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/app/templates/running-jobs.hbs b/app/templates/running-jobs.hbs index 0c12c853..c99fd7e3 100644 --- a/app/templates/running-jobs.hbs +++ b/app/templates/running-jobs.hbs @@ -1,29 +1,7 @@ {{#if isLoaded}} {{#if controller.length}} {{#each job in controller}} -
- {{#if job.repo.slug}} - - {{#link-to "job" job.repo job}}{{job.repo.slug}}{{/link-to}} - {{/if}} - -

- - {{#if job.repo.slug}} - {{#link-to "job" job.repo job}}{{job.number}}{{/link-to}} - {{/if}} -

- -

- - Duration: - - {{format-duration job.duration}} - -

- -
- + {{running-jobs-item job=job}} {{/each}} {{else}}
There are no jobs running
diff --git a/app/views/build.coffee b/app/views/build.coffee index 10792cdb..124b8055 100644 --- a/app/views/build.coffee +++ b/app/views/build.coffee @@ -1,10 +1,13 @@ `import { colorForState } from 'travis/utils/helpers'` `import BasicView from 'travis/views/basic'` +`import Polling from 'travis/mixins/polling'` -View = BasicView.extend +View = BasicView.extend Polling, classNameBindings: ['color'] buildBinding: 'controller.build' + pollModels: 'controller.build' + color: (-> colorForState(@get('build.state')) ).property('build.state') diff --git a/app/views/builds.coffee b/app/views/builds.coffee new file mode 100644 index 00000000..cdee99f0 --- /dev/null +++ b/app/views/builds.coffee @@ -0,0 +1,18 @@ +`import BasicView from 'travis/views/basic'` +`import Polling from 'travis/mixins/polling'` + +View = BasicView.extend Polling, + pollHook: (store) -> + contentType = @get('controller.contentType') + repositoryId = @get('controller.repo.id') + store = @get('controller.store') + + if contentType == 'builds' + store.find('build', { event_type: 'push', repository_id: repositoryId }) + else if contentType == 'pull_requests' + store.filter('build', { event_type: 'pull_request', repository_id: repositoryId }) + else + store.find 'build', repository_id: repositoryId, branches: true + + +`export default View` diff --git a/app/views/job.coffee b/app/views/job.coffee index 8eaeeafb..b4c6a092 100644 --- a/app/views/job.coffee +++ b/app/views/job.coffee @@ -1,8 +1,11 @@ `import Ember from 'ember'` `import { colorForState } from 'travis/utils/helpers'` `import { githubCommit, gravatarImage } from 'travis/utils/urls'` +`import Polling from 'travis/mixins/polling'` + +View = Ember.View.extend Polling, + pollModels: 'controller.job.build' -View = Ember.View.extend repoBinding: 'controller.repo' jobBinding: 'controller.job' commitBinding: 'job.commit' diff --git a/app/views/repo-show-tools.coffee b/app/views/repo-show-tools.coffee index 57b153f2..2f19aecd 100644 --- a/app/views/repo-show-tools.coffee +++ b/app/views/repo-show-tools.coffee @@ -9,7 +9,7 @@ View = BasicView.extend buildBinding: 'controller.build' jobBinding: 'controller.job' tabBinding: 'controller.tab' - currentUserBinding: 'controller.currentUser' + currentUserBinding: 'controller.currentUser.model' slugBinding: 'controller.repo.slug' diff --git a/app/views/repo.coffee b/app/views/repo.coffee index 64089307..90a3db2c 100644 --- a/app/views/repo.coffee +++ b/app/views/repo.coffee @@ -2,14 +2,17 @@ `import StatusImagesView from 'travis/views/status-images'` `import BasicView from 'travis/views/basic'` `import config from 'travis/config/environment'` +`import Polling from 'travis/mixins/polling'` -View = BasicView.extend +View = BasicView.extend Polling, reposBinding: 'controllers.repos' repoBinding: 'controller.repo' buildBinding: 'controller.build' jobBinding: 'controller.job' tabBinding: 'controller.tab' + pollModels: 'controller.repo' + classNameBindings: ['controller.isLoading:loading'] isEmpty: (-> diff --git a/app/views/repos-list.coffee b/app/views/repos-list.coffee index 65239f90..5d4fba03 100644 --- a/app/views/repos-list.coffee +++ b/app/views/repos-list.coffee @@ -1,5 +1,6 @@ `import Ember from 'ember'` `import { colorForState } from 'travis/utils/helpers'` +`import Polling from 'travis/mixins/polling'` View = Ember.CollectionView.extend elementId: '' @@ -8,7 +9,9 @@ View = Ember.CollectionView.extend emptyView: Ember.View.extend templateName: 'repos-list/empty' - itemViewClass: Ember.View.extend + itemViewClass: Ember.View.extend Polling, + pollModels: 'repo' + repoBinding: 'content' classNames: ['repo'] classNameBindings: ['color', 'selected'] diff --git a/app/views/running-jobs.coffee b/app/views/running-jobs.coffee new file mode 100644 index 00000000..11c9a768 --- /dev/null +++ b/app/views/running-jobs.coffee @@ -0,0 +1,8 @@ +`import BasicView from 'travis/views/basic'` +`import Polling from 'travis/mixins/polling'` + +View = BasicView.extend Polling, + pollHook: (store) -> + @get('controller.store').find('job', {}) + +`export default View` diff --git a/tests/unit/components/running-jobs-item-test.coffee b/tests/unit/components/running-jobs-item-test.coffee new file mode 100644 index 00000000..69a8afd9 --- /dev/null +++ b/tests/unit/components/running-jobs-item-test.coffee @@ -0,0 +1,17 @@ +`import { test, moduleForComponent } from 'ember-qunit'` + +moduleForComponent 'running-jobs-item', { + # specify the other units that are required for this test + needs: ['mixin:polling', 'service:polling'] +} + +test 'it renders', (assert) -> + assert.expect 2 + + # creates the component instance + component = @subject() + assert.equal component._state, 'preRender' + + # renders the component to the page + @render() + assert.equal component._state, 'inDOM' diff --git a/tests/unit/mixins/polling-test.coffee b/tests/unit/mixins/polling-test.coffee new file mode 100644 index 00000000..7b127a8e --- /dev/null +++ b/tests/unit/mixins/polling-test.coffee @@ -0,0 +1,105 @@ +`import { test, moduleForComponent } from 'ember-qunit'` +`import Polling from 'travis/mixins/polling'` + +hookRuns = 0 +pollingChangesHistory = [] + +# define component just for testing +define('travis/components/polling-test', [], -> + PollingService = Ember.Object.extend( + startPolling: (model) -> + pollingChangesHistory.push(type: 'start', model: model) + + stopPolling: (model) -> + pollingChangesHistory.push(type: 'stop', model: model) + + startPollingHook: (source) -> + pollingChangesHistory.push(type: 'start-hook', source: source+'') + + stopPollingHook: (source) -> + pollingChangesHistory.push(type: 'stop-hook', source: source+'') + ) + + Ember.Component.extend(Polling, + init: -> + @_super.apply this, arguments + + @set('polling', PollingService.create()) + + pollModels: ['model1', 'model2'], + pollHook: -> + hookRuns += 1 + + toString: -> + '' + ) +) + + +# I want to test this mixin in context of component, so I'm using +# modelForComponent +moduleForComponent 'polling-test', 'PollingTestComponent', { + # specify the other units that are required for this test + needs: [] + + setup: -> + hookRuns = 0 + pollingChangesHistory = [] +} + +test 'it works even if one of the model is null', -> + component = @subject(model1: { name: 'model1' }) + @append() + + Ember.run -> + component.destroy() + + expected = [ + { type: 'start', model: { name: 'model1' } }, + { type: 'start-hook', source: '' } + { type: 'stop', model: { name: 'model1' } }, + { type: 'stop-hook', source: '' } + ] + + deepEqual pollingChangesHistory, expected + +test 'it polls for both models if they are present', -> + component = @subject(model1: { name: 'model1' }, model2: { name: 'model2' }) + @append() + + Ember.run -> + component.destroy() + + expected = [ + { type: 'start', model: { name: 'model1' } }, + { type: 'start', model: { name: 'model2' } }, + { type: 'start-hook', source: '' } + { type: 'stop', model: { name: 'model1' } }, + { type: 'stop', model: { name: 'model2' } }, + { type: 'stop-hook', source: '' } + ] + + deepEqual pollingChangesHistory, expected + +test 'it detects model changes', -> + component = @subject(model1: { name: 'foo' }) + @append() + + Ember.run -> + component.set('model1', { name: 'bar' }) + + Ember.run -> + component.destroy() + + expected = [ + { type: 'start', model: { name: 'foo' } }, + { type: 'start-hook', source: '' } + { type: 'stop', model: { name: 'foo' } }, + { type: 'start', model: { name: 'bar' } }, + { type: 'stop', model: { name: 'bar' } }, + { type: 'stop-hook', source: '' } + ] + + deepEqual pollingChangesHistory, expected + + diff --git a/tests/unit/services/polling-test.coffee b/tests/unit/services/polling-test.coffee new file mode 100644 index 00000000..6bbe1ac9 --- /dev/null +++ b/tests/unit/services/polling-test.coffee @@ -0,0 +1,168 @@ +`import Ember from 'ember'` +`import Polling from 'travis/services/polling'` + +service = null + +module 'PollingService', + teardown: -> + unless service.get('isDestroyed') + Ember.run -> + service.destroy() + +test 'polls for each of the models', -> + expect(3) + + history = [] + + service = Polling.create( + pollingInterval: 20 + ) + + model1 = { + reload: -> + ok(true) + history.push 'model1' + } + + model2 = { + reload: -> + ok(true) + history.push 'model2' + } + + service.startPolling(model1) + service.startPolling(model2) + + stop() + + setTimeout -> + start() + + deepEqual history, ['model1', 'model2'] + + Ember.run -> + service.destroy() + , 30 + +test 'it will stop running any reloads after it is destroyed', -> + expect(1) + + service = Polling.create( + pollingInterval: 20 + ) + + model = { + reload: -> + ok(true) + } + + service.startPolling(model) + + stop() + + setTimeout -> + Ember.run -> + service.destroy() + , 30 + + setTimeout -> + start() + , 50 + +test 'it stops reloading models after they were removed from polling', -> + expect(4) + + history = [] + + service = Polling.create( + pollingInterval: 30 + ) + + model1 = { + reload: -> + ok(true) + history.push 'model1' + } + + model2 = { + reload: -> + ok(true) + history.push 'model2' + } + + service.startPolling(model1) + service.startPolling(model2) + + stop() + + setTimeout -> + service.stopPolling(model2) + + setTimeout -> + Ember.run -> + service.destroy() + + start() + + deepEqual history, ['model1', 'model2', 'model1'] + , 30 + , 40 + +test 'it runs a hook on each interval', -> + expect(1) + + history = [] + + service = Polling.create( + pollingInterval: 20 + ) + + source = { + pollHook: -> + ok(true) + } + + service.startPollingHook(source) + + stop() + + setTimeout -> + service.stopPollingHook(source) + + setTimeout -> + Ember.run -> + service.destroy() + + start() + , 10 + , 30 + +test 'it will not run pollHook if the source is destroyed', -> + expect(1) + + history = [] + + service = Polling.create( + pollingInterval: 20 + ) + + source = Ember.Object.extend( + pollHook: -> + ok(true) + ).create() + + service.startPollingHook(source) + + stop() + + setTimeout -> + Ember.run -> + source.destroy() + + setTimeout -> + Ember.run -> + service.destroy() + + start() + , 35 + , 30