Merge pull request #420 from travis-ci/feature-flag-api-v3

Feature flag API V3
This commit is contained in:
Piotr Sarnacki 2015-12-17 12:47:36 +01:00
commit 462b3d637f
13 changed files with 191 additions and 126 deletions

View File

@ -1,24 +1,24 @@
import V3Adapter from 'travis/adapters/v3'; import V3Adapter from 'travis/adapters/v3';
import ApplicationAdapter from 'travis/adapters/application';
import Config from 'travis/config/environment';
export default V3Adapter.extend({ let Adapter = Config.useV3API ? V3Adapter : ApplicationAdapter;
export default Adapter.extend({
defaultSerializer: '-repo', defaultSerializer: '-repo',
buildUrl(modelName, id, snapshot, requestType, query) {
var url = this._super(...arguments);
return url;
},
ajaxOptions(url, type, options) { ajaxOptions(url, type, options) {
var hash = options || {}; var hash = options || {};
if(!hash.data) { if(!hash.data) {
hash.data = {}; hash.data = {};
} }
if(hash.data.include) { if(Config.useV3API) {
hash.data.include += ',repository.default_branch,branch.last_build,build.commit'; if(hash.data.include) {
} else { hash.data.include += ',repository.default_branch,branch.last_build,build.commit';
hash.data.include = 'repository.default_branch,branch.last_build,build.commit'; } else {
hash.data.include = 'repository.default_branch,branch.last_build,build.commit';
}
} }
return this._super(url, type, hash); return this._super(url, type, hash);

View File

@ -16,8 +16,8 @@ ReposListItemComponent = Ember.Component.extend Polling,
).property('selectedRepo') ).property('selectedRepo')
color: (-> color: (->
colorForState(@get('repo.defaultBranch.lastBuild.state')) colorForState(@get('repo.lastBuildState'))
).property('repo.defaultBranch.lastBuild.state') ).property('repo.lastBuildState')
scrollTop: (-> scrollTop: (->
if (window.scrollY > 0) if (window.scrollY > 0)

View File

@ -4,12 +4,14 @@
Controller = Ember.Controller.extend Controller = Ember.Controller.extend
jobController: Ember.inject.controller('job') jobController: Ember.inject.controller('job')
buildController: Ember.inject.controller('build') buildController: Ember.inject.controller('build')
buildsController: Ember.inject.controller('builds')
reposController: Ember.inject.controller('repos') reposController: Ember.inject.controller('repos')
currentUserBinding: 'auth.currentUser' currentUserBinding: 'auth.currentUser'
classNames: ['repo'] classNames: ['repo']
build: Ember.computed.alias('buildController.build') build: Ember.computed.alias('buildController.build')
builds: Ember.computed.alias('buildsController.content')
job: Ember.computed.alias('jobController.job') job: Ember.computed.alias('jobController.job')
slug: (-> @get('repo.slug') ).property('repo.slug') slug: (-> @get('repo.slug') ).property('repo.slug')
@ -77,15 +79,15 @@ Controller = Ember.Controller.extend
Ember.run.scheduleOnce('actions', this, @_lastBuildDidChange); Ember.run.scheduleOnce('actions', this, @_lastBuildDidChange);
_lastBuildDidChange: -> _lastBuildDidChange: ->
build = @get('repo.defaultBranch.lastBuild') build = @get('repo.lastBuild')
@set('build', build) @set('build', build)
stopObservingLastBuild: -> stopObservingLastBuild: ->
@removeObserver('repo.defaultBranch.lastBuild', this, 'lastBuildDidChange') @removeObserver('repo.lastBuild', this, 'lastBuildDidChange')
observeLastBuild: -> observeLastBuild: ->
@lastBuildDidChange() @lastBuildDidChange()
@addObserver('repo.defaultBranch.lastBuild', this, 'lastBuildDidChange') @addObserver('repo.lastBuild', this, 'lastBuildDidChange')
connectTab: (tab) -> connectTab: (tab) ->
# TODO: such implementation seems weird now, because we render # TODO: such implementation seems weird now, because we render

View File

@ -1,27 +1,28 @@
import Ember from 'ember'; import Ember from 'ember';
import limit from 'travis/utils/computed-limit'; import limit from 'travis/utils/computed-limit';
import Repo from 'travis/models/repo'; import Repo from 'travis/models/repo';
import Config from 'travis/config/environment';
var sortCallback = function(repo1, repo2) { var sortCallback = function(repo1, repo2) {
// this function could be made simpler, but I think it's clearer this way // this function could be made simpler, but I think it's clearer this way
// what're we really trying to achieve // what're we really trying to achieve
var lastBuild1 = repo1.get('defaultBranch.lastBuild'); var lastBuildId1 = repo1.get('lastBuildId');
var lastBuild2 = repo2.get('defaultBranch.lastBuild'); var lastBuildId2 = repo2.get('lastBuildId');
if(!lastBuild1 && !lastBuild2) { if(!lastBuildId1 && !lastBuildId2) {
// if both repos lack builds, put newer repo first // if both repos lack builds, put newer repo first
return repo1.get('id') > repo2.get('id') ? -1 : 1; return repo1.get('id') > repo2.get('id') ? -1 : 1;
} else if(lastBuild1 && !lastBuild2) { } else if(lastBuildId1 && !lastBuildId2) {
// if only repo1 has a build, it goes first // if only repo1 has a build, it goes first
return -1; return -1;
} else if(lastBuild2 && !lastBuild1) { } else if(lastBuildId2 && !lastBuildId1) {
// if only repo2 has a build, it goes first // if only repo2 has a build, it goes first
return 1; return 1;
} }
var finishedAt1 = lastBuild1.get('finishedAt'); var finishedAt1 = repo1.get('lastBuildFinishedAt');
var finishedAt2 = lastBuild2.get('finishedAt'); var finishedAt2 = repo2.get('lastBuildFinishedAt');
if(finishedAt1) { if(finishedAt1) {
finishedAt1 = new Date(finishedAt1); finishedAt1 = new Date(finishedAt1);
@ -41,7 +42,7 @@ var sortCallback = function(repo1, repo2) {
return -1; return -1;
} else { } else {
// none of the builds finished, put newer build first // none of the builds finished, put newer build first
return lastBuild1.get('id') > lastBuild2.get('id') ? -1 : 1; return lastBuildId1 > lastBuildId2 ? -1 : 1;
} }
throw "should not happen"; throw "should not happen";
@ -157,16 +158,22 @@ var Controller = Ember.Controller.extend({
this.set('isLoaded', false); this.set('isLoaded', false);
if (user = this.get('currentUser')) { if (user = this.get('currentUser')) {
user.get('_rawPermissions').then( (data) => { let callback = (reposRecordArray) => {
repos = Repo.accessibleBy(this.store, data.pull).then( this.set('isLoaded', true);
(reposRecordArray) => { this.set('_repos', reposRecordArray);
this.set('isLoaded', true); this.set('ownedRepos', reposRecordArray);
this.set('_repos', reposRecordArray); this.set('fetchingOwnedRepos', false);
this.set('ownedRepos', reposRecordArray); return reposRecordArray;
this.set('fetchingOwnedRepos', false); };
return reposRecordArray;
}); if(Config.useV3API) {
}); user.get('_rawPermissions').then( (data) => {
Repo.accessibleBy(this.store, data.pull).then(callback);
});
} else {
let login = user.get('login');
Repo.accessibleBy(this.store, login).then(callback);
}
} }
} }
}, },

View File

@ -18,7 +18,6 @@ Build = Model.extend DurationCalculations,
pullRequestTitle: DS.attr() pullRequestTitle: DS.attr()
pullRequestNumber: DS.attr('number') pullRequestNumber: DS.attr('number')
eventType: DS.attr('string') eventType: DS.attr('string')
repositoryId: DS.attr('number')
branch: DS.belongsTo('branch', async: false, inverse: 'builds') branch: DS.belongsTo('branch', async: false, inverse: 'builds')
repo: DS.belongsTo('repo', async: true) repo: DS.belongsTo('repo', async: true)

View File

@ -4,8 +4,54 @@
# the function stops being visible inside computed properties. # the function stops being visible inside computed properties.
`import { durationFrom as durationFromHelper } from 'travis/utils/helpers'` `import { durationFrom as durationFromHelper } from 'travis/utils/helpers'`
`import Build from 'travis/models/build'` `import Build from 'travis/models/build'`
`import Config from 'travis/config/environment'`
Repo = Model.extend Repo = null
if Config.useV3API
Repo = Model.extend
defaultBranch: DS.belongsTo('branch', async: false)
lastBuild: Ember.computed.oneWay('defaultBranch.lastBuild')
lastBuildFinishedAt: Ember.computed.oneWay('lastBuild.finishedAt')
lastBuildId: Ember.computed.oneWay('lastBuild.id')
lastBuildState: Ember.computed.oneWay('lastBuild.state')
lastBuildNumber: Ember.computed.oneWay('lastBuild.number')
lastBuildStartedAt: Ember.computed.oneWay('lastBuild.startedAt')
lastBuildDuration: Ember.computed.oneWay('lastBuild.duration')
else
Repo = Model.extend
lastBuildNumber: DS.attr('number')
lastBuildState: DS.attr()
lastBuildStartedAt: DS.attr()
lastBuildFinishedAt: DS.attr()
_lastBuildDuration: DS.attr('number')
lastBuildLanguage: DS.attr()
lastBuildId: DS.attr('number')
lastBuildHash: (->
{
id: @get('lastBuildId')
number: @get('lastBuildNumber')
repo: this
}
).property('lastBuildId', 'lastBuildNumber')
lastBuild: (->
if id = @get('lastBuildId')
@store.findRecord('build', id)
@store.recordForId('build', id)
).property('lastBuildId')
lastBuildDuration: (->
duration = @get('_lastBuildDuration')
duration = durationFromHelper(@get('lastBuildStartedAt'), @get('lastBuildFinishedAt')) unless duration
duration
).property('_lastBuildDuration', 'lastBuildStartedAt', 'lastBuildFinishedAt')
Repo.reopen
ajax: Ember.inject.service() ajax: Ember.inject.service()
slug: DS.attr() slug: DS.attr()
@ -14,15 +60,8 @@ Repo = Model.extend
githubLanguage: DS.attr() githubLanguage: DS.attr()
active: DS.attr() active: DS.attr()
#lastBuild: DS.belongsTo('build')
defaultBranch: DS.belongsTo('branch', async: false)
# just for sorting
lastBuildFinishedAt: Ember.computed.oneWay('defaultBranch.lastBuild.finishedAt')
lastBuildId: Ember.computed.oneWay('defaultBranch.lastBuild.id')
withLastBuild: -> withLastBuild: ->
@filter( (repo) -> repo.get('defaultBranch.lastBuild') ) @filter( (repo) -> repo.get('lastBuildId') )
sshKey: (-> sshKey: (->
@store.find('ssh_key', @get('id')) @store.find('ssh_key', @get('id'))
@ -39,7 +78,7 @@ Repo = Model.extend
builds: (-> builds: (->
id = @get('id') id = @get('id')
builds = @store.filter('build', event_type: ['push', 'api'], repository_id: id, (b) -> builds = @store.filter('build', event_type: ['push', 'api'], repository_id: id, (b) ->
b.get('repositoryId')+'' == id+'' && (b.get('eventType') == 'push' || b.get('eventType') == 'api') b.get('repo.id')+'' == id+'' && (b.get('eventType') == 'push' || b.get('eventType') == 'api')
) )
# TODO: move to controller # TODO: move to controller
@ -56,7 +95,7 @@ Repo = Model.extend
pullRequests: (-> pullRequests: (->
id = @get('id') id = @get('id')
builds = @store.filter('build', event_type: 'pull_request', repository_id: id, (b) -> builds = @store.filter('build', event_type: 'pull_request', repository_id: id, (b) ->
b.get('repositoryId')+'' == id+'' && b.get('eventType') == 'pull_request' b.get('repo.id')+'' == id+'' && b.get('eventType') == 'pull_request'
) )
# TODO: move to controller # TODO: move to controller
@ -90,12 +129,12 @@ Repo = Model.extend
).property('slug') ).property('slug')
sortOrderForLandingPage: (-> sortOrderForLandingPage: (->
state = @get('defaultBranch.lastBuild.state') state = @get('lastBuildState')
if state != 'passed' && state != 'failed' if state != 'passed' && state != 'failed'
0 0
else else
parseInt(@get('defaultBranch.lastBuild.id')) parseInt(@get('lastBuildId'))
).property('defaultBranch.lastBuild.id', 'defaultBranch.lastBuild.state') ).property('lastBuildId', 'lastBuildState')
stats: (-> stats: (->
if @get('slug') if @get('slug')
@ -106,8 +145,11 @@ Repo = Model.extend
).property('slug') ).property('slug')
updateTimes: -> updateTimes: ->
if lastBuild = @get('defaultBranch.lastBuild') if Config.useV3API
lastBuild.updateTimes() if lastBuild = @get('lastBuild')
lastBuild.updateTimes()
else
@notifyPropertyChange 'lastBuildDuration'
regenerateKey: (options) -> regenerateKey: (options) ->
@get('ajax').ajax '/repos/' + @get('id') + '/key', 'post', options @get('ajax').ajax '/repos/' + @get('id') + '/key', 'post', options
@ -123,41 +165,49 @@ Repo.reopenClass
recent: -> recent: ->
@find() @find()
accessibleBy: (store, reposIds) -> accessibleBy: (store, reposIdsOrlogin) ->
# this fires only for authenticated users and with API v3 that means getting if Config.useV3API
# only repos of currently logged in owner, but in the future it would be reposIds = reposIdsOrlogin
# nice to not use that as it may change in the future # this fires only for authenticated users and with API v3 that means getting
repos = store.filter('repo', (repo) -> # only repos of currently logged in owner, but in the future it would be
reposIds.indexOf(parseInt(repo.get('id'))) != -1 # nice to not use that as it may change in the future
) repos = store.filter('repo', (repo) ->
reposIds.indexOf(parseInt(repo.get('id'))) != -1
promise = new Ember.RSVP.Promise (resolve, reject) ->
store.query('repo', { 'repository.active': 'true', limit: 20 }).then( ->
resolve(repos)
, ->
reject()
) )
promise promise = new Ember.RSVP.Promise (resolve, reject) ->
store.query('repo', { 'repository.active': 'true', limit: 20 }).then( ->
resolve(repos)
, ->
reject()
)
promise
else
login = reposIdsOrlogin
store.find('repo', { member: login, orderBy: 'name' })
search: (store, ajax, query) -> search: (store, ajax, query) ->
queryString = $.param(search: query, orderBy: 'name', limit: 5) if Config.useV3API
promise = ajax.ajax("/repos?#{queryString}", 'get') queryString = $.param(search: query, orderBy: 'name', limit: 5)
result = Ember.ArrayProxy.create(content: []) promise = ajax.ajax("/repos?#{queryString}", 'get')
result = Ember.ArrayProxy.create(content: [])
promise.then (data, status, xhr) -> promise.then (data, status, xhr) ->
promises = data.repos.map (repoData) -> promises = data.repos.map (repoData) ->
store.findRecord('repo', repoData.id).then (record) -> store.findRecord('repo', repoData.id).then (record) ->
result.pushObject(record) result.pushObject(record)
result.set('isLoaded', true) result.set('isLoaded', true)
record record
Ember.RSVP.allSettled(promises).then -> Ember.RSVP.allSettled(promises).then ->
result result
else
store.find('repo', search: query, orderBy: 'name')
withLastBuild: (store) -> withLastBuild: (store) ->
repos = store.filter('repo', {}, (build) -> repos = store.filter('repo', {}, (build) ->
build.get('defaultBranch.lastBuild') build.get('lastBuildId')
) )
repos.then () -> repos.then () ->
@ -170,22 +220,31 @@ Repo.reopenClass
if repos.get('length') > 0 if repos.get('length') > 0
repos.get('firstObject') repos.get('firstObject')
else else
adapter = store.adapterFor('repo') promise = null
modelClass = store.modelFor('repo')
adapter.findRecord(store, modelClass, slug).then (payload) -> if Config.useV3API
serializer = store.serializerFor('repo') adapter = store.adapterFor('repo')
modelClass = store.modelFor('repo') modelClass = store.modelFor('repo')
result = serializer.normalizeResponse(store, modelClass, payload, null, 'findRecord')
repo = store.push(data: result.data) promise = adapter.findRecord(store, modelClass, slug).then (payload) ->
for record in result.included serializer = store.serializerFor('repo')
r = store.push(data: record) modelClass = store.modelFor('repo')
result = serializer.normalizeResponse(store, modelClass, payload, null, 'findRecord')
repo repo = store.push(data: result.data)
, -> for record in result.included
r = store.push(data: record)
repo
else
promise = store.find('repo', { slug: slug }).then (repos) ->
repos.get('firstObject') || throw("no repos found")
promise.catch ->
error = new Error('repo not found') error = new Error('repo not found')
error.slug = slug error.slug = slug
Ember.get(repos, 'firstObject') || throw(error) throw(error)
# buildURL: (slug) -> # buildURL: (slug) ->
# if slug then slug else 'repos' # if slug then slug else 'repos'

View File

@ -1,4 +1,5 @@
`import TravisRoute from 'travis/routes/basic'` `import TravisRoute from 'travis/routes/basic'`
`import Config from 'travis/config/environment'`
Route = TravisRoute.extend Route = TravisRoute.extend
setupController: (controller, model) -> setupController: (controller, model) ->
@ -6,7 +7,7 @@ Route = TravisRoute.extend
@controllerFor('repo').activate('current') @controllerFor('repo').activate('current')
renderTemplate: -> renderTemplate: ->
if @modelFor('repo').get('defaultBranch.lastBuild') if @modelFor('repo').get('lastBuildId')
@render 'build' @render 'build'
else else
@render 'builds/not_found' @render 'builds/not_found'

View File

@ -94,14 +94,6 @@ var Serializer = V2FallbackSerializer.extend({
data = result.data; data = result.data;
if (repoId = resourceHash.repository_id) {
data.attributes.repositoryId = repoId;
} else if (resourceHash.repository) {
if (href = resourceHash.repository['@href']) {
id = href.match(/\d+/)[0];
data.attributes.repositoryId = id;
}
}
return result; return result;
} }
}); });

View File

@ -1,5 +1,5 @@
`import DS from 'ember-data'` `import DS from 'ember-data'`
`import config from 'travis/config/environment'` `import Config from 'travis/config/environment'`
Store = DS.Store.extend Store = DS.Store.extend
auth: Ember.inject.service() auth: Ember.inject.service()
@ -71,21 +71,25 @@ Store = DS.Store.extend
if type == 'build' && (json.repository || json.repo) if type == 'build' && (json.repository || json.repo)
data = json.repository || json.repo data = json.repository || json.repo
default_branch = data.default_branch if Config.useV3API
if default_branch default_branch = data.default_branch
default_branch.default_branch = true if default_branch
default_branch.default_branch = true
last_build_id = default_branch.last_build_id last_build_id = default_branch.last_build_id
# a build is a synchronous relationship on a branch model, so we need to # a build is a synchronous relationship on a branch model, so we need to
# have a build record present when we put default_branch from a repository # have a build record present when we put default_branch from a repository
# model into the store. We don't send last_build's payload in pusher, so # model into the store. We don't send last_build's payload in pusher, so
# we need to get it here, if it's not already in the store. In the future # we need to get it here, if it's not already in the store. In the future
# we may decide to make this relationship async, but I don't want to # we may decide to make this relationship async, but I don't want to
# change the code at the moment # change the code at the moment
if build = @peekRecord('build', last_build_id) if !last_build_id || (build = @peekRecord('build', last_build_id))
@push(this.normalize('repo', data))
else
@findRecord('build', last_build_id).then =>
@push(this.normalize('repo', data)) @push(this.normalize('repo', data))
else
@findRecord('build', last_build_id).then =>
@push(this.normalize('repo', data))
else
@push(this.normalize('repo', data))
`export default Store` `export default Store`

View File

@ -1,19 +1,19 @@
<div class="tile {{repo.defaultBranch.lastBuild.state}}"> <div class="tile {{repo.lastBuildState}}">
<h2 class="tile-title {{repo.defaultBranch.lastBuild.state}}"> <h2 class="tile-title {{repo.lastBuildState}}">
{{#if repo.slug}} {{#if repo.slug}}
{{#link-to "repo" repo}} {{#link-to "repo" repo}}
{{status-icon status=repo.defaultBranch.lastBuild.state}} {{status-icon status=repo.lastBuildState}}
<span class="label-align">{{repo.slug}}</span> <span class="label-align">{{repo.slug}}</span>
{{/link-to}} {{/link-to}}
{{/if}} {{/if}}
</h2> </h2>
{{#if repo.slug}} {{#if repo.slug}}
{{#if repo.defaultBranch.lastBuild.id}} {{#if repo.lastBuildId}}
<p class="tile-title float-right {{repo.defaultBranch.lastBuild.state}}"> <p class="tile-title float-right {{repo.lastBuildState}}">
{{#link-to "build" repo repo.defaultBranch.lastBuild.id}} {{#link-to "build" repo repo.lastBuildId}}
<span class="icon-hash"></span> <span class="icon-hash"></span>
<span class="label-align">{{repo.defaultBranch.lastBuild.number}}</span> <span class="label-align">{{repo.lastBuildNumber}}</span>
{{/link-to}} {{/link-to}}
</p> </p>
{{/if}} {{/if}}
@ -22,16 +22,16 @@
<p> <p>
<span class="icon-clock"></span> <span class="icon-clock"></span>
<span class="label-align">Duration: <span class="label-align">Duration:
<abbr class="duration" title={{repo.defaultBranch.lastBuild.startedAt}}> <abbr class="duration" title={{repo.lastBuildStartedAt}}>
{{format-duration repo.defaultBranch.lastBuild.duration}} {{format-duration repo.lastBuildDuration}}
</abbr></span> </abbr></span>
</p> </p>
<p> <p>
<span class="icon-calendar"></span> <span class="icon-calendar"></span>
<span class="label-align">Finished: <span class="label-align">Finished:
<abbr class="finished_at timeago" title={{repo.defaultBranch.lastBuild.finishedAt}}> <abbr class="finished_at timeago" title={{repo.lastBuildFinishedAt}}>
{{format-time repo.defaultBranch.lastBuild.finishedAt}} {{format-time repo.lastBuildFinishedAt}}
</abbr></span> </abbr></span>
</p> </p>
</div> </div>

View File

@ -26,7 +26,7 @@
{{#if repo.active}} {{#if repo.active}}
{{outlet}} {{outlet}}
{{else}} {{else}}
{{#if repo.defaultBranch.lastBuild.id}} {{#if repo.lastBuildId}}
{{outlet}} {{outlet}}
{{else}} {{else}}
{{not-active user=currentUser repo=repo}} {{not-active user=currentUser repo=repo}}

View File

@ -13,7 +13,7 @@ mixin = Ember.Mixin.create
updateTimes: -> updateTimes: ->
unless @get('isFinished') unless @get('isFinished')
@notifyPropertyChange '_duration' @notifyPropertyChange 'duration'
@notifyPropertyChange 'finished_at' @notifyPropertyChange 'finishedAt'
`export default mixin` `export default mixin`

View File

@ -2,6 +2,7 @@
module.exports = function(environment) { module.exports = function(environment) {
var ENV = { var ENV = {
useV3API: false,
modulePrefix: 'travis', modulePrefix: 'travis',
environment: environment, environment: environment,
baseURL: '/', baseURL: '/',