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
@ -330,7 +353,7 @@ class GithubBuildTrigger(BuildTrigger):
payload = request.get_json() payload = request.get_json()
if not payload or payload.get('head_commit') is None: if not payload or payload.get('head_commit') is None:
raise SkipRequestException() raise SkipRequestException()
if 'zen' in payload: if 'zen' in payload:
raise ValidationRequestException() raise ValidationRequestException()
@ -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>
<!-- Loading pull information -->
<div ng-show="currentView == 'analyzing'">
<span class="quay-spinner"></span> Checking pull credential requirements...
</div>
<!-- Pull information --> <!-- Pull information -->
<div class="trigger-option-section" ng-show="showPullRequirements"> <div class="trigger-option-section" ng-show="currentView == 'analyzed'">
<div ng-show="!pullRequirements">
<span class="quay-spinner"></span> Checking pull credential requirements...
</div>
<div ng-show="pullRequirements"> <!-- Messaging -->
<div class="alert alert-danger" ng-if="pullRequirements.status == 'error'"> <div class="alert alert-danger" ng-if="pullInfo.analysis.status == 'error'">
{{ pullRequirements.message }} {{ pullInfo.analysis.message }}
</div> </div>
<div class="alert alert-warning" ng-if="pullRequirements.status == 'warning'"> <div class="alert alert-warning" ng-if="pullInfo.analysis.status == 'warning'">
{{ pullRequirements.message }} {{ pullRequirements.message }}
</div> </div>
<div class="alert alert-success" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.is_public === false"> <div class="dockerfile-found" ng-if="pullInfo.analysis.is_public === false">
The <div class="dockerfile-found-content">
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a> A robot account is <strong>required</strong> for this build trigger because
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span>
depends on the private <span class="registry-name"></span> repository the
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank"> <a href="{{ pullInfo.analysis.dockerfile_url }}" ng-if="pullInfo.analysis.dockerfile_url" target="_blank">
{{ pullRequirements.namespace }}/{{ pullRequirements.name }} Dockerfile found
</a> which requires </a>
a robot account for pull access, because it is marked <strong>private</strong>. <span ng-if="!pullInfo.analysis.dockerfile_url">Dockerfile found</span>
pulls from the private <span class="registry-name"></span> repository
<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%;"> <div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
<tr> <strong>Note:</strong> In order to set pull credentials for a build trigger, you must be an
<td style="width: 162px"> Administrator of the namespace <strong>{{ repository.namespace }}</strong>
<span class="context-tooltip" data-title="The credentials given to 'docker pull' in the builder for pulling images" </div>
style="margin-bottom: 10px" bs-tooltip>
docker pull Credentials:
</span>
</td>
<td>
<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>
</div>
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repository.namespace)">
<button type="button" class="btn btn-default"
ng-class="publicPull ? 'active btn-info' : ''" ng-click="setPublicPull(true)">None</button>
<button type="button" class="btn btn-default"
ng-class="publicPull ? '' : 'active btn-info'" ng-click="setPublicPull(false)">
<i class="fa fa-wrench"></i>
Robot account
</button>
</div>
</td>
</tr>
</table>
<table style="width: 100%;"> <!-- Namespace admin -->
<tr ng-show="!publicPull"> <div ng-show="isNamespaceAdmin(repository.namespace)">
<td> <!-- Select credentials -->
<div class="entity-search" namespace="repository.namespace" <div class="btn-group btn-group-sm">
placeholder="'Select robot account for pulling...'" <button type="button" class="btn btn-default"
current-entity="pullEntity" ng-class="pullInfo.is_public ? 'active btn-info' : ''"
allowed-entities="['robot']"></div> ng-click="pullInfo.is_public = true">
None
</button>
<button type="button" class="btn btn-default"
ng-class="pullInfo.is_public ? '' : 'active btn-info'"
ng-click="pullInfo.is_public = false">
<i class="fa fa-wrench"></i>
Robot account
</button>
</div>
<div class="alert alert-info" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.robots.length" <!-- Robot Select -->
style="margin-top: 20px; margin-bottom: 0px;"> <div ng-show="!pullInfo.is_public" style="margin-top: 10px">
Note: We've automatically selected robot account <div class="entity-search" namespace="repository.namespace"
<span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the private placeholder="'Select robot account for pulling...'"
repository. current-entity="pullInfo.pull_entity"
</div> allowed-entities="['robot']"></div>
<div class="alert alert-warning"
ng-if="pullRequirements.status == 'analyzed' && !pullRequirements.robots.length && pullRequirements.name" <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: No robot account currently has access to the private repository. Please create one and/or assign access in the <span class="entity-reference" entity="pullInfo.analysis.robots[0]"></span>,
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's since it has access to the private repository.
admin panel</a>. </div>
</div> <div ng-if="!pullInfo.analysis.robots.length && pullInfo.analysis.name"
</td> style="margin-top: 20px; margin-bottom: 0px;">
</tr> <strong>Note</strong>: No robot account currently has access to the private repository. Please create one and/or assign access in the
</table> <a href="/repository/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}/admin" target="_blank">
repository's admin panel.
</a>
</div>
</div>
</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>
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!trigger.config.subdir && !short"> <div>
<span><span class="trigger-description-subtitle">Dockerfile:</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"> <span ng-if="trigger.config.subdir">//{{ trigger.config.subdir}}/Dockerfile</span>
//Dockerfile <span ng-if="!trigger.config.subdir">//Dockerfile</span>
</a> </div>
</span>
</div> </div>
</span> </span>
<span ng-switch-default> <span ng-switch-default>

View file

@ -1,48 +1,147 @@
<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>
</td>
</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> </div>
<div ng-show="!loading">
<div style="margin-bottom: 18px">Please choose the GitHub repository that will trigger the build:</div>
<!-- Step view -->
<div class="step-view" next-step-counter="nextStepCounter" current-step-valid="currentStepValid"
steps-completed="stepsCompleted()">
<!-- Repository select --> <!-- Repository select -->
<div class="dropdown-select" placeholder="'Select a repository'" selected-item="currentRepo" <div class="step-view-step" complete-condition="currentRepo" load-callback="loadRepositories(callback)"
lookahead-items="repoLookahead"> load-message="Loading Repositories">
<!-- Icons --> <div style="margin-bottom: 12px">Please choose the GitHub repository that will trigger the build:</div>
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i> <div class="dropdown-select" placeholder="'Select a repository'" selected-item="currentRepo"
<img class="dropdown-select-icon github-org-icon" ng-src="{{ currentRepo.avatar_url ? currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}"> lookahead-items="repoLookahead">
<!-- Icons -->
<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/' }}">
<!-- Dropdown menu --> <!-- Dropdown menu -->
<ul class="dropdown-select-menu" role="menu"> <ul class="dropdown-select-menu" role="menu">
<li ng-repeat-start="org in orgs" role="presentation" class="dropdown-header github-org-header"> <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 }} <img ng-src="{{ org.info.avatar_url }}" class="github-org-icon">{{ org.info.name }}
</li> </li>
<li ng-repeat="repo in org.repos" class="github-repo-listing"> <li ng-repeat="repo in org.repos" class="github-repo-listing">
<a href="javascript:void(0)" ng-click="selectRepo(repo, org)"><i class="fa fa-github fa-lg"></i> {{ repo }}</a> <a href="javascript:void(0)" ng-click="selectRepo(repo, org)"><i class="fa fa-github fa-lg"></i> {{ repo }}</a>
</li> </li>
<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>
<!-- 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> </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>
<li class="dropdown-header" role="presentation" ng-show="!locations.length">No Dockerfiles found in repository</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.publicPull = value;
};
$scope.checkAnalyze = function(isValid) { $scope.checkAnalyze = function(isValid) {
$scope.currentView = 'analyzing';
$scope.pullInfo = {
'is_public': true
};
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.pullInfo['is_public'] = false;
} }
$scope.checkingPullRequirements = false; $scope.pullInfo['analysis'] = resp;
}, function(resp) { }, ApiService.errorDisplay('Cannot load Dockerfile information'));
$scope.pullRequirements = resp;
$scope.checkingPullRequirements = false;
});
}; };
$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) {
$scope.selectRepoInternal(repo); if (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'];
}
}); });
} }
}; };