Get UI for activating github build triggers in place and working. Note that the actual server-side activation is still not done (but the proper method is invoked)

This commit is contained in:
Joseph Schorr 2014-02-20 18:57:49 -05:00
parent c494c889f5
commit 5519d93a64
8 changed files with 388 additions and 15 deletions

View file

@ -25,8 +25,11 @@ from auth.permissions import (ReadRepositoryPermission,
CreateRepositoryPermission,
AdministerOrganizationPermission,
OrganizationMemberPermission,
ViewTeamPermission)
ViewTeamPermission,
UserPermission
)
from endpoints.common import common_login, get_route_data
from endpoints.trigger import BuildTrigger, TriggerActivationException
from util.cache import cache_control
from datetime import datetime, timedelta
@ -1120,11 +1123,14 @@ def get_repo(namespace, repository):
def trigger_view(trigger):
if trigger and trigger.uuid:
config_dict = json.loads(trigger.config)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
return {
'service': trigger.service.name,
'config': json.loads(trigger.config),
'config': config_dict,
'id': trigger.uuid,
'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict)
}
return None
@ -1358,6 +1364,44 @@ def get_build_trigger(namespace, repository, trigger_uuid):
abort(403) # Permission denied
@api.route('/repository/<path:repository>/trigger/<trigger_uuid>/activate',
methods=['POST'])
@api_login_required
@parse_repository_name
def activate_build_trigger(namespace, repository, trigger_uuid):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
abort(404)
return
handler = BuildTrigger.get_trigger_for_service(trigger.service.name)
existing_config_dict = json.loads(trigger.config)
if handler.is_active(existing_config_dict):
abort(400)
return
user_permission = UserPermission(trigger.connected_user.username)
if user_permission.can():
new_config_dict = request.get_json()
try:
handler.activate(trigger.auth_token, new_config_dict)
except TriggerActivationException as e:
abort(400, message = e.msg)
return
# Save the updated config.
trigger.config = json.dumps(new_config_dict)
trigger.save()
return jsonify(trigger_view(trigger))
abort(403) # Permission denied
@api.route('/repository/<path:repository>/trigger/<trigger_uuid>/builds',
methods=['GET'])
@api_login_required
@ -1374,6 +1418,31 @@ def list_trigger_recent_builds(namespace, repository, trigger_uuid):
abort(403) # Permission denied
@api.route('/repository/<path:repository>/trigger/<trigger_uuid>/sources',
methods=['GET'])
@api_login_required
@parse_repository_name
def list_trigger_build_sources(namespace, repository, trigger_uuid):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
abort(404)
user_permission = UserPermission(trigger.connected_user.username)
if user_permission.can():
trigger_handler = BuildTrigger.get_trigger_for_service(trigger.service.name)
return jsonify({
'sources': trigger_handler.list_build_sources(trigger.auth_token)
})
abort(403) # Permission denied
@api.route('/repository/<path:repository>/trigger/', methods=['GET'])
@api_login_required
@parse_repository_name

View file

@ -123,10 +123,10 @@ def attach_github_build_trigger(namespace, repository):
msg = 'Invalid repository: %s/%s' % (namespace, repository)
abort(404, message=msg)
model.create_build_trigger(repo, 'github', token, current_user.db_user())
trigger = model.create_build_trigger(repo, 'github', token, current_user.db_user())
admin_path = '%s/%s/%s' % (namespace, repository, 'admin')
full_url = url_for('web.repository', path=admin_path) + '?tab=trigger'
full_url = url_for('web.repository', path=admin_path) + '?tab=trigger&new_trigger=' + trigger.uuid
logger.debug('Redirecting to full url: %s' % full_url)
return redirect(full_url)
abort(403)
abort(403)

View file

@ -21,10 +21,12 @@ CHUNK_SIZE = 512 * 1024
class BuildArchiveException(Exception):
pass
class InvalidServiceException(Exception):
pass
class TriggerActivationException(Exception):
pass
class BuildTrigger(object):
def __init__(self):
@ -43,6 +45,18 @@ class BuildTrigger(object):
"""
raise NotImplementedError
def is_active(self, config):
"""
Returns True if the current build trigger is active. Inactive means further setup is needed.
"""
raise NotImplementedError
def activate(self, auth_token, config):
"""
Activates the trigger for the service, with the given new configuration.
"""
raise NotImplementedError
@classmethod
def service_name(cls):
"""
@ -73,15 +87,43 @@ class GithubBuildTrigger(BuildTrigger):
def service_name(cls):
return 'github'
def is_active(self, config):
return 'build_source' in config and len(config['build_source']) > 0
def activate(self, auth_token, config):
# TODO: Add the callback web hook to the github repository.
pass
def list_build_sources(self, auth_token):
gh_client = self._get_client(auth_token)
usr = gh_client.get_user()
repo_list = [repo.full_name for repo in usr.get_repos()]
for org in usr.get_orgs():
repo_list.extend((repo.full_name for repo in org.get_repos()))
personal = {
'personal': True,
'repos': [repo.full_name for repo in usr.get_repos()],
'info': {
'name': usr.login,
'avatar_url': usr.avatar_url,
}
}
return repo_list
repos_by_org = [personal]
for org in usr.get_orgs():
repo_list = []
for repo in org.get_repos():
repo_list.append(repo.full_name)
repos_by_org.append({
'personal': False,
'repos': repo_list,
'info': {
'name': org.name,
'avatar_url': org.avatar_url
}
})
return repos_by_org
def handle_trigger_request(self, request, auth_token, config):
payload = request.get_json()
@ -110,4 +152,4 @@ class GithubBuildTrigger(BuildTrigger):
logger.debug('Successfully prepared job')
return dockerfile_id, branch_name, commit_id
return dockerfile_id, branch_name, commit_id

View file

@ -3247,4 +3247,61 @@ pre.command:before {
.label.MAINTAINER {
border-color: #aaa !important;
}
.dropdown-select {
margin: 10px;
position: relative;
}
.dropdown-select .dropdown-icon {
position: absolute;
top: 6px;
left: 6px;
z-index: 2;
}
.dropdown-select .dropdown-icon.none-icon {
top: 10px;
left: 8px;
font-size: 20px;
color: #ccc;
}
.dropdown-select .lookahead-input {
padding-left: 32px;
}
.dropdown-select .twitter-typeahead {
display: block !important;
}
.dropdown-select .twitter-typeahead .tt-hint {
padding-left: 32px;
}
.dropdown-select .dropdown {
position: absolute;
right: 0px;
top: 0px;
}
.dropdown-select .dropdown button.dropdown-toggle {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.trigger-setup-github-element .github-org-icon {
width: 20px;
margin-right: 8px;
vertical-align: middle;
}
.trigger-setup-github-element li.github-repo-listing i {
margin-right: 10px;
margin-left: 6px;
}
.trigger-setup-github-element li.github-org-header {
padding-left: 6px;
}

View file

@ -0,0 +1,30 @@
<div class="trigger-setup-github-element">
<div ng-show="loading">
<span class="quay-spinner" style="vertical-align: middle; margin-right: 10px"></span>
Loading Repository List
</div>
<div ng-show="!loading">
<div style="margin-bottom: 18px">Please choose the GitHub repository that will trigger the build:</div>
<div class="dropdown-select">
<div class="current-item">
<i ng-show="!currentRepo" class="fa fa-github fa-lg dropdown-icon none-icon"></i>
<img ng-show="currentRepo" class="dropdown-icon github-org-icon" ng-src="{{ currentRepo.avatar_url ? currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
<input type="text" class="lookahead-input form-control" placeholder="Select a Repository"></input>
</div>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu pull-right" role="menu">
<li ng-repeat-start="org in orgs" role="presentation" class="dropdown-header github-org-header">
<img ng-src="{{ org.info.avatar_url }}" class="github-org-icon">{{ org.info.name }}
</li>
<li ng-repeat="repo in org.repos" class="gtihub-repo-listing">
<a href="javascript:void(0)" ng-click="selectRepo(repo, org)"><i class="fa fa-github fa-lg"></i> {{ repo }}</a>
</li>
<li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li>
</div>
</div>
</div>
</div>

View file

@ -2544,6 +2544,96 @@ quayApp.directive('triggerDescription', function () {
});
quayApp.directive('triggerSetupGithub', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-setup-github.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger'
},
controller: function($scope, $element, ApiService) {
$scope.setupReady = false;
$scope.loading = true;
var input = $($element).find('.lookahead-input');
$scope.clearSelectedRepo = function() {
$scope.currentRepo = null;
$scope.trigger.$ready = false;
};
$scope.selectRepo = function(repo, org) {
$(input).val(repo);
$scope.selectRepoInternal(repo, org);
};
$scope.selectRepoInternal = function(repo, org) {
$scope.currentRepo = {
'name': repo,
'avatar_url': org['info']['avatar_url']
};
$scope.trigger['config'] = {
'build_source': repo
};
$scope.trigger.$ready = true;
};
var setupTypeahead = function() {
var repos = [];
for (var i = 0; i < $scope.orgs.length; ++i) {
var org = $scope.orgs[i];
var orepos = org['repos'];
for (var j = 0; j < orepos.length; ++j) {
repos.push({'name': orepos[j], 'org': org, 'value': orepos[j]});
}
}
$(input).typeahead({
name: 'repos-' + $scope.trigger.id,
local: repos,
template: function (datum) {
template = datum['name'];
return template;
}
});
};
var loadSources = function() {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
ApiService.listTriggerBuildSources(null, params).then(function(resp) {
$scope.orgs = resp['sources'];
setupTypeahead();
$scope.loading = false;
});
};
loadSources();
$(input).on('input', function(e) {
$scope.$apply(function() {
$scope.clearSelectedRepo();
});
});
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
$scope.selectRepoInternal(datum.repo, datum.org);
});
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildLogCommand', function () {
var directiveDefinitionObject = {
priority: 0,
@ -3115,8 +3205,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
var tabName = e.target.getAttribute('data-target').substr(1);
$rootScope.$apply(function() {
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
var data = isDefaultTab ? {} : {'tab': tabName};
$location.search(data);
var newSearch = $.extend($location.search(), {});
if (isDefaultTab) {
delete newSearch['tab'];
} else {
newSearch['tab'] = tabName;
}
$location.search(newSearch);
});
e.preventDefault();

View file

@ -1409,10 +1409,60 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
$scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) {
$scope.triggers = resp.triggers;
// Check to see if we need to setup any trigger.
var newTriggerId = $routeParams.new_trigger;
if (newTriggerId) {
for (var i = 0; i < $scope.triggers.length; ++i) {
var trigger = $scope.triggers[i];
if (trigger['id'] == newTriggerId && !trigger['is_active']) {
$scope.setupTrigger(trigger);
break;
}
}
}
return $scope.triggers;
});
};
$scope.setupTrigger = function(trigger) {
$scope.triggerSetupReady = false;
$scope.currentSetupTrigger = trigger;
$('#setupTriggerModal').modal({});
};
$scope.finishSetupTrigger = function(trigger) {
$('#setupTriggerModal').modal('hide');
$scope.currentSetupTrigger = null;
var params = {
'repository': namespace + '/' + name,
'trigger_uuid': trigger.id
};
ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) {
trigger['is_active'] = true;
}, function(resp) {
bootbox.dialog({
"message": resp['message'] || 'The build trigger setup could not be completed',
"title": "Could not activate build trigger",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
$scope.cancelSetupTrigger = function() {
$('#setupTriggerModal').modal('hide');
$scope.deleteTrigger($scope.currentSetupTrigger);
$scope.currentSetupTrigger = null;
};
$scope.deleteTrigger = function(trigger) {
var params = {
'repository': namespace + '/' + name,

View file

@ -230,10 +230,14 @@
<tbody>
<tr ng-repeat="trigger in triggers">
<td>
<div class="trigger-description" trigger="trigger"></div>
<div ng-show="!trigger.is_active" style="color: #444;">
<span class="quay-spinner" style="vertical-align: middle; margin-right: 6px;"></span>
Setting up trigger
</div>
<div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></div>
</td>
<td style="white-space: nowrap;">
<div class="dropdown" style="display: inline-block">
<div class="dropdown" style="display: inline-block" ng-visible="trigger.is_active">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build History" bs-tooltip="tooltip.title" data-container="body"
ng-click="loadTriggerBuildHistory(trigger)">
<i class="fa fa-tasks"></i>
@ -257,6 +261,7 @@
<b class="caret"></b>
</button>
<ul class="dropdown-menu dropdown-menu-right pull-right">
<li><a href="javascript:void(0)" ng-click="setupTrigger(trigger)" ng-show="!trigger.is_active"><i class="fa fa-wrench"></i>Resume Setup</a></li>
<li><a href="javascript:void(0)" ng-click="deleteTrigger(trigger)"><i class="fa fa-times"></i>Delete Trigger</a></li>
</ul>
</div>
@ -337,6 +342,30 @@
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="setupTriggerModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Setup new build trigger</h4>
</div>
<div class="modal-body">
<div class="trigger-description-element" ng-switch on="currentSetupTrigger.service">
<div ng-switch-when="github">
<div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-disabled="!currentSetupTrigger.$ready" ng-click="finishSetupTrigger(currentSetupTrigger)">Finished</button>
<button type="button" class="btn btn-default" ng-click="cancelSetupTrigger(currentSetupTrigger)">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
<div class="modal-dialog">