Merge branch 'ps-env-vars'

Conflicts:
	assets/scripts/app/controllers.coffee
	assets/scripts/app/helpers/handlebars.coffee
	assets/scripts/app/templates/repo/settings.hbs
	assets/scripts/app/views/repo/show.coffee
	assets/styles/tabs.sass
	config.ru
This commit is contained in:
Piotr Sarnacki 2014-08-21 16:02:07 +02:00
commit 9741ede075
41 changed files with 1021 additions and 189 deletions

View File

@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64" fill="white">
<circle cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(45 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.125s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(90 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.25s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(135 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.375s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(180 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.5s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(225 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.625s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(270 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.75s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(315 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.875s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
<circle transform="rotate(180 16 16)" cx="16" cy="3" r="0">
<animate attributeName="r" values="0;3;0;0" dur="1s" repeatCount="indefinite" begin="0.5s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
</circle>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/images/ui/secure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

View File

@ -22,10 +22,13 @@ unless window.TravisApplication
annotations: Travis.Annotation
request: Travis.Request
requests: Travis.Request
env_var: Travis.EnvVar
env_vars: Travis.EnvVar
ssh_key: Travis.SshKey
).property()
modelClasses: (->
[Travis.User, Travis.Build, Travis.Job, Travis.Repo, Travis.Commit, Travis.Worker, Travis.Account, Travis.Broadcast, Travis.Hook, Travis.Annotation, Travis.Request]
[Travis.User, Travis.Build, Travis.Job, Travis.Repo, Travis.Commit, Travis.Worker, Travis.Account, Travis.Broadcast, Travis.Hook, Travis.Annotation, Travis.Request, Travis.EnvVar, Travis.SshKey]
).property()
setup: ->
@ -33,6 +36,12 @@ unless window.TravisApplication
klass.adapter = Travis.Adapter.create()
klass.url = "/#{klass.pluralName()}"
Travis.EnvVar.url = "/settings/env_vars"
Travis.EnvVar.adapter = Travis.EnvVarsAdapter.create()
Travis.SshKey.url = "/settings/ssh_key"
Travis.SshKey.adapter = Travis.SshKeyAdapter.create()
@slider = new Travis.Slider()
@pusher = new Travis.Pusher(Travis.config.pusher_key) if Travis.config.pusher_key
@tailing = new Travis.Tailing($(window), '#tail', '#log')

View File

@ -50,27 +50,6 @@ Travis.FirstSyncController = Em.Controller.extend
Travis.IndexErrorController = Em.Controller.extend()
Travis.RepoSettingsTabController = Em.ObjectController.extend()
Travis.RepoSettingsController = Em.ObjectController.extend
needs: ['repoSettingsTab']
tab: Ember.computed.alias('controllers.repoSettingsTab.model.tab')
settings: Ember.computed.alias('model.settings')
settingsChanged: (->
value = @get('settings.maximum_number_of_builds')
console.log value
if parseInt(value) > 0 || value == '0' || value == 0
@set('settings.maximum_number_of_builds_valid', '')
@get('model').saveSettings(@get('settings')).then null, ->
Travis.flash(error: 'There was an error while saving settings. Please try again.')
else
@set('settings.maximum_number_of_builds_valid', 'invalid')
).observes('settings.maximum_number_of_builds')
save: ->
@get('model').saveSettings(@get('settings')).then null, ->
Travis.flash(error: 'There was an error while saving settings. Please try again.')
require 'controllers/accounts'
require 'controllers/auth'
require 'controllers/account'
@ -82,8 +61,13 @@ require 'controllers/job'
require 'controllers/profile'
require 'controllers/repos'
require 'controllers/repo'
require 'controllers/repo_settings'
require 'controllers/stats'
require 'controllers/current_user'
require 'controllers/request'
require 'controllers/requests'
require 'controllers/caches'
require 'controllers/env_var'
require 'controllers/env_vars'
require 'controllers/env_var_new'
require 'controllers/ssh_key'

View File

@ -0,0 +1,52 @@
require 'travis/validations'
Travis.EnvVarController = Ember.ObjectController.extend Travis.Validations,
isEditing: false
isDeleting: false
validates:
name: ['presence']
actionType: 'Save'
showValueField: Ember.computed.alias('public')
value: ( (key, value) ->
if arguments.length == 2
@get('model').set('value', value)
value
else if @get('public')
@get('model.value')
else
'••••••••••••••••'
).property('model.value', 'public')
actions:
delete: ->
return if @get('isDeleting')
@set('isDeleting', true)
deletingDone = => @set('isDeleting', false)
@get('model').deleteRecord().then deletingDone, deletingDone
edit: ->
@set('isEditing', true)
cancel: ->
@set('isEditing', false)
@get('model').revert()
save: ->
return if @get('isSaving')
@set('isSaving', true)
if @isValid()
env_var = @get('model')
# TODO: handle errors
env_var.save().then =>
@set('isEditing', false)
@set('isSaving', false)
, =>
@set('isSaving', false)
else
@set('isSaving', false)

View File

@ -0,0 +1,41 @@
require 'travis/validations'
Travis.EnvVarsNewController = Travis.Controller.extend Travis.Validations,
needs: ['repo']
repo: Ember.computed.alias('controllers.repo.repo')
validates:
name: ['presence']
actionType: 'Add'
showValueField: true
reset: ->
@setProperties(name: null, value: null, public: null)
actions:
cancel: ->
@reset()
@transitionToRoute('env_vars')
save: ->
return if @get('isSaving')
@set('isSaving', true)
if @isValid()
env_var = Travis.EnvVar.create(
name: @get('name')
value: @get('value')
public: @get('public')
repo: @get('repo')
)
self = this
env_var.save().then =>
@set('isSaving', false)
@reset()
self.transitionToRoute('env_vars')
, =>
@set('isSaving', false)
else
@set('isSaving', false)

View File

@ -0,0 +1 @@
Travis.EnvVarsController = Ember.ArrayController.extend()

View File

@ -59,6 +59,9 @@ Travis.RepoController = Travis.Controller.extend
viewRequest: ->
@connectTab('request')
viewSettings: ->
@connectTab('settings')
lastBuildDidChange: ->
Ember.run.scheduleOnce('data', this, @_lastBuildDidChange);

View File

@ -0,0 +1,30 @@
Travis.RepoSettingsController = Em.ObjectController.extend
tabs:
index: "General Settings"
env_vars: "Environment Variables"
ssh_key: "SSH Key"
init: ->
@_super.apply this, arguments
tabs = []
@set('_tabs', tabs)
for own id, name of @get('tabs')
tabs.pushObject Travis.Tab.create(id: id, name: name)
settings: Ember.computed.alias('model.settings')
settingsChanged: (->
value = @get('settings.maximum_number_of_builds')
console.log value
if parseInt(value) > 0 || value == '0' || value == 0
@set('settings.maximum_number_of_builds_valid', '')
@get('model').saveSettings(@get('settings')).then null, ->
Travis.flash(error: 'There was an error while saving settings. Please try again.')
else
@set('settings.maximum_number_of_builds_valid', 'invalid')
).observes('settings.maximum_number_of_builds')
save: ->
@get('model').saveSettings(@get('settings')).then null, ->
Travis.flash(error: 'There was an error while saving settings. Please try again.')

View File

@ -0,0 +1,51 @@
require 'travis/validations'
Travis.SshKeyController = Ember.ObjectController.extend Travis.Validations,
isEditing: false
isSaving: false
isDeleting: false
defaultKey: null
needs: ['repo']
repo: Ember.computed.alias('controllers.repo.repo')
validates:
value: ['presence']
actions:
add: ->
model = Travis.SshKey.create(id: @get('repo.id'))
@set('model', model)
@set('isEditing', true)
save: ->
return if @get('isSaving')
@set('isSaving', true)
if @isValid()
@get('model').save().then =>
@set('isEditing', false)
@set('isSaving', false)
, (xhr) =>
@set('isSaving', false)
if xhr.status == 422
@addErrorsFromResponse(JSON.parse(xhr.response)['errors'])
else
@set('isSaving', false)
delete: ->
return if @get('isDeleting')
@set('isDeleting', true)
deletingDone = => @set('isDeleting', false)
@get('model').deleteRecord().then(deletingDone, deletingDone).then =>
@set('model', null)
cancel: ->
if model = @get('model')
if model.get('isNew')
@set('model', null)
@set('isEditing', false)
edit: ->
@set('isEditing', true)

View File

@ -8,101 +8,6 @@ Ember.Handlebars.helper('mb', (size) ->
(size / 1024 / 1024).toFixed(2)
, 'size')
Travis.Tab = Ember.Object.extend
show: ->
@get('tabs').forEach( (t) -> t.hide() )
@set('visible', true)
hide: ->
@set('visible', false)
Travis.TabsView = Ember.View.extend
tabBinding: 'controller.tab'
tabsBinding: 'controller.tabs'
tabDidChange: (->
@activateTab(@get('tab'))
).observes('tab')
tabsDidChange: (->
tab = @get('tab')
if tab
@activateTab(tab)
else if @get('tabs.length')
@activateTab(@get('tabs.firstObject.id'))
).observes('tabs.length', 'tabs')
activateTab: (tabId) ->
tab = @get('tabs').findBy('id', tabId)
return unless tab
tab.show() unless tab.get('visible')
# TODO: remove hardcoded link
layout: Ember.Handlebars.compile(
'<ul class="tabs">' +
' {{#each tab in tabs}}' +
' <li {{bindAttr class="tab.visible:active"}}>' +
' <h5>{{#link-to "repo.settings.tab" tab.id}}{{tab.name}}{{/link-to}}</h5>' +
' </li>' +
' {{/each}}' +
'</ul>' +
'{{yield}}')
Travis.TabView = Ember.View.extend
attributeBindings: ['style']
style: (->
if !@get('tab.visible')
'display: none'
).property('tab.visible')
Ember.Handlebars.registerHelper('travis-tab', (id, name, options) ->
controller = this
controller.set('tabs', []) unless controller.get('tabs')
tab = Travis.Tab.create(id: id, name: name, tabs: controller.get('tabs'))
view = Travis.TabView.create(
controller: this
tab: tab
)
controller = this
Ember.run.schedule('afterRender', ->
if controller.get('tabs.length') == 0
tab.show()
controller.get('tabs').pushObject(tab)
)
Ember.Handlebars.helpers.view.call(this, view, options)
)
Ember.Handlebars.registerHelper('travis-tabs', (options) ->
template = options.fn
delete options.fn
@set('tabs', [])
view = Travis.TabsView.create(
controller: this
template: template
)
Ember.Handlebars.helpers.view.call(this, view, options)
)
Travis.FormSettingsView = Ember.View.extend Ember.TargetActionSupport,
target: Ember.computed.alias('controller')
actionContext: Ember.computed.alias('context'),
action: 'submit'
tagName: 'form'
submit: (event) ->
event.preventDefault()
@triggerAction()
Ember.LinkView.reopen
init: ->
@_super()
@ -114,18 +19,100 @@ Ember.LinkView.reopen
_trackEvent: (event) ->
event.preventDefault()
Ember.Handlebars.registerHelper('settings-form', (path, options) ->
if arguments.length == 1
options = path
path = 'settings'
FormFieldRowView = Ember.View.extend
invalid: Ember.computed.notEmpty('errors.[]')
classNameBindings: ['invalid']
classNames: 'field'
view = Travis.FormSettingsView.create(
template: options.fn
LabelView = Ember.View.extend(
tagName: 'label'
attributeBindings: ['for', 'accesskey', 'form']
classNameBindings: ['class']
)
Ember.Handlebars.registerHelper('label', (options) ->
view = LabelView
name = options.hash.for
if name
labels = @get('_labels')
unless labels
labels = Ember.Object.create()
@set('_labels', labels)
# for now I support only label + input in their own context
id = labels.get(name)
unless id
id = "#{name}-#{Math.round(Math.random() * 1000000)}"
labels.set(name, id)
options.hash.for = id
options.hashTypes.for = 'STRING'
options.hashContexts.for = this
Ember.Handlebars.helpers.view.call(this, view, options)
)
originalInputHelper = Ember.Handlebars.helpers.input
Ember.Handlebars.registerHelper('input', (options) ->
# for now I can match label only with the property name
# passed here matches the label
name = (options.hash.value || options.hash.checked)
id = options.hash.id
# generate id only if it's not given
if name && !name.match(/\./) && !id
labels = @get('_labels')
unless labels
labels = Ember.Object.create()
@set('_labels', labels)
# for now I support only label + input in their own context
id = labels.get(name)
unless id
id = "#{name}-#{Math.round(Math.random() * 1000000)}"
labels.set(name, id)
options.hash.id = id
options.hashTypes.id = 'STRING'
options.hashContexts.id = this
originalInputHelper.call(this, options)
)
Ember.Handlebars.registerHelper('travis-field', (name, options) ->
errors = @get('errors').for(name)
template = options.fn
delete options.fn
view = FormFieldRowView.create(
controller: this
settingsPath: path
template: template
errors: errors
name: name
classNameBindings: ['name']
)
delete options.fn
Ember.Handlebars.helpers.view.call(this, view, options)
)
Travis.ErrorsView = Ember.View.extend
tagName: 'span'
template: Ember.Handlebars.compile("{{#each view.errors}}{{message}}{{/each}}")
classNames: ['error']
classNameBindings: ['codes']
codes: (->
@get('errors').mapBy('code')
).property('@errors')
Ember.Handlebars.helper('travis-errors', (name, options) ->
errors = @get('errors').for(name)
view = Travis.ErrorsView.create(
controller: this
errors: errors
)
Ember.Handlebars.helpers.view.call(this, view, options)
)

View File

@ -13,4 +13,5 @@ require 'models/repo'
require 'models/request'
require 'models/user'
require 'models/worker'
require 'models/env_var'
require 'models/ssh_key'

View File

@ -0,0 +1,16 @@
require 'travis/model'
Travis.EnvVar = Travis.Model.extend
name: Ember.attr('string')
value: Ember.attr('string')
public: Ember.attr('boolean')
repo: Ember.belongsTo('Travis.Repo', key: 'repository_id')
isPropertyLoaded: (key) ->
if key == 'value'
return true
else
@_super(key)

View File

@ -23,6 +23,29 @@ require 'travis/model'
}
).property('lastBuildId', 'lastBuildNumber')
sshKey: (->
Travis.SshKey.find(@get('id'))
)
envVars: (->
id = @get('id')
envVars = Travis.EnvVar.find repository_id: id
# TODO: move to controller
array = Travis.ExpandableRecordArray.create
type: Travis.EnvVar
content: Ember.A([])
array.load(envVars)
globalEnvVars = Ember.RecordArray.create({ modelClass: Travis.EnvVar, content: Ember.A([]) })
Travis.EnvVar.registerRecordArray(globalEnvVars)
array.observe(globalEnvVars, (envVar) -> envVar.get('isLoaded') && envVar.get('repo.id') == id )
array
).property()
allBuilds: (->
recordArray = Ember.RecordArray.create({ modelClass: Travis.Build, content: Ember.A([]) })
Travis.Build.registerRecordArray(recordArray)

View File

@ -0,0 +1,13 @@
Travis.SshKey = Travis.Model.extend
id: Ember.attr('string')
value: Ember.attr('string')
description: Ember.attr('string')
fingerprint: Ember.attr('string')
isPropertyLoaded: (key) ->
if key == 'value'
return true
else
@_super(key)

View File

@ -62,10 +62,11 @@ Travis.Router.map ->
@resource 'caches', path: '/caches' if Travis.config.caches_enabled
@resource 'request', path: '/requests/:request_id'
# this can't be nested in repo, because we want a set of different
# templates rendered for settings (for example no "current", "builds", ... tabs)
@resource 'repo.settings', path: '/:owner/:name/settings', ->
@route 'tab', path: ':tab'
@resource 'settings', ->
@route 'index', path: '/'
@resource 'env_vars', ->
@route 'new'
@resource 'ssh_key' if Travis.config.ssh_key_enabled
@route 'first_sync'
@route 'insufficient_oauth_permissions'
@ -380,24 +381,40 @@ Travis.AuthRoute = Travis.Route.extend
@transitionTo('index.current')
return true
Travis.RepoSettingsRoute = Travis.Route.extend
Travis.SettingsRoute = Travis.Route.extend
setupController: (controller, model) ->
# TODO: if repo is just a data hash with id and slug load it
# as incomplete record
model = Travis.Repo.find(model.id) if model && !model.get
@_super(controller, model)
@controllerFor('repo').activate('settings')
serialize: (repo) ->
slug = if repo.get then repo.get('slug') else repo.slug
[owner, name] = slug.split('/')
{ owner: owner, name: name }
model: (params) ->
slug = "#{params.owner}/#{params.name}"
Travis.Repo.fetchBySlug(slug)
afterModel: (repo) ->
# I'm using afterModel to fetch settings, because model is not always called.
# If link-to already provides a model, it will be just set as a route context.
Travis.SettingsIndexRoute = Travis.Route.extend
model: ->
repo = @modelFor('repo')
repo.fetchSettings().then (settings) ->
repo.set('settings', settings)
Travis.EnvVarsRoute = Travis.Route.extend
model: (params) ->
repo = @modelFor('repo')
repo.get('envVars.promise')
Travis.SshKeyRoute = Travis.Route.extend
model: (params) ->
repo = @modelFor('repo')
self = this
Travis.SshKey.fetch(repo.get('id')).then ( (result) -> result ), (xhr) ->
if xhr.status == 404
# if there is no model, just return null. I'm not sure if this is the
# best answer, maybe we should just redirect to different route, like
# ssh_key.new or ssh_key.no_key
return null
afterModel: (model, transition) ->
repo = @modelFor('repo')
Travis.ajax.get "/repositories/#{repo.get('id')}/key", (data) =>
@defaultKey = Ember.Object.create(fingerprint: data.fingerprint)
setupController: (controller, model) ->
@_super.apply this, arguments
if @defaultKey
controller.set('defaultKey', @defaultKey)
@defaultKey = null

View File

@ -35,7 +35,7 @@
<p class="description">{{hook.description}}</p>
<div class="controls">
{{#link-to "repo.settings" hook.repo class="repo-settings-icon tool-tip" title="Repository settings"}}{{/link-to}}
{{#link-to "settings" hook.repo class="repo-settings-icon tool-tip" title="Repository settings"}}{{/link-to}}
{{travis-switch action="toggle" target=hook toggleAutomatically="false"}}
</div>
</li>

View File

@ -0,0 +1,18 @@
{{outlet}}
<ul class="env-vars">
{{#each controller itemController="envVar"}}
<li class="env-var">
{{#if isEditing}}
{{partial 'env_vars/form'}}
{{else}}
<a href="#" class="edit-var" {{action "edit"}}>Edit</a>
<a href="#" {{action "delete"}} {{bind-attr class=":delete-var isDeleting:deleting"}}>
Delete
</a>
<span class="name">{{name}}</span>
<span {{bind-attr class=":value :value-display public::secure"}}>{{value}}</span>
{{/if}}
</li>
{{/each}}
</ul>

View File

@ -0,0 +1,26 @@
<form class="env-var" {{action "save" on="submit"}}>
{{#travis-field "name"}}
{{#label for="name" class="name"}}Name:{{/label}}
{{input value=name class="env-name" placeholder="Name"}} {{travis-errors "name"}}
{{/travis-field}}
<span class="equals">=</span>
{{#if showValueField}}
<div class="field value">
{{#label for="value" class="value"}}Value:{{/label}}
{{input value=value class="env-value" placeholder="Value"}}
</div>
{{else}}
<span class="value value-display secure">{{value}}</span>
{{/if}}
<div style="clear: both"></div>
<div class="field">
{{travis-switch active=public class="value"}}
{{#label for="secure" class="public"}}Display value in build logs{{/label}}
</div>
<div class="actions">
<input type="submit" {{bind-attr value=actionType class=":submit-env-var isSaving:saving" disabled=isSaving}} />
<span class="or">or</span>
<a href="#" class="cancel-env-var" {{action "cancel"}}>Cancel</a>
</div>
</form>

View File

@ -0,0 +1 @@
{{#link-to "env_vars.new" class="add-env-var"}}Add a new variable{{/link-to}}

View File

@ -0,0 +1 @@
{{partial 'env_vars/form'}}

View File

@ -57,6 +57,15 @@
{{/if}}
</h5>
</li>
<li id="tab_settings" {{bind-attr class="view.classSettings"}}>
<h5>
{{#if repo.slug}}
{{#link-to "settings" repo}}
Settings
{{/link-to}}
{{/if}}
</h5>
</li>
<li id="tab_requests" {{bind-attr class="view.classRequests"}}>
<h5>
{{#if repo.slug}}

View File

@ -13,7 +13,7 @@
{{/if}}
{{#if view.displaySettingsLink}}
<li>
{{#link-to "repo.settings" view.repo}}Settings{{/link-to}}
{{#link-to "settings" view.repo}}Settings{{/link-to}}
</li>
{{/if}}
<li>

View File

@ -0,0 +1,11 @@
<ul class="navigation">
<li>{{#link-to "settings.index"}}General Settings{{/link-to}}</li>
<li>{{#link-to "env_vars"}}Environment Variables{{/link-to}}</li>
{{#if Travis.config.ssh_key_enabled}}
<li>{{#link-to "ssh_key"}}SSH Key{{/link-to}}</li>
{{/if}}
</ul>
<div id="settings">
{{outlet}}
</div>

View File

@ -0,0 +1,25 @@
<form class='settings-form'>
<p class="settings-row">
Build only if .travis.yml is present
{{travis-switch action="save" active=settings.builds_only_with_travis_yml}}
</p>
<p class="settings-row">
Build pushes
{{travis-switch action="save" active=settings.build_pushes}}
</p>
<p class="settings-row">
Build pull requests
{{travis-switch action="save" active=settings.build_pull_requests}}
</p>
<p class="settings-row">
<p class="short-settings-element" {{bind-attr class="settings.maximum_number_of_builds_valid"}}>
{{input value=settings.maximum_number_of_builds size="4" pattern='/^[0-9]+$/'}}
</p>
<label>
Concurrent builds
</label>
</p>
</form>

View File

@ -0,0 +1 @@
<div class="loading"><span>Loading</span></div>

View File

@ -0,0 +1,30 @@
{{#if model}}
{{#if isEditing}}
{{partial "ssh_key/form"}}
{{else}}
<div class="ssh-key">
<p>SSH key is set.</p>
{{#if description}}
<div class="row">
<span class="label">Description:</label>
<span class="value">{{description}}</span>
</div>
{{/if}}
<div class="row">
<span class="label">Fingerprint:</label>
<span class="value">{{fingerprint}}</span>
</div>
<div class="actions">
<a href="#" {{action "delete"}} {{bind-attr class=":delete-ssh-key isDeleting:deleting"}}>
Delete
</a>
</div>
{{/if}}
{{else}}
<p>
You don't have any custom key set up.
The default key's fingerprint is <code>{{defaultKey.fingerprint}}</code>
</p>
<a {{action "add"}} href="#" class="add-ssh-key">Add a custom SSH key</a>
{{/if}}

View File

@ -0,0 +1,16 @@
<form class="ssh-key" {{action "save" on="submit"}}>
<div class="field">
<label>Description:</label>
{{input value=description}}
</div>
{{#travis-field "value"}}
<label class="value">Private Key:</label>
{{textarea value=value}} {{travis-errors "value"}}
{{/travis-field}}
<div class="actions">
<input type="submit" {{bind-attr value=actionType class=":submit-ssh-key isSaving:saving" disabled=isSaving}} />
<span class="or">or</span>
<a href="#" class="cancel-ssh-key" {{action "cancel"}}>Cancel</a>
</div>
</form>

View File

@ -79,6 +79,10 @@ Travis.reopen
'active display-inline' if @get('tab') == 'caches'
).property('tab')
classSettings: (->
'active display-inline' if @get('tab') == 'settings'
).property('tab')
classRequest: (->
'active display-inline' if @get('tab') == 'request'
).property('tab')

View File

@ -1,3 +1,5 @@
get = Ember.get
Travis.Adapter = Ember.RESTAdapter.extend
ajax: (url, params, method) ->
Travis.ajax.ajax(url, method || 'get', data: params)
@ -17,7 +19,7 @@ Travis.Adapter = Ember.RESTAdapter.extend
records.load(klass, dataToLoad)
@addToRecordArrays(records.get('content'))
buildURL: ->
buildURL: (klass, id, record) ->
@_super.apply(this, arguments).replace(/\.json$/, '')
didFind: (record, id, data) ->
@ -55,7 +57,6 @@ Travis.Adapter = Ember.RESTAdapter.extend
for record in records
record.constructor.addToRecordArrays(record)
sideload: (klass, data) ->
for name, records of data
records = [records] unless Ember.isArray(records)
@ -66,4 +67,35 @@ Travis.Adapter = Ember.RESTAdapter.extend
record = type.findFromCacheOrLoad(record)
@addToRecordArrays(record)
find: (record, id) ->
url = @buildURL(record.constructor, id, record)
self = this
@ajax(url).then (data) ->
self.didFind record, id, data
record
createRecord: (record) ->
url = @buildURL(record.constructor, undefined, record)
self = this
@ajax(url, record.toJSON(), "POST").then (data) ->
self.didCreateRecord record, data
record
deleteRecord: (record) ->
primaryKey = get(record.constructor, "primaryKey")
url = @buildURL(record.constructor, get(record, primaryKey), record)
self = this
@ajax(url, record.toJSON(), "DELETE").then (data) -> # TODO: Some APIs may or may not return data
self.didDeleteRecord record, data
return
saveRecord: (record) ->
primaryKey = get(record.constructor, 'primaryKey')
url = this.buildURL(record.constructor, get(record, primaryKey), record)
self = this
return this.ajax(url, record.toJSON(), "PATCH").then (data) ->
self.didSaveRecord(record, data)
return record

View File

@ -0,0 +1,10 @@
require 'travis/adapter'
get = Ember.get
Travis.EnvVarsAdapter = Travis.Adapter.extend
buildURL: (klass, id, record) ->
url = @_super.apply this, arguments
if record && (repo_id = get(record, 'repository_id') || get(record, 'repo.id'))
url = "#{url}?repository_id=#{repo_id}"
url

View File

@ -0,0 +1,10 @@
Travis.SshKeyAdapter = Travis.Adapter.extend
buildURL: (klass, id, record) ->
url = @_super.apply this, arguments
createRecord: (record) ->
url = @buildURL(record.constructor, record.get('id'), record)
self = this
@ajax(url, record.toJSON(), "PATCH").then (data) ->
self.didCreateRecord record, data
record

View File

@ -2,6 +2,23 @@ Travis.ExpandableRecordArray = Ember.RecordArray.extend
isLoaded: false
isLoading: false
promise: (->
console.log 'promise'
self = this
new Ember.RSVP.Promise (resolve, reject) ->
console.log 'inside promise'
observer = ->
console.log 'observer', self.get('isLoaded')
if self.get('isLoaded')
console.log 'resolve'
resolve(self)
self.removeObserver('isLoaded', observer)
true
unless observer()
self.addObserver 'isLoaded', observer
).property()
load: (array) ->
@set 'isLoading', true
self = this
@ -25,11 +42,17 @@ Travis.ExpandableRecordArray = Ember.RecordArray.extend
willChange: 'observedArrayWillChange'
didChange: 'observedArraydidChange'
observedArrayWillChange: (->)
observedArrayWillChange: (array, index, removedCount, addedCount) ->
removedObjects = array.slice index, index + removedCount
for object in removedObjects
@removeObject(object)
observedArraydidChange: (array, index, removedCount, addedCount) ->
addedObjects = array.slice index, index + addedCount
for object in addedObjects
if @get('filterWith').call this, object
# TODO: I'm not sure why deleted objects get here, but I'll just filter them
# for now
if !object.get('isDeleted') && @get('filterWith').call(this, object)
@pushObject(object) unless @contains(object)
pushObject: (record) ->

View File

@ -0,0 +1,87 @@
get = Ember.get
Error = Ember.Object.extend
message: (->
switch @get('code')
when 'blank' then "can't be blank"
when 'not_a_private_key' then "the key is not a valid private key"
else "unknown error"
).property('code')
FieldErrors = Ember.ArrayProxy.extend
add: (error) ->
@get('content').pushObject(error)
isValid: ->
@get('length') == 0
Errors = Ember.ArrayProxy.extend
for: (name) ->
fieldErrors = @findBy('name', name)
unless fieldErrors
fieldErrors = FieldErrors.create(name: name, content: [])
@get('content').pushObject(fieldErrors)
fieldErrors
add: (name, code) ->
@for(name).add(Error.create(name: name, code: code))
isValid: ->
@every (fieldErrors) -> fieldErrors.isValid()
clear: ->
@forEach (fieldErrors) -> fieldErrors.clear()
Validator = Ember.Object.extend
setError: (target) ->
target.get('errors').add(@get('name'), @get('code'))
isValid: (target) ->
name = @get('name')
@get('validator').call(target, get(target, name))
validate: (target) ->
unless @isValid(target)
@setError(target)
Travis.Validations = Ember.Mixin.create
init: ->
@_super.apply this, arguments
@validators = []
@set('errors', Errors.create(content: []))
if validations = @get('validates')
for field, properties of validations
for property in properties
@_addValidation(field, property)
_addValidation: (name, type) ->
observer = ->
@get('errors').for(name).clear()
@addObserver(name, this, observer)
@["_add#{type.capitalize()}Validator"].call(this, name)
_addPresenceValidator: (name) ->
@_addValidator name, "blank", (value) ->
!Ember.isBlank(value)
_addValidator: (name, code, validator) ->
@validators.pushObject(Validator.create(name: name, code: code, validator: validator))
validate: ->
@get('errors').clear()
for validator in @validators
validator.validate(this)
isValid: ->
@validate()
@get('errors').isValid()
clearValidations: ->
@get('errors').clear()
addErrorsFromResponse: (errors) ->
for error in errors
@get('errors').add(error.field, error.code)

View File

@ -81,6 +81,7 @@ $.extend Travis,
pusher_key: $('meta[name="travis.pusher_key"]').attr('value')
ga_code: $('meta[name="travis.ga_code"]').attr('value')
code_climate: $('meta[name="travis.code_climate"]').attr('value')
ssh_key_enabled: $('meta[name="travis.ssh_key_enabled"]').attr('value') == 'true'
code_climate_url: $('meta[name="travis.code_climate_url"]').attr('value')
caches_enabled: $('meta[name="travis.caches_enabled"]').attr('value') == 'true'
show_repos_hint: 'private'
@ -163,6 +164,8 @@ Ember.LinkView.reopen
require 'travis/ajax'
require 'travis/adapter'
require 'travis/adapters/env_vars'
require 'travis/adapters/ssh_key'
require 'routes'
require 'auth'
require 'controllers'

View File

@ -11,15 +11,15 @@ p.settings-row
position: relative
display: inline-block
float: left
width: 60px
height: 18px
width: 5em
height: 1.55em
margin: 20px 0 50px 0
padding: 5px 18px 5px 18px
padding: 0.42em 1.5em
background-color: #F5F5F5
border: 1px solid #E3E1E1
border-radius: 4px
line-height: 19px
font-size: 11px
line-height: 1.58em
font-size: 12px
color: #999999
cursor: pointer
text-align: right
@ -28,15 +28,13 @@ p.settings-row
position: absolute
top: 1px
left: 1px
width: 42px
height: 24px
width: 3.5em
height: 1.9em
background: #e9e9e7
border: 1px solid #d7d4d4
border-radius: 2px
.travis-switch.active
width: 60px
padding: 5px 18px 5px 18px
background-color: #607A83
border-radius: 4px
color: #ffffff
@ -45,8 +43,6 @@ p.settings-row
&:before
left: auto
right: 1px
width: 42px
height: 24px
border: 1px solid #9cafb5
span.on

View File

@ -9,6 +9,8 @@
margin-top: 5px
#hooks, #unadministerable-hooks
.travis-switch
font-size: 10px
// @include list-base
margin-top: 10px

View File

@ -1,33 +1,281 @@
form
margin: 20px
#settings
.settings-form
margin: 30px 0 30px 0
p.short-settings-element
display: inline-block
vertical-align: middle
margin: 0 10px 0 0
float: left
p.short-settings-element
display: inline-block
vertical-align: middle
margin: 0 10px 0 0
float: left
label
line-height: 30px
label
line-height: 30px
input
display: inline-block
float: left
width: 60px
height: 18px
padding: 5px 18px 5px 18px
background-color: #F5F5F5
border: 1px solid #E3E1E1
border-radius: 4px
line-height: 19px
font-size: 11px
color: #999999
text-align: center
.invalid
input
display: inline-block
float: left
width: 60px
height: 18px
padding: 5px 18px 5px 18px
background-color: #F5F5F5
border: 1px solid #E3E1E1
border-radius: 4px
line-height: 19px
font-size: 11px
color: #999999
text-align: center
.field.invalid
input, textarea
border-width: 1px
border-color: #ffb6c1
background: rgb(252, 227, 230)
form.env-var, form.ssh-key
margin-top: 20px
.field
padding-bottom: 10px
.field:after
visibility: hidden
display: block
font-size: 0
content: " "
clear: both
height: 0
label
display: inline-block
form.env-var
label
width: 40px
label.value
width: auto
line-height: 1.9em
.travis-switch.value
font-size: 10px
form.ssh-key
span.error
display: block
margin-left: 76px
label
width: 73px
label.value
float: left
textarea
margin-left: 3px
width: 580px
height: 250px
display: inline-block
padding: 0 5px 0 5px
background-color: #fff
border: 1px solid #ddd
line-height: 17px
font-size: 13px
color: #999999
text-align: left
span.error
display: inline-block
margin-left: 10px
color: #a80000
.env-var input[type=submit].saving, .ssh-key input[type=submit].saving,
.env-var .delete-var.deleting, .delete-ssh-key.deleting
background-color: #bbb
background-image: inline-image('ui/round-spinner.svg')
background-repeat: no-repeat
background-position: center
background-size: 20px
text-indent: -9999px
.env-var-name
span
font-size: 18px
font-weight: 600
color: #5e6872
span.value.value-display
font-size: 14px
background-color: #efefdf
border-radius: 4px
padding: 0 10px 0 10px
color: #838b8c
font-family: monospace
span.value.value-display.secure
background-image: inline-image('ui/secure.png')
background-position: 8px center
background-repeat: no-repeat
background-size: 0.5em
padding-left: 26px
width: 142px
.add-env-var, .add-ssh-key
color: #ffffff
background-color: #97a3aa
border-radius: 4px
padding: 8px 12px 8px 12px
margin-bottom: 20px
font-size: 13px
line-height: 55px
cursor: pointer
.add-env-var:hover, .add-ssh-key:hover
background-color: #7c878d
input
display: inline-block
width: 260px
height: 20px
padding: 0 5px 0 5px
background-color: #fff
border: 1px solid #ddd
line-height: 17px
font-size: 13px
color: #999999
text-align: left
form.env-var .actions
margin-bottom: 35px
input.submit-env-var, input.submit-ssh-key
color: #ffffff
background-color: #7ea35a
border-radius: 4px
font-size: 13px
width: 60px
height: 32px
text-align: center
margin: 10px 6px 0 0
cursor: pointer
.submit-env-var:hover, .submit-ssh-key:hover
background-color: #6f924f
.cancel-env-var, .cancel-ssh-key
margin-left: 3px
.cancel-env-var:hover, .cancel-ssh-key:hover
text-decoration: underline
input[type="checkbox"]
display: inline-block
vertical-align: middle
.edit-var
display: inline-block
margin: 6px 10px 7px 0
color: #ffffff
background-color: #bcbcbb
border-radius: 4px
padding: 4px 15px 4px 15px
font-size: 13px
cursor: pointer
.edit-var:hover
background-color: #a4a5a4
.delete-var, .delete-ssh-key
display: inline-block
margin: 6px 0 7px 0
color: #ffffff
background-color: #932828
border-radius: 4px
padding: 4px 15px 4px 15px
font-size: 13px
cursor: pointer
.delete-var:hover, .delete-ssh-key:hover
background-color: #820b0b
form.env-var
width: 100%
margin-top: 20px
margin-bottom: 30px
border-bottom: 1px solid #f1f1f1
label.name, label.value
display: none
label.public
width: 80%
.field.name, .field.value
float: left
padding-bottom: 0
height: 33px
line-height: 33px
span.equals
float: left
display: block
line-height: 33px
margin: 0 10px 0 14px
.field.name
width: 242px
margin-bottom: 10px
input
width: 97%
.field.value
width: 45%
input
width: 97%
.actions
margin-bottom: 10px
ul.env-vars
display: block
li.env-var
margin: 10px 40px 0 0
padding-bottom: 10px
overflow: auto
display: flex
display: -webkit-flex
display: -moz-flex
-webkit-align-items: center
-moz-align-items: center
align-items: center
justify-content: left
-webkit-justify-content: left
-moz-justify-content: left
border-bottom: 1px solid #F1F1F1
width: 100%
form.env-var
margin-bottom: 6px
border-bottom: none
.var
font-size: 13px
display: inline-block
margin-right: 10px
.row
.label
display: inline-block
border: 1px solid #ddd
font-size: 13px
margin-top: 4px
height: 20px
span.name, span.value
display: inline-block
line-height: 40px
position: relative
font-size: 15px
.equals
margin: 0 10px 0 14px
span.name
width: 120px
margin: 0 10px 0 20px
text-align: left
span.value
width: auto
text-align: left
min-width: 158px
max-width: 450px

View File

@ -67,11 +67,31 @@
.tab
margin-top: 20px
ul.navigation
margin: -18px 0 20px 0
height: 40px
line-height: 40px
border-bottom: 1px solid #EAEAEA
padding-left: 10px
li
display: inline-block
padding-right: 10px
a
color: #ACACAC
font-weight: 600
font-size: 14px
a.active
color: #55888E
#tab_build,
#tab_job,
#tab_request,
#tab_caches,
#tab_requests
#tab_caches,
#tab_settings
display: none
#profile

View File

@ -33,4 +33,5 @@ run Travis::Web::App.build(
root: File.expand_path('../public', __FILE__),
server_start: Time.now,
caches_enabled: ENV['CACHES_ENABLED']
ssh_key_enabled: ENV['SSH_KEY_ENABLED']
)

View File

@ -4,6 +4,7 @@
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800' rel='stylesheet' type='text/css'>
<meta charset="UTF-8">
<meta rel="travis.api_endpoint" href="https://api.travis-ci.org">
<meta name="travis.ssh_key_enabled" value="false">
<meta name="travis.pusher_key" value="5df8ac576dcccf4fd076">
<meta name="travis.ga_code" value="UA-24868285-1">
<meta name="travis.caches_enabled" value="false">