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:
parent
c494c889f5
commit
5519d93a64
8 changed files with 388 additions and 15 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
30
static/directives/trigger-setup-github.html
Normal file
30
static/directives/trigger-setup-github.html
Normal 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>
|
100
static/js/app.js
100
static/js/app.js
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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">×</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">
|
||||
|
|
Reference in a new issue