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 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',
buildUrl(modelName, id, snapshot, requestType, query) {
var url = this._super(...arguments);
return url;
},
ajaxOptions(url, type, options) {
var hash = options || {};
if(!hash.data) {
hash.data = {};
}
if(hash.data.include) {
hash.data.include += ',repository.default_branch,branch.last_build,build.commit';
} else {
hash.data.include = 'repository.default_branch,branch.last_build,build.commit';
if(Config.useV3API) {
if(hash.data.include) {
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);

View File

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

View File

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

View File

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

View File

@ -4,8 +4,54 @@
# the function stops being visible inside computed properties.
`import { durationFrom as durationFromHelper } from 'travis/utils/helpers'`
`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()
slug: DS.attr()
@ -14,15 +60,8 @@ Repo = Model.extend
githubLanguage: 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: ->
@filter( (repo) -> repo.get('defaultBranch.lastBuild') )
@filter( (repo) -> repo.get('lastBuildId') )
sshKey: (->
@store.find('ssh_key', @get('id'))
@ -39,7 +78,7 @@ Repo = Model.extend
builds: (->
id = @get('id')
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
@ -56,7 +95,7 @@ Repo = Model.extend
pullRequests: (->
id = @get('id')
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
@ -90,12 +129,12 @@ Repo = Model.extend
).property('slug')
sortOrderForLandingPage: (->
state = @get('defaultBranch.lastBuild.state')
state = @get('lastBuildState')
if state != 'passed' && state != 'failed'
0
else
parseInt(@get('defaultBranch.lastBuild.id'))
).property('defaultBranch.lastBuild.id', 'defaultBranch.lastBuild.state')
parseInt(@get('lastBuildId'))
).property('lastBuildId', 'lastBuildState')
stats: (->
if @get('slug')
@ -106,8 +145,11 @@ Repo = Model.extend
).property('slug')
updateTimes: ->
if lastBuild = @get('defaultBranch.lastBuild')
lastBuild.updateTimes()
if Config.useV3API
if lastBuild = @get('lastBuild')
lastBuild.updateTimes()
else
@notifyPropertyChange 'lastBuildDuration'
regenerateKey: (options) ->
@get('ajax').ajax '/repos/' + @get('id') + '/key', 'post', options
@ -123,41 +165,49 @@ Repo.reopenClass
recent: ->
@find()
accessibleBy: (store, reposIds) ->
# this fires only for authenticated users and with API v3 that means getting
# only repos of currently logged in owner, but in the future it would be
# 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()
accessibleBy: (store, reposIdsOrlogin) ->
if Config.useV3API
reposIds = reposIdsOrlogin
# this fires only for authenticated users and with API v3 that means getting
# only repos of currently logged in owner, but in the future it would be
# 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
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) ->
queryString = $.param(search: query, orderBy: 'name', limit: 5)
promise = ajax.ajax("/repos?#{queryString}", 'get')
result = Ember.ArrayProxy.create(content: [])
if Config.useV3API
queryString = $.param(search: query, orderBy: 'name', limit: 5)
promise = ajax.ajax("/repos?#{queryString}", 'get')
result = Ember.ArrayProxy.create(content: [])
promise.then (data, status, xhr) ->
promises = data.repos.map (repoData) ->
store.findRecord('repo', repoData.id).then (record) ->
result.pushObject(record)
result.set('isLoaded', true)
record
promise.then (data, status, xhr) ->
promises = data.repos.map (repoData) ->
store.findRecord('repo', repoData.id).then (record) ->
result.pushObject(record)
result.set('isLoaded', true)
record
Ember.RSVP.allSettled(promises).then ->
result
Ember.RSVP.allSettled(promises).then ->
result
else
store.find('repo', search: query, orderBy: 'name')
withLastBuild: (store) ->
repos = store.filter('repo', {}, (build) ->
build.get('defaultBranch.lastBuild')
build.get('lastBuildId')
)
repos.then () ->
@ -170,22 +220,31 @@ Repo.reopenClass
if repos.get('length') > 0
repos.get('firstObject')
else
adapter = store.adapterFor('repo')
modelClass = store.modelFor('repo')
adapter.findRecord(store, modelClass, slug).then (payload) ->
serializer = store.serializerFor('repo')
promise = null
if Config.useV3API
adapter = store.adapterFor('repo')
modelClass = store.modelFor('repo')
result = serializer.normalizeResponse(store, modelClass, payload, null, 'findRecord')
repo = store.push(data: result.data)
for record in result.included
r = store.push(data: record)
promise = adapter.findRecord(store, modelClass, slug).then (payload) ->
serializer = store.serializerFor('repo')
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.slug = slug
Ember.get(repos, 'firstObject') || throw(error)
throw(error)
# buildURL: (slug) ->
# if slug then slug else 'repos'

View File

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

View File

@ -94,14 +94,6 @@ var Serializer = V2FallbackSerializer.extend({
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;
}
});

View File

@ -1,5 +1,5 @@
`import DS from 'ember-data'`
`import config from 'travis/config/environment'`
`import Config from 'travis/config/environment'`
Store = DS.Store.extend
auth: Ember.inject.service()
@ -71,21 +71,25 @@ Store = DS.Store.extend
if type == 'build' && (json.repository || json.repo)
data = json.repository || json.repo
default_branch = data.default_branch
if default_branch
default_branch.default_branch = true
if Config.useV3API
default_branch = data.default_branch
if default_branch
default_branch.default_branch = true
last_build_id = default_branch.last_build_id
# 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
# 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 may decide to make this relationship async, but I don't want to
# change the code at the moment
if build = @peekRecord('build', last_build_id)
@push(this.normalize('repo', data))
else
@findRecord('build', last_build_id).then =>
last_build_id = default_branch.last_build_id
# 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
# 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 may decide to make this relationship async, but I don't want to
# change the code at the moment
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))
else
@push(this.normalize('repo', data))
`export default Store`

View File

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

View File

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

View File

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

View File

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