Redo the UI for the trigger setup dialog and add the ability for github triggers to be filtered using a regex on their branch name.

This commit is contained in:
Joseph Schorr 2014-10-14 15:46:35 -04:00
parent 37aa70c28e
commit c3171a2690
10 changed files with 597 additions and 226 deletions

View file

@ -317,7 +317,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
if not found_repository: if not found_repository:
return { return {
'status': 'error', 'status': 'error',
'message': 'Repository "%s" was not found' % (base_image) 'message': 'Repository "%s" referenced by the Dockerfile was not found' % (base_image)
} }
# If the repository is private and the user cannot see that repo, then # If the repository is private and the user cannot see that repo, then
@ -326,7 +326,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
if found_repository.visibility.name != 'public' and not can_read: if found_repository.visibility.name != 'public' and not can_read:
return { return {
'status': 'error', 'status': 'error',
'message': 'Repository "%s" was not found' % (base_image) 'message': 'Repository "%s" referenced by the Dockerfile was not found' % (base_image)
} }
# Check to see if the repository is public. If not, we suggest the # Check to see if the repository is public. If not, we suggest the
@ -450,18 +450,18 @@ class BuildTriggerFieldValues(RepositoryParamResource):
""" Custom verb to fetch a values list for a particular field name. """ """ Custom verb to fetch a values list for a particular field name. """
@require_repo_admin @require_repo_admin
@nickname('listTriggerFieldValues') @nickname('listTriggerFieldValues')
def get(self, namespace, repository, trigger_uuid, field_name): def post(self, namespace, repository, trigger_uuid, field_name):
""" List the field values for a custom run field. """ """ List the field values for a custom run field. """
try: try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid) trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
config = request.get_json() or json.loads(trigger.config)
user_permission = UserAdminPermission(trigger.connected_user.username) user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can(): if user_permission.can():
trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
values = trigger_handler.list_field_values(trigger.auth_token, json.loads(trigger.config), values = trigger_handler.list_field_values(trigger.auth_token, config, field_name)
field_name)
if values is None: if values is None:
raise NotFound() raise NotFound()

View file

@ -70,7 +70,7 @@ def create_user():
abort(400, 'User creation is disabled. Please speak to your administrator.') abort(400, 'User creation is disabled. Please speak to your administrator.')
user_data = request.get_json() user_data = request.get_json()
if not 'username' in user_data: if not user_data or not 'username' in user_data:
abort(400, 'Missing username') abort(400, 'Missing username')
username = user_data['username'] username = user_data['username']

View file

@ -3,6 +3,7 @@ import io
import os.path import os.path
import tarfile import tarfile
import base64 import base64
import re
from github import Github, UnknownObjectException, GithubException from github import Github, UnknownObjectException, GithubException
from tempfile import SpooledTemporaryFile from tempfile import SpooledTemporaryFile
@ -229,13 +230,35 @@ class GithubBuildTrigger(BuildTrigger):
return repos_by_org return repos_by_org
def matches_branch(self, branch_name, regex):
if not regex:
return False
m = regex.match(branch_name)
if not m:
return False
return len(m.group(0)) == len(branch_name)
def list_build_subdirs(self, auth_token, config): def list_build_subdirs(self, auth_token, config):
gh_client = self._get_client(auth_token) gh_client = self._get_client(auth_token)
source = config['build_source'] source = config['build_source']
try: try:
repo = gh_client.get_repo(source) repo = gh_client.get_repo(source)
default_commit = repo.get_branch(repo.default_branch or 'master').commit
# Find the first matching branch.
branches = None
if 'branch_regex' in config:
try:
regex = re.compile(config['branch_regex'])
branches = [branch.name for branch in repo.get_branches()
if self.matches_branch(branch.name, regex)]
except:
pass
branches = branches or [repo.default_branch or 'master']
default_commit = repo.get_branch(branches[0]).commit
commit_tree = repo.get_git_tree(default_commit.sha, recursive=True) commit_tree = repo.get_git_tree(default_commit.sha, recursive=True)
return [os.path.dirname(elem.path) for elem in commit_tree.tree return [os.path.dirname(elem.path) for elem in commit_tree.tree
@ -339,6 +362,16 @@ class GithubBuildTrigger(BuildTrigger):
commit_sha = payload['head_commit']['id'] commit_sha = payload['head_commit']['id']
commit_message = payload['head_commit'].get('message', '') commit_message = payload['head_commit'].get('message', '')
if 'branch_regex' in config:
try:
regex = re.compile(config['branch_regex'])
except:
regex = re.compile('.*')
branch = ref.split('/')[-1]
if not self.matches_branch(branch, regex):
raise SkipRequestException()
if should_skip_commit(commit_message): if should_skip_commit(commit_message):
raise SkipRequestException() raise SkipRequestException()

View file

@ -4105,6 +4105,27 @@ pre.command:before {
border-bottom-left-radius: 0px; border-bottom-left-radius: 0px;
} }
.trigger-setup-github-element .branch-reference.not-match {
color: #ccc !important;
}
.trigger-setup-github-element .branch-reference.not-match a {
color: #ccc !important;
text-decoration: line-through;
}
.trigger-setup-github-element .branch-filter {
white-space: nowrap;
}
.trigger-setup-github-element .branch-filter span {
display: inline-block;
}
.trigger-setup-github-element .selected-info {
margin-bottom: 20px;
}
.trigger-setup-github-element .github-org-icon { .trigger-setup-github-element .github-org-icon {
width: 20px; width: 20px;
margin-right: 8px; margin-right: 8px;
@ -4120,6 +4141,45 @@ pre.command:before {
padding-left: 6px; padding-left: 6px;
} }
.trigger-setup-github-element .matching-branches {
margin: 0px;
padding: 0px;
margin-left: 10px;
display: inline-block;
}
.trigger-setup-github-element .matching-branches li:before {
content: "\f126";
font-family: FontAwesome;
}
.trigger-setup-github-element .matching-branches li {
list-style: none;
display: inline-block;
margin-left: 10px;
}
.setup-trigger-directive-element .dockerfile-found-content {
margin-left: 32px;
}
.setup-trigger-directive-element .dockerfile-found-content:before {
content: "\f071";
font-family: FontAwesome;
color: rgb(255, 194, 0);
position: absolute;
top: 0px;
left: 0px;
font-size: 20px;
}
.setup-trigger-directive-element .dockerfile-found {
position: relative;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.slideinout { .slideinout {
-webkit-transition:0.5s all; -webkit-transition:0.5s all;
transition:0.5s linear all; transition:0.5s linear all;
@ -4127,7 +4187,7 @@ pre.command:before {
position: relative; position: relative;
height: 75px; height: 32px;
opacity: 1; opacity: 1;
} }

View file

@ -8,102 +8,110 @@
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Setup new build trigger</h4> <h4 class="modal-title">Setup new build trigger</h4>
</div> </div>
<div class="modal-body" ng-show="activating"> <div class="modal-body" ng-show="currentView == 'activating'">
<span class="quay-spinner"></span> Setting up trigger... <span class="quay-spinner"></span> Setting up trigger...
</div> </div>
<div class="modal-body" ng-show="!activating"> <div class="modal-body" ng-show="currentView != 'activating'">
<!-- Trigger-specific setup --> <!-- Trigger-specific setup -->
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service"> <div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
<div ng-switch-when="github"> <div ng-switch-when="github">
<div class="trigger-setup-github" repository="repository" trigger="trigger" <div class="trigger-setup-github" repository="repository" trigger="trigger"
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div> analyze="checkAnalyze(isValid)"></div>
</div> </div>
</div> </div>
<!-- Pull information --> <!-- Loading pull information -->
<div class="trigger-option-section" ng-show="showPullRequirements"> <div ng-show="currentView == 'analyzing'">
<div ng-show="!pullRequirements">
<span class="quay-spinner"></span> Checking pull credential requirements... <span class="quay-spinner"></span> Checking pull credential requirements...
</div> </div>
<div ng-show="pullRequirements"> <!-- Pull information -->
<div class="alert alert-danger" ng-if="pullRequirements.status == 'error'"> <div class="trigger-option-section" ng-show="currentView == 'analyzed'">
<!-- Messaging -->
<div class="alert alert-danger" ng-if="pullInfo.analysis.status == 'error'">
{{ pullInfo.analysis.message }}
</div>
<div class="alert alert-warning" ng-if="pullInfo.analysis.status == 'warning'">
{{ pullRequirements.message }} {{ pullRequirements.message }}
</div> </div>
<div class="alert alert-warning" ng-if="pullRequirements.status == 'warning'"> <div class="dockerfile-found" ng-if="pullInfo.analysis.is_public === false">
{{ pullRequirements.message }} <div class="dockerfile-found-content">
</div> A robot account is <strong>required</strong> for this build trigger because
<div class="alert alert-success" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.is_public === false">
The the
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a> <a href="{{ pullInfo.analysis.dockerfile_url }}" ng-if="pullInfo.analysis.dockerfile_url" target="_blank">
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span> Dockerfile found
depends on the private <span class="registry-name"></span> repository </a>
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank"> <span ng-if="!pullInfo.analysis.dockerfile_url">Dockerfile found</span>
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
</a> which requires pulls from the private <span class="registry-name"></span> repository
a robot account for pull access, because it is marked <strong>private</strong>.
<a href="/repository/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}" target="_blank">
{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}
</a>
</div> </div>
</div> </div>
<div ng-show="pullRequirements"> <div style="margin-bottom: 12px">Please select the credentials to use when pulling the base image:</div>
<table style="width: 100%;">
<tr>
<td style="width: 162px">
<span class="context-tooltip" data-title="The credentials given to 'docker pull' in the builder for pulling images"
style="margin-bottom: 10px" bs-tooltip>
docker pull Credentials:
</span>
</td>
<td>
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;"> <div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repository.namespace }}</strong> <strong>Note:</strong> In order to set pull credentials for a build trigger, you must be an
Administrator of the namespace <strong>{{ repository.namespace }}</strong>
</div> </div>
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repository.namespace)">
<!-- Namespace admin -->
<div ng-show="isNamespaceAdmin(repository.namespace)">
<!-- Select credentials -->
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default" <button type="button" class="btn btn-default"
ng-class="publicPull ? 'active btn-info' : ''" ng-click="setPublicPull(true)">None</button> ng-class="pullInfo.is_public ? 'active btn-info' : ''"
ng-click="pullInfo.is_public = true">
None
</button>
<button type="button" class="btn btn-default" <button type="button" class="btn btn-default"
ng-class="publicPull ? '' : 'active btn-info'" ng-click="setPublicPull(false)"> ng-class="pullInfo.is_public ? '' : 'active btn-info'"
ng-click="pullInfo.is_public = false">
<i class="fa fa-wrench"></i> <i class="fa fa-wrench"></i>
Robot account Robot account
</button> </button>
</div> </div>
</td>
</tr>
</table>
<table style="width: 100%;"> <!-- Robot Select -->
<tr ng-show="!publicPull"> <div ng-show="!pullInfo.is_public" style="margin-top: 10px">
<td>
<div class="entity-search" namespace="repository.namespace" <div class="entity-search" namespace="repository.namespace"
placeholder="'Select robot account for pulling...'" placeholder="'Select robot account for pulling...'"
current-entity="pullEntity" current-entity="pullInfo.pull_entity"
allowed-entities="['robot']"></div> allowed-entities="['robot']"></div>
<div class="alert alert-info" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.robots.length" <div ng-if="pullInfo.analysis.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
style="margin-top: 20px; margin-bottom: 0px;"> <strong>Note</strong>: We've automatically selected robot account
Note: We've automatically selected robot account <span class="entity-reference" entity="pullInfo.analysis.robots[0]"></span>,
<span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the private since it has access to the private repository.
repository.
</div> </div>
<div class="alert alert-warning" <div ng-if="!pullInfo.analysis.robots.length && pullInfo.analysis.name"
ng-if="pullRequirements.status == 'analyzed' && !pullRequirements.robots.length && pullRequirements.name"
style="margin-top: 20px; margin-bottom: 0px;"> style="margin-top: 20px; margin-bottom: 0px;">
Note: No robot account currently has access to the private repository. Please create one and/or assign access in the <strong>Note</strong>: No robot account currently has access to the private repository. Please create one and/or assign access in the
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's <a href="/repository/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}/admin" target="_blank">
admin panel</a>. repository's admin panel.
</a>
</div>
</div> </div>
</td>
</tr>
</table>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" ng-disabled="!state.stepValid"
ng-click="nextStepCounter = nextStepCounter + 1"
ng-show="currentView == 'config'">Next</button>
<button type="button" class="btn btn-primary" <button type="button" class="btn btn-primary"
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements || activating" ng-disabled="!trigger.$ready || (!pullInfo['is_public'] && !pullInfo['pull_entity'])"
ng-click="activate()">Finished</button> ng-click="activate()"
ng-show="currentView == 'analyzed'">Create Trigger</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->

View file

@ -0,0 +1,9 @@
<span class="step-view-step-content">
<span ng-show="!loading">
<span ng-transclude></span>
</span>
<span ng-show="loading">
<span class="quay-spinner"></span>
{{ loadMessage }}
</span>
</span>

View file

@ -0,0 +1,3 @@
<div class="step-view-element">
<div class="transcluded" ng-transclude>
</div>

View file

@ -2,19 +2,18 @@
<span ng-switch-when="github"> <span ng-switch-when="github">
<i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i> <i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i>
Push to GitHub repository <a href="https://github.com/{{ trigger.config.build_source }}" target="_new">{{ trigger.config.build_source }}</a> Push to GitHub repository <a href="https://github.com/{{ trigger.config.build_source }}" target="_new">{{ trigger.config.build_source }}</a>
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="trigger.config.subdir"> <div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!short">
<span>Dockerfile: <div>
<a href="https://github.com/{{ trigger.config.build_source }}/tree/{{ trigger.config.master_branch || 'master' }}/{{ trigger.config.subdir }}/Dockerfile" target="_blank"> <span class="trigger-description-subtitle">Branches:</span>
//{{ trigger.config.subdir }}/Dockerfile <span ng-if="trigger.config.branch_regex">Matching Regular Expression {{ trigger.config.branch_regex }}</span>
</a> <span ng-if="!trigger.config.branch_regex">(All Branches)</span>
</span> </div>
<div>
<span class="trigger-description-subtitle">Dockerfile:</span>
<span ng-if="trigger.config.subdir">//{{ trigger.config.subdir}}/Dockerfile</span>
<span ng-if="!trigger.config.subdir">//Dockerfile</span>
</div> </div>
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!trigger.config.subdir && !short">
<span><span class="trigger-description-subtitle">Dockerfile:</span>
<a href="https://github.com/{{ trigger.config.build_source }}/tree/{{ trigger.config.master_branch || 'master' }}/Dockerfile" target="_blank">
//Dockerfile
</a>
</span>
</div> </div>
</span> </span>
<span ng-switch-default> <span ng-switch-default>

View file

@ -1,17 +1,59 @@
<div class="trigger-setup-github-element"> <div class="trigger-setup-github-element">
<div ng-show="loading"> <!-- Current selected info -->
<span class="quay-spinner" style="vertical-align: middle; margin-right: 10px"></span> <div class="selected-info" ng-show="nextStepCounter > 0">
Loading Repository List <table style="width: 100%;">
<tr ng-show="currentRepo && nextStepCounter > 0">
<td width="200px">
Repository:
</td>
<td>
<div class="current-repo">
<img class="dropdown-select-icon github-org-icon"
ng-src="{{ currentRepo.avatar_url ? currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
{{ currentRepo.repo }}
</div> </div>
<div ng-show="!loading"> </td>
<div style="margin-bottom: 18px">Please choose the GitHub repository that will trigger the build:</div> </tr>
<tr ng-show="nextStepCounter > 1">
<td>
Branches:
</td>
<td>
<div class="branch-filter">
<span ng-if="!state.hasBranchFilter">(All Branches)</span>
<span ng-if="state.hasBranchFilter">Regular Expression: <code>{{ state.branchFilter }}</code></span>
</div>
</td>
</tr>
<tr ng-show="nextStepCounter > 2">
<td>
Dockerfile Location:
</td>
<td>
<div class="dockerfile-location">
<i class="fa fa-folder fa-lg"></i> {{ state.currentLocation || '(Repository Root)' }}
</div>
</td>
</tr>
</table>
</div>
<!-- Step view -->
<div class="step-view" next-step-counter="nextStepCounter" current-step-valid="currentStepValid"
steps-completed="stepsCompleted()">
<!-- Repository select --> <!-- Repository select -->
<div class="step-view-step" complete-condition="currentRepo" load-callback="loadRepositories(callback)"
load-message="Loading Repositories">
<div style="margin-bottom: 12px">Please choose the GitHub repository that will trigger the build:</div>
<div class="dropdown-select" placeholder="'Select a repository'" selected-item="currentRepo" <div class="dropdown-select" placeholder="'Select a repository'" selected-item="currentRepo"
lookahead-items="repoLookahead"> lookahead-items="repoLookahead">
<!-- Icons --> <!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i> <i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i>
<img class="dropdown-select-icon github-org-icon" ng-src="{{ currentRepo.avatar_url ? currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}"> <img class="dropdown-select-icon github-org-icon"
ng-src="{{ currentRepo.avatar_url ? currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
<!-- Dropdown menu --> <!-- Dropdown menu -->
<ul class="dropdown-select-menu" role="menu"> <ul class="dropdown-select-menu" role="menu">
@ -24,25 +66,82 @@
<li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li> <li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li>
</ul> </ul>
</div> </div>
</div>
<!-- Branch filter/select -->
<div class="step-view-step" complete-condition="!state.hasBranchFilter || state.branchFilter"
load-callback="loadBranches(callback)"
load-message="Loading Branches">
<div style="margin-bottom: 12px">Please choose the branches to which this trigger will apply:</div>
<div style="margin-left: 20px;">
<div class="btn-group btn-group-sm" style="margin-bottom: 12px">
<button type="button" class="btn btn-default"
ng-class="state.hasBranchFilter ? '' : 'active btn-info'" ng-click="state.hasBranchFilter = false">
All Branches
</button>
<button type="button" class="btn btn-default"
ng-class="state.hasBranchFilter ? 'active btn-info' : ''" ng-click="state.hasBranchFilter = true">
Matching Regular Expression
</button>
</div>
<div ng-show="state.hasBranchFilter" style="margin-top: 10px;">
<form>
<input class="form-control" type="text" ng-model="state.branchFilter"
placeholder="(Regular expression)" required>
</form>
<div style="margin-top: 10px">
<div ng-if="branchNames.length">
Branches:
<ul class="matching-branches">
<li ng-repeat="branchName in branchNames | limitTo:20"
class="branch-reference"
ng-class="isMatchingBranch(branchName, state.branchFilter) ? 'match' : 'not-match'">
<a href="https://github.com/{{ currentRepo.repo }}/tree/{{ branchName }}" target="_blank">
{{ branchName }}
</a>
</li>
</ul>
<span ng-if="branchNames.length > 20">...</span>
</div>
<div ng-if="state.branchFilter && !branchNames.length"
style="margin-top: 10px">
<strong>Warning:</strong> No branches found
</div>
</div>
</div>
</div>
</div>
<!-- Dockerfile folder select --> <!-- Dockerfile folder select -->
<div class="slideinout" ng-show="currentRepo"> <div class="step-view-step" complete-condition="trigger.$ready" load-callback="loadLocations(callback)"
<div style="margin-top: 10px">Dockerfile Location:</div> load-message="Loading Folders">
<div class="dropdown-select" placeholder="'(Repository Root)'" selected-item="currentLocation"
lookahead-items="locations" handle-input="handleLocationInput(input)" handle-item-selected="handleLocationSelected(datum)" <div style="margin-bottom: 12px">Dockerfile Location:</div>
<div class="dropdown-select" placeholder="'(Repository Root)'" selected-item="state.currentLocation"
lookahead-items="locations" handle-input="handleLocationInput(input)"
handle-item-selected="handleLocationSelected(datum)"
allow-custom-input="true"> allow-custom-input="true">
<!-- Icons --> <!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg" ng-show="isInvalidLocation"></i> <i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg" ng-show="state.isInvalidLocation"></i>
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;" ng-show="!isInvalidLocation"></i> <i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;" ng-show="!state.isInvalidLocation"></i>
<i class="dropdown-select-icon fa fa-folder fa-lg"></i> <i class="dropdown-select-icon fa fa-folder fa-lg"></i>
<!-- Dropdown menu --> <!-- Dropdown menu -->
<ul class="dropdown-select-menu" role="menu"> <ul class="dropdown-select-menu" role="menu">
<li ng-repeat="location in locations"> <li ng-repeat="location in locations">
<a href="javascript:void(0)" ng-click="setLocation(location)" ng-if="!location"><i class="fa fa-github fa-lg"></i> Repository Root</a> <a href="javascript:void(0)" ng-click="setLocation(location)" ng-if="!location">
<a href="javascript:void(0)" ng-click="setLocation(location)" ng-if="location"><i class="fa fa-folder fa-lg"></i> {{ location }}</a> <i class="fa fa-github fa-lg"></i> Repository Root
</a>
<a href="javascript:void(0)" ng-click="setLocation(location)" ng-if="location">
<i class="fa fa-folder fa-lg"></i> {{ location }}
</a>
</li>
<li class="dropdown-header" role="presentation" ng-show="!locations.length">
No Dockerfiles found in repository
</li> </li>
<li class="dropdown-header" role="presentation" ng-show="!locations.length">No Dockerfiles found in repository</li>
</ul> </ul>
</div> </div>
@ -53,10 +152,10 @@
<div class="alert alert-warning" ng-show="locationError"> <div class="alert alert-warning" ng-show="locationError">
{{ locationError }} {{ locationError }}
</div> </div>
<div class="alert alert-info" ng-show="locations.length && isInvalidLocation"> <div class="alert alert-info" ng-show="locations.length && state.isInvalidLocation">
Note: The folder does not currently exist or contain a Dockerfile Note: The folder does not currently exist or contain a Dockerfile
</div> </div>
</div> </div>
<!-- /step-view -->
</div> </div>
</div> </div>

View file

@ -2988,6 +2988,28 @@ quayApp.directive('dockerAuthDialog', function (Config) {
}); });
quayApp.filter('regex', function() {
return function(input, regex) {
if (!regex) { return []; }
try {
var patt = new RegExp(regex);
} catch (ex) {
return [];
}
var out = [];
for (var i = 0; i < input.length; ++i){
var m = input[i].match(patt);
if (m && m[0].length == input[i].length) {
out.push(input[i]);
}
}
return out;
};
});
quayApp.filter('reverse', function() { quayApp.filter('reverse', function() {
return function(items) { return function(items) {
return items.slice().reverse(); return items.slice().reverse();
@ -4744,6 +4766,118 @@ quayApp.directive('triggerDescription', function () {
}); });
quayApp.directive('stepView', function ($compile) {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/step-view.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'nextStepCounter': '=nextStepCounter',
'currentStepValid': '=currentStepValid',
'stepsCompleted': '&stepsCompleted'
},
controller: function($scope, $element, $rootScope) {
this.currentStepIndex = -1;
this.steps = [];
this.watcher = null;
this.getCurrentStep = function() {
return this.steps[this.currentStepIndex];
};
this.reset = function() {
this.currentStepIndex = -1;
for (var i = 0; i < this.steps.length; ++i) {
this.steps[i].element.hide();
}
$scope.currentStepValid = false;
};
this.next = function() {
if (this.currentStepIndex >= 0) {
if (!this.getCurrentStep().scope.completeCondition) {
return;
}
this.getCurrentStep().element.hide();
if (this.unwatch) {
this.unwatch();
this.unwatch = null;
}
}
this.currentStepIndex++;
if (this.currentStepIndex < this.steps.length) {
var currentStep = this.getCurrentStep();
currentStep.element.show();
currentStep.scope.load()
this.unwatch = currentStep.scope.$watch('completeCondition', function(cc) {
$scope.currentStepValid = !!cc;
});
} else {
$scope.stepsCompleted();
}
};
this.register = function(scope, element) {
element.hide();
this.steps.push({
'scope': scope,
'element': element
});
};
var that = this;
$scope.$watch('nextStepCounter', function(nsc) {
if (nsc >= 0) {
that.next();
} else {
that.reset();
}
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('stepViewStep', function () {
var directiveDefinitionObject = {
priority: 1,
require: '^stepView',
templateUrl: '/static/directives/step-view-step.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'completeCondition': '=completeCondition',
'loadCallback': '&loadCallback',
'loadMessage': '@loadMessage'
},
link: function(scope, element, attrs, controller) {
controller.register(scope, element);
},
controller: function($scope, $element) {
$scope.load = function() {
$scope.loading = true;
$scope.loadCallback({'callback': function() {
$scope.loading = false;
}});
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('dropdownSelect', function ($compile) { quayApp.directive('dropdownSelect', function ($compile) {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -4986,25 +5120,28 @@ quayApp.directive('setupTriggerDialog', function () {
controller: function($scope, $element, ApiService, UserService) { controller: function($scope, $element, ApiService, UserService) {
var modalSetup = false; var modalSetup = false;
$scope.state = {};
$scope.nextStepCounter = -1;
$scope.currentView = 'config';
$scope.show = function() { $scope.show = function() {
if (!$scope.trigger || !$scope.repository) { return; } if (!$scope.trigger || !$scope.repository) { return; }
$scope.activating = false; $scope.currentView = 'config';
$scope.pullEntity = null;
$scope.publicPull = true;
$scope.showPullRequirements = false;
$('#setupTriggerModal').modal({}); $('#setupTriggerModal').modal({});
if (!modalSetup) { if (!modalSetup) {
$('#setupTriggerModal').on('hidden.bs.modal', function () { $('#setupTriggerModal').on('hidden.bs.modal', function () {
if (!$scope.trigger || $scope.trigger['is_active']) { return; } if (!$scope.trigger || $scope.trigger['is_active']) { return; }
$scope.nextStepCounter = -1;
$scope.$apply(function() { $scope.$apply(function() {
$scope.cancelSetupTrigger(); $scope.cancelSetupTrigger();
}); });
}); });
modalSetup = true; modalSetup = true;
$scope.nextStepCounter = 0;
} }
}; };
@ -5017,27 +5154,20 @@ quayApp.directive('setupTriggerDialog', function () {
}; };
$scope.hide = function() { $scope.hide = function() {
$scope.activating = false;
$('#setupTriggerModal').modal('hide'); $('#setupTriggerModal').modal('hide');
}; };
$scope.setPublicPull = function(value) { $scope.checkAnalyze = function(isValid) {
$scope.publicPull = value; $scope.currentView = 'analyzing';
$scope.pullInfo = {
'is_public': true
}; };
$scope.checkAnalyze = function(isValid) {
if (!isValid) { if (!isValid) {
$scope.publicPull = true; $scope.currentView = 'analyzed';
$scope.pullEntity = null;
$scope.showPullRequirements = false;
$scope.checkingPullRequirements = false;
return; return;
} }
$scope.checkingPullRequirements = true;
$scope.showPullRequirements = true;
$scope.pullRequirements = null;
var params = { var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id 'trigger_uuid': $scope.trigger.id
@ -5048,26 +5178,20 @@ quayApp.directive('setupTriggerDialog', function () {
}; };
ApiService.analyzeBuildTrigger(data, params).then(function(resp) { ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
$scope.pullRequirements = resp; $scope.currentView = 'analyzed';
if (resp['status'] == 'publicbase') {
$scope.publicPull = true;
$scope.pullEntity = null;
} else if (resp['namespace']) {
$scope.publicPull = false;
if (resp['status'] == 'analyzed') {
if (resp['robots'] && resp['robots'].length > 0) { if (resp['robots'] && resp['robots'].length > 0) {
$scope.pullEntity = resp['robots'][0]; $scope.pullInfo['pull_entity'] = resp['robots'][0];
} else { } else {
$scope.pullEntity = null; $scope.pullInfo['pull_entity'] = null;
}
} }
$scope.checkingPullRequirements = false; $scope.pullInfo['is_public'] = false;
}, function(resp) { }
$scope.pullRequirements = resp;
$scope.checkingPullRequirements = false; $scope.pullInfo['analysis'] = resp;
}); }, ApiService.errorDisplay('Cannot load Dockerfile information'));
}; };
$scope.activate = function() { $scope.activate = function() {
@ -5084,7 +5208,7 @@ quayApp.directive('setupTriggerDialog', function () {
data['pull_robot'] = $scope.pullEntity['name']; data['pull_robot'] = $scope.pullEntity['name'];
} }
$scope.activating = true; $scope.currentView = 'activating';
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) { var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
$scope.hide(); $scope.hide();
@ -5125,17 +5249,99 @@ quayApp.directive('triggerSetupGithub', function () {
scope: { scope: {
'repository': '=repository', 'repository': '=repository',
'trigger': '=trigger', 'trigger': '=trigger',
'nextStepCounter': '=nextStepCounter',
'currentStepValid': '=currentStepValid',
'analyze': '&analyze' 'analyze': '&analyze'
}, },
controller: function($scope, $element, ApiService) { controller: function($scope, $element, ApiService) {
$scope.analyzeCounter = 0; $scope.analyzeCounter = 0;
$scope.setupReady = false; $scope.setupReady = false;
$scope.loading = true;
$scope.branchNames = null;
$scope.state = {
'branchFilter': '',
'hasBranchFilter': false,
'isInvalidLocation': true,
'currentLocation': null
};
$scope.isMatchingBranch = function(branchName, filter) {
try {
var patt = new RegExp(filter);
} catch (ex) {
return false;
}
var m = branchName.match(patt);
return m && m[0].length == branchName.length;
}
$scope.stepsCompleted = function() {
$scope.analyze({'isValid': !$scope.state.isInvalidLocation});
};
$scope.loadRepositories = function(callback) {
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();
callback();
}, ApiService.errorDisplay('Cannot load repositories'));
};
$scope.loadBranches = function(callback) {
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger['id'],
'field_name': 'branch_name'
};
ApiService.listTriggerFieldValues($scope.trigger['config'], params).then(function(resp) {
$scope.branchNames = resp['values'];
callback();
}, ApiService.errorDisplay('Cannot load branch names'));
};
$scope.loadLocations = function(callback) {
$scope.locations = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
if (resp['status'] == 'error') {
callback(resp['message'] || 'Could not load Dockerfile locations');
return;
}
$scope.locations = resp['subdir'] || [];
// Select a default location (if any).
if ($scope.locations.length > 0) {
$scope.setLocation($scope.locations[0]);
} else {
$scope.state.currentLocation = null;
$scope.state.isInvalidLocation = resp['subdir'].indexOf('') < 0;
$scope.trigger.$ready = true;
}
callback();
}, ApiService.errorDisplay('Cannot load locations'));
}
$scope.handleLocationInput = function(location) { $scope.handleLocationInput = function(location) {
$scope.state.isInvalidLocation = $scope.locations.indexOf(location) < 0;
$scope.trigger['config']['subdir'] = location || ''; $scope.trigger['config']['subdir'] = location || '';
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0; $scope.trigger.$ready = true;
$scope.analyze({'isValid': !$scope.isInvalidLocation});
}; };
$scope.handleLocationSelected = function(datum) { $scope.handleLocationSelected = function(datum) {
@ -5143,10 +5349,10 @@ quayApp.directive('triggerSetupGithub', function () {
}; };
$scope.setLocation = function(location) { $scope.setLocation = function(location) {
$scope.currentLocation = location; $scope.state.currentLocation = location;
$scope.state.isInvalidLocation = false;
$scope.trigger['config']['subdir'] = location || ''; $scope.trigger['config']['subdir'] = location || '';
$scope.isInvalidLocation = false; $scope.trigger.$ready = true;
$scope.analyze({'isValid': true});
}; };
$scope.selectRepo = function(repo, org) { $scope.selectRepo = function(repo, org) {
@ -5160,10 +5366,7 @@ quayApp.directive('triggerSetupGithub', function () {
}; };
$scope.selectRepoInternal = function(currentRepo) { $scope.selectRepoInternal = function(currentRepo) {
if (!currentRepo) {
$scope.trigger.$ready = false; $scope.trigger.$ready = false;
return;
}
var params = { var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name, 'repository': $scope.repository.namespace + '/' + $scope.repository.name,
@ -5175,39 +5378,6 @@ quayApp.directive('triggerSetupGithub', function () {
'build_source': repo, 'build_source': repo,
'subdir': '' 'subdir': ''
}; };
// Lookup the possible Dockerfile locations.
$scope.locations = null;
if (repo) {
ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
if (resp['status'] == 'error') {
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
return;
}
$scope.locationError = null;
$scope.locations = resp['subdir'] || [];
$scope.trigger.$ready = true;
if ($scope.locations.length > 0) {
$scope.setLocation($scope.locations[0]);
} else {
$scope.currentLocation = null;
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
$scope.analyze({'isValid': !$scope.isInvalidLocation});
}
}, function(resp) {
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
});
}
}; };
var setupTypeahead = function() { var setupTypeahead = function() {
@ -5237,30 +5407,20 @@ quayApp.directive('triggerSetupGithub', function () {
$scope.repoLookahead = repos; $scope.repoLookahead = repos;
}; };
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;
});
};
var check = function() {
if ($scope.repository && $scope.trigger) {
loadSources();
}
};
$scope.$watch('repository', check);
$scope.$watch('trigger', check);
$scope.$watch('currentRepo', function(repo) { $scope.$watch('currentRepo', function(repo) {
if (repo) {
$scope.selectRepoInternal(repo); $scope.selectRepoInternal(repo);
}
});
$scope.$watch('state.branchFilter', function(bf) {
if (!$scope.trigger) { return; }
if ($scope.state.hasBranchFilter) {
$scope.trigger['config']['branch_regex'] = bf;
} else {
delete $scope.trigger['config']['branch_regex'];
}
}); });
} }
}; };