Merge pull request #350 from travis-ci/ajax-polling

Ajax polling
This commit is contained in:
Piotr Sarnacki 2015-05-11 12:15:39 +02:00
commit dfbd1cab2c
19 changed files with 467 additions and 32 deletions

View File

@ -0,0 +1,8 @@
`import Ember from 'ember'`
`import Polling from 'travis/mixins/polling'`
RunningJobsItemComponent = Ember.Component.extend(Polling,
pollModels: 'job'
)
`export default RunningJobsItemComponent`

View File

@ -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`

View File

@ -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 =>

63
app/mixins/polling.coffee Normal file
View File

@ -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`

View File

@ -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')

View File

@ -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`

View File

@ -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

View File

@ -0,0 +1 @@
{{yield}}

View File

@ -1,29 +1,7 @@
{{#if isLoaded}}
{{#if controller.length}}
{{#each job in controller}}
<div {{bind-attr class=":tile :tile--sidebar job.state"}}>
{{#if job.repo.slug}}
<span {{bind-attr class=":icon :icon--job job.state"}}></span>
{{#link-to "job" job.repo job}}{{job.repo.slug}}{{/link-to}}
{{/if}}
<p class="tile-title float-right">
<span class="icon icon--hash"></span>
{{#if job.repo.slug}}
{{#link-to "job" job.repo job}}{{job.number}}{{/link-to}}
{{/if}}
</p>
<p>
<span class="icon icon--clock"></span>
Duration:
<abbr class="duration" {{bind-attr title="job.startedAt"}}>
{{format-duration job.duration}}
</abbr>
</p>
</div>
{{running-jobs-item job=job}}
{{/each}}
{{else}}
<div class="spinner-container">There are no jobs running</div>

View File

@ -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')

18
app/views/builds.coffee Normal file
View File

@ -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`

View File

@ -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'

View File

@ -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'

View File

@ -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: (->

View File

@ -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']

View File

@ -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`

View File

@ -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'

View File

@ -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: ->
'<PollingTestingComponent>'
)
)
# 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: '<PollingTestingComponent>' }
{ type: 'stop', model: { name: 'model1' } },
{ type: 'stop-hook', source: '<PollingTestingComponent>' }
]
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: '<PollingTestingComponent>' }
{ type: 'stop', model: { name: 'model1' } },
{ type: 'stop', model: { name: 'model2' } },
{ type: 'stop-hook', source: '<PollingTestingComponent>' }
]
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: '<PollingTestingComponent>' }
{ type: 'stop', model: { name: 'foo' } },
{ type: 'start', model: { name: 'bar' } },
{ type: 'stop', model: { name: 'bar' } },
{ type: 'stop-hook', source: '<PollingTestingComponent>' }
]
deepEqual pollingChangesHistory, expected

View File

@ -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