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}}
-
-
+ {{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