diff --git a/assets/images/ui/bubbles-spinner.svg b/assets/images/ui/bubbles-spinner.svg new file mode 100644 index 00000000..abea1057 --- /dev/null +++ b/assets/images/ui/bubbles-spinner.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ui/secure.png b/assets/images/ui/secure.png new file mode 100644 index 00000000..8bb37b0d Binary files /dev/null and b/assets/images/ui/secure.png differ diff --git a/assets/scripts/app/app.coffee b/assets/scripts/app/app.coffee index f866d741..6514083f 100644 --- a/assets/scripts/app/app.coffee +++ b/assets/scripts/app/app.coffee @@ -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') diff --git a/assets/scripts/app/controllers.coffee b/assets/scripts/app/controllers.coffee index 718b331b..d44c02a0 100644 --- a/assets/scripts/app/controllers.coffee +++ b/assets/scripts/app/controllers.coffee @@ -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' diff --git a/assets/scripts/app/controllers/env_var.coffee b/assets/scripts/app/controllers/env_var.coffee new file mode 100644 index 00000000..c0fe6179 --- /dev/null +++ b/assets/scripts/app/controllers/env_var.coffee @@ -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) diff --git a/assets/scripts/app/controllers/env_var_new.coffee b/assets/scripts/app/controllers/env_var_new.coffee new file mode 100644 index 00000000..2aac40e2 --- /dev/null +++ b/assets/scripts/app/controllers/env_var_new.coffee @@ -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) diff --git a/assets/scripts/app/controllers/env_vars.coffee b/assets/scripts/app/controllers/env_vars.coffee new file mode 100644 index 00000000..5c6d4d65 --- /dev/null +++ b/assets/scripts/app/controllers/env_vars.coffee @@ -0,0 +1 @@ +Travis.EnvVarsController = Ember.ArrayController.extend() diff --git a/assets/scripts/app/controllers/repo.coffee b/assets/scripts/app/controllers/repo.coffee index 79733236..4144b681 100644 --- a/assets/scripts/app/controllers/repo.coffee +++ b/assets/scripts/app/controllers/repo.coffee @@ -59,6 +59,9 @@ Travis.RepoController = Travis.Controller.extend viewRequest: -> @connectTab('request') + viewSettings: -> + @connectTab('settings') + lastBuildDidChange: -> Ember.run.scheduleOnce('data', this, @_lastBuildDidChange); diff --git a/assets/scripts/app/controllers/repo_settings.coffee b/assets/scripts/app/controllers/repo_settings.coffee new file mode 100644 index 00000000..3cf63526 --- /dev/null +++ b/assets/scripts/app/controllers/repo_settings.coffee @@ -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.') diff --git a/assets/scripts/app/controllers/ssh_key.coffee b/assets/scripts/app/controllers/ssh_key.coffee new file mode 100644 index 00000000..c177ae7c --- /dev/null +++ b/assets/scripts/app/controllers/ssh_key.coffee @@ -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) diff --git a/assets/scripts/app/helpers/handlebars.coffee b/assets/scripts/app/helpers/handlebars.coffee index 6d183b4a..c08285b2 100644 --- a/assets/scripts/app/helpers/handlebars.coffee +++ b/assets/scripts/app/helpers/handlebars.coffee @@ -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( - '' + - '{{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) ) diff --git a/assets/scripts/app/models.coffee b/assets/scripts/app/models.coffee index c2efe38d..937eb98b 100644 --- a/assets/scripts/app/models.coffee +++ b/assets/scripts/app/models.coffee @@ -13,4 +13,5 @@ require 'models/repo' require 'models/request' require 'models/user' require 'models/worker' - +require 'models/env_var' +require 'models/ssh_key' diff --git a/assets/scripts/app/models/env_var.coffee b/assets/scripts/app/models/env_var.coffee new file mode 100644 index 00000000..5ddd444c --- /dev/null +++ b/assets/scripts/app/models/env_var.coffee @@ -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) + + diff --git a/assets/scripts/app/models/repo.coffee b/assets/scripts/app/models/repo.coffee index 3f94fc93..91d12919 100644 --- a/assets/scripts/app/models/repo.coffee +++ b/assets/scripts/app/models/repo.coffee @@ -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) diff --git a/assets/scripts/app/models/ssh_key.coffee b/assets/scripts/app/models/ssh_key.coffee new file mode 100644 index 00000000..59f05b19 --- /dev/null +++ b/assets/scripts/app/models/ssh_key.coffee @@ -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) + + diff --git a/assets/scripts/app/routes.coffee b/assets/scripts/app/routes.coffee index b30fb1fe..c9dfe0a8 100644 --- a/assets/scripts/app/routes.coffee +++ b/assets/scripts/app/routes.coffee @@ -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 diff --git a/assets/scripts/app/templates/account.hbs b/assets/scripts/app/templates/account.hbs index 944503b8..675191a8 100644 --- a/assets/scripts/app/templates/account.hbs +++ b/assets/scripts/app/templates/account.hbs @@ -35,7 +35,7 @@

{{hook.description}}

- {{#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"}}
diff --git a/assets/scripts/app/templates/env_vars.hbs b/assets/scripts/app/templates/env_vars.hbs new file mode 100644 index 00000000..b3cc1cb2 --- /dev/null +++ b/assets/scripts/app/templates/env_vars.hbs @@ -0,0 +1,18 @@ +{{outlet}} + + diff --git a/assets/scripts/app/templates/env_vars/_form.hbs b/assets/scripts/app/templates/env_vars/_form.hbs new file mode 100644 index 00000000..3042cd7c --- /dev/null +++ b/assets/scripts/app/templates/env_vars/_form.hbs @@ -0,0 +1,26 @@ +
+ {{#travis-field "name"}} + {{#label for="name" class="name"}}Name:{{/label}} + {{input value=name class="env-name" placeholder="Name"}} {{travis-errors "name"}} + {{/travis-field}} + = + {{#if showValueField}} +
+ {{#label for="value" class="value"}}Value:{{/label}} + {{input value=value class="env-value" placeholder="Value"}} +
+ {{else}} + {{value}} + {{/if}} +
+
+ {{travis-switch active=public class="value"}} + {{#label for="secure" class="public"}}Display value in build logs{{/label}} +
+ +
+ + or + Cancel +
+
diff --git a/assets/scripts/app/templates/env_vars/index.hbs b/assets/scripts/app/templates/env_vars/index.hbs new file mode 100644 index 00000000..04eb7bef --- /dev/null +++ b/assets/scripts/app/templates/env_vars/index.hbs @@ -0,0 +1 @@ +{{#link-to "env_vars.new" class="add-env-var"}}Add a new variable{{/link-to}} diff --git a/assets/scripts/app/templates/env_vars/new.hbs b/assets/scripts/app/templates/env_vars/new.hbs new file mode 100644 index 00000000..1afee20f --- /dev/null +++ b/assets/scripts/app/templates/env_vars/new.hbs @@ -0,0 +1 @@ +{{partial 'env_vars/form'}} diff --git a/assets/scripts/app/templates/repos/show/tabs.hbs b/assets/scripts/app/templates/repos/show/tabs.hbs index 5221fec7..793bd65c 100644 --- a/assets/scripts/app/templates/repos/show/tabs.hbs +++ b/assets/scripts/app/templates/repos/show/tabs.hbs @@ -57,6 +57,15 @@ {{/if}} +
  • +
    + {{#if repo.slug}} + {{#link-to "settings" repo}} + Settings + {{/link-to}} + {{/if}} +
    +
  • {{#if repo.slug}} diff --git a/assets/scripts/app/templates/repos/show/tools.hbs b/assets/scripts/app/templates/repos/show/tools.hbs index f917795f..ed110ae6 100644 --- a/assets/scripts/app/templates/repos/show/tools.hbs +++ b/assets/scripts/app/templates/repos/show/tools.hbs @@ -13,7 +13,7 @@ {{/if}} {{#if view.displaySettingsLink}}
  • - {{#link-to "repo.settings" view.repo}}Settings{{/link-to}} + {{#link-to "settings" view.repo}}Settings{{/link-to}}
  • {{/if}}
  • diff --git a/assets/scripts/app/templates/settings.hbs b/assets/scripts/app/templates/settings.hbs new file mode 100644 index 00000000..9c9458e4 --- /dev/null +++ b/assets/scripts/app/templates/settings.hbs @@ -0,0 +1,11 @@ + + +
    + {{outlet}} +
    diff --git a/assets/scripts/app/templates/settings/index.hbs b/assets/scripts/app/templates/settings/index.hbs new file mode 100644 index 00000000..e34aa4c1 --- /dev/null +++ b/assets/scripts/app/templates/settings/index.hbs @@ -0,0 +1,25 @@ +
    +

    + Build only if .travis.yml is present + {{travis-switch action="save" active=settings.builds_only_with_travis_yml}} +

    + +

    + Build pushes + {{travis-switch action="save" active=settings.build_pushes}} +

    + +

    + Build pull requests + {{travis-switch action="save" active=settings.build_pull_requests}} +

    + +

    +

    + {{input value=settings.maximum_number_of_builds size="4" pattern='/^[0-9]+$/'}} +

    + +

    +
    diff --git a/assets/scripts/app/templates/settings/loading.hbs b/assets/scripts/app/templates/settings/loading.hbs new file mode 100644 index 00000000..0277cf2e --- /dev/null +++ b/assets/scripts/app/templates/settings/loading.hbs @@ -0,0 +1 @@ +
    Loading
    diff --git a/assets/scripts/app/templates/ssh_key.hbs b/assets/scripts/app/templates/ssh_key.hbs new file mode 100644 index 00000000..648b57ff --- /dev/null +++ b/assets/scripts/app/templates/ssh_key.hbs @@ -0,0 +1,30 @@ +{{#if model}} + {{#if isEditing}} + {{partial "ssh_key/form"}} + {{else}} +
    +

    SSH key is set.

    + {{#if description}} +
    + Description: + {{description}} +
    + {{/if}} +
    + Fingerprint: + {{fingerprint}} +
    + + {{/if}} +{{else}} +

    + You don't have any custom key set up. + The default key's fingerprint is {{defaultKey.fingerprint}} +

    + + Add a custom SSH key +{{/if}} diff --git a/assets/scripts/app/templates/ssh_key/_form.hbs b/assets/scripts/app/templates/ssh_key/_form.hbs new file mode 100644 index 00000000..0e35e09d --- /dev/null +++ b/assets/scripts/app/templates/ssh_key/_form.hbs @@ -0,0 +1,16 @@ +
    +
    + + {{input value=description}} +
    + {{#travis-field "value"}} + + {{textarea value=value}} {{travis-errors "value"}} + {{/travis-field}} + +
    + + or + Cancel +
    +
    diff --git a/assets/scripts/app/views/repo/show.coffee b/assets/scripts/app/views/repo/show.coffee index 487e8602..4bd5cc7c 100644 --- a/assets/scripts/app/views/repo/show.coffee +++ b/assets/scripts/app/views/repo/show.coffee @@ -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') diff --git a/assets/scripts/lib/travis/adapter.coffee b/assets/scripts/lib/travis/adapter.coffee index 7baf43b0..3ea2e657 100644 --- a/assets/scripts/lib/travis/adapter.coffee +++ b/assets/scripts/lib/travis/adapter.coffee @@ -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 + diff --git a/assets/scripts/lib/travis/adapters/env_vars.coffee b/assets/scripts/lib/travis/adapters/env_vars.coffee new file mode 100644 index 00000000..00eba0df --- /dev/null +++ b/assets/scripts/lib/travis/adapters/env_vars.coffee @@ -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 diff --git a/assets/scripts/lib/travis/adapters/ssh_key.coffee b/assets/scripts/lib/travis/adapters/ssh_key.coffee new file mode 100644 index 00000000..11a683b2 --- /dev/null +++ b/assets/scripts/lib/travis/adapters/ssh_key.coffee @@ -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 diff --git a/assets/scripts/lib/travis/expandable_record_array.coffee b/assets/scripts/lib/travis/expandable_record_array.coffee index 778b9f6b..4db4321e 100644 --- a/assets/scripts/lib/travis/expandable_record_array.coffee +++ b/assets/scripts/lib/travis/expandable_record_array.coffee @@ -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) -> diff --git a/assets/scripts/lib/travis/validations.coffee b/assets/scripts/lib/travis/validations.coffee new file mode 100644 index 00000000..f39d375f --- /dev/null +++ b/assets/scripts/lib/travis/validations.coffee @@ -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) diff --git a/assets/scripts/travis.coffee b/assets/scripts/travis.coffee index 726fad3b..9fb252ce 100644 --- a/assets/scripts/travis.coffee +++ b/assets/scripts/travis.coffee @@ -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' diff --git a/assets/styles/components/travis-switch.sass b/assets/styles/components/travis-switch.sass index ccf1f16a..eeda994f 100644 --- a/assets/styles/components/travis-switch.sass +++ b/assets/styles/components/travis-switch.sass @@ -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 diff --git a/assets/styles/profile/hooks.sass b/assets/styles/profile/hooks.sass index 2411c8ee..aeaeb318 100644 --- a/assets/styles/profile/hooks.sass +++ b/assets/styles/profile/hooks.sass @@ -9,6 +9,8 @@ margin-top: 5px #hooks, #unadministerable-hooks + .travis-switch + font-size: 10px // @include list-base margin-top: 10px diff --git a/assets/styles/settings.sass b/assets/styles/settings.sass index db7379dd..727ffcc2 100644 --- a/assets/styles/settings.sass +++ b/assets/styles/settings.sass @@ -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 diff --git a/assets/styles/tabs.sass b/assets/styles/tabs.sass index c4e19229..c118bab6 100644 --- a/assets/styles/tabs.sass +++ b/assets/styles/tabs.sass @@ -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 diff --git a/config.ru b/config.ru index 5210c220..b693786f 100644 --- a/config.ru +++ b/config.ru @@ -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'] ) diff --git a/public/index.html b/public/index.html index 204e31ee..e5348ebe 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,7 @@ +