Implement new create and manager trigger UI

Implements the new trigger setup user interface, which is now a linear workflow found on its own page, rather than a tiny modal dialog

Fixes #1187
This commit is contained in:
Joseph Schorr 2016-09-27 16:52:34 +02:00
parent 21b09a7451
commit 8e863b8cf5
47 changed files with 1835 additions and 1068 deletions

View file

@ -1397,16 +1397,38 @@ a:focus {
margin-bottom: 6px;
}
.co-check-bar .co-filter-box input, .co-top-bar .co-filter-box input {
.co-check-bar .co-filter-box input, .co-top-bar .co-filter-box input[type="text"] {
width: 300px;
display: inline-block;
vertical-align: middle;
}
.co-check-bar .co-filter-box input, .co-top-bar .co-filter-box label {
margin-left: 6px;
}
.co-top-bar .co-filter-box input {
vertical-align: top;
}
@media screen and (max-width: 767px) {
.co-top-bar .page-controls {
display: block;
margin-bottom: 10px;
text-align: right;
}
.co-top-bar .co-filter-box {
display: block;
margin-bottom: 10px;
}
.co-top-bar .filter-options {
display: block;
margin-bottom: 10px;
}
}
.empty {
border-bottom: none !important;
}

View file

@ -0,0 +1,64 @@
.linear-workflow-section {
margin-bottom: 10px;
}
.linear-workflow-section.row {
margin-left: 0px;
margin-right: 0px;
}
.linear-workflow .upcoming-table {
vertical-align: middle;
margin-left: 20px;
}
.linear-workflow .upcoming-table .fa {
margin-right: 8px;
}
.linear-workflow .upcoming {
color: #888;
vertical-align: middle;
margin-left: 10px;
}
.linear-workflow .upcoming ul {
padding: 0px;
display: inline-block;
margin: 0px;
}
.linear-workflow .upcoming li {
display: inline-block;
margin-right: 6px;
margin-left: 2px;
}
.linear-workflow .upcoming li:after {
content: "•";
display: inline-block;
margin-left: 6px;
margin-right: 2px;
}
.linear-workflow .upcoming li:last-child:after {
content: "";
}
.linear-workflow .bottom-controls {
padding: 10px;
}
.linear-workflow-section-element {
padding: 20px;
padding-top: 10px;
}
.linear-workflow-section-element h3, .linear-workflow-section-element strong {
color: #444;
}
.linear-workflow-section-element.current-section h3,
.linear-workflow-section-element.current-section strong {
color: black;
}

View file

@ -0,0 +1,106 @@
.manage-trigger-control .help-col {
padding: 30px;
padding-top: 100px;
}
.manage-trigger-control .main-col {
padding-left: 10px;
padding-right: 10px;
padding-top: 10px;
}
.manage-trigger-control strong {
margin-bottom: 10px;
display: block;
}
.manage-trigger-control .namespace-avatar {
margin-right: 4px;
width: 24px;
vertical-align: middle;
}
.manage-trigger-control .importance-col {
text-align: center;
width: 120px;
}
.manage-trigger-control .co-top-bar {
margin-top: 20px;
height: 28px;
}
.manage-trigger-control .namespace-avatar {
margin-left: 2px;
margin-right: 2px;
display: inline-block;
}
.manage-trigger-control .service-icon {
font-size: 24px;
margin-right: 4px;
vertical-align: middle;
}
.manage-trigger-control .fa-exclamation-triangle {
color: #FCA657;
}
.manage-trigger-control .empty-description {
color: #ccc;
}
.manage-trigger-control .radio {
margin-bottom: 20px;
}
.manage-trigger-control .radio input[type="radio"] {
padding: 4px;
}
.manage-trigger-control .radio label .title {
font-size: 16px;
}
.manage-trigger-control .radio label .weak {
font-weight: 300;
}
.manage-trigger-control .radio label .description {
margin-top: 6px;
color: #aaa;
}
.manage-trigger-control .radio label .extended {
margin-top: 20px;
}
.manage-trigger-control .radio label td:first-child {
vertical-align: top;
padding: 4px;
padding-right: 10px;
}
.manage-trigger-control .regex-match-view {
margin-top: 20px;
}
.manage-trigger-control h3 .fa {
margin-right: 4px;
}
.manage-trigger-control h3.warning {
color: #FCA657;
}
.manage-trigger-control h3.error {
color: #D64456;
}
.manage-trigger-control .success {
color: #2FC98E !important;
}
.manage-trigger-control .nowrap-col {
white-space: nowrap;
}

View file

@ -0,0 +1,36 @@
.regex-match-view-element .match-list {
list-style: none;
overflow: auto;
max-height: 150px;
}
.regex-match-view-element .match-list li {
display: inline-block;
margin-right: 4px;
width: 120px;
padding: 4px;
}
.regex-match-view-element .match-list li .fa {
margin-right: 4px;
vertical-align: middle;
}
.regex-match-view-element .match-list.not-matching li {
color: #aaa;
}
.regex-match-view-element .match-list.matching li {
color: #2fc98e;
}
.regex-match-view-element .match-table td:first-child {
vertical-align: top;
white-space: nowrap;
}
.regex-match-view-element .fa-exclamation-triangle {
margin-right: 4px;
}

View file

@ -1,28 +0,0 @@
.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 .loading {
text-align: center;
}
.setup-trigger-directive-element .loading .cor-loader-inline {
margin-right: 4px;
}
.setup-trigger-directive-element .dockerfile-found {
position: relative;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}

View file

@ -1,9 +0,0 @@
.step-view-step-content .loading-message {
position: relative;
text-align: center;
display: block;
}
.step-view-step-content .loading-message .cor-loader-inline {
margin-right: 6px;
}

View file

@ -0,0 +1,35 @@
.trigger-setup-element .activated .content {
padding-top: 10px;
padding-bottom: 10px;
}
.trigger-setup-element .activated h3 {
text-align: center;
margin-bottom: 30px;
display: block;
}
.trigger-setup-element .button-bar {
text-align: right;
margin-top: 16px;
}
.trigger-setup-element .activating .cor-loader-inline {
margin-right: 6px;
}
.trigger-setup-element .activating .btn-success {
display: none;
}
.trigger-setup-element .activating-message {
padding: 10px;
padding-left: 30px;
}
.trigger-setup-element .activating-message b {
vertical-align: middle;
font-size: 18px;
font-weight: normal;
}

View file

@ -4,9 +4,7 @@
<!-- Credentials -->
<div ng-repeat="credential in trigger.config.credentials">
<p>
{{ credential.name }}:
<div class="copy-box" value="credential.value"></div>
</p>
{{ credential.name }}:
<div class="copy-box" value="credential.value"></div>
</div>
</div>

View file

@ -0,0 +1,32 @@
<div class="dockerfile-path-select-element">
<div class="dropdown-select" placeholder="'Enter path containing a Dockerfile'"
selected-item="selectedPath"
lookahead-items="paths"
handle-input="setPath(input)"
handle-item-selected="setSelectedPath(datum.value)"
allow-custom-input="true"
hide-dropdown="!supportsFullListing">
<!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg" ng-show="isUnknownPath"></i>
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;" ng-show="!isUnknownPath"></i>
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
<!-- Dropdown menu -->
<ul class="dropdown-select-menu pull-right" role="menu">
<li ng-repeat="path in paths">
<a ng-click="setSelectedPath(path)" ng-if="path">
<i class="fa fa-folder fa-lg"></i> {{ path }}
</a>
</li>
<li class="dropdown-header" role="presentation" ng-show="!paths.length">
No Dockerfiles found in repository
</li>
</ul>
</div>
<div style="padding: 10px">
<div class="co-alert co-alert-danger" ng-show="!isValidPath && currentPath">
Path entered for folder containing Dockerfile is invalid: Must start with a '/'.
</div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="linear-workflow-section-element" ng-show="sectionVisible"
ng-class="isCurrentSection ? 'current-section' : ''">
<form ng-submit="submitSection()">
<div ng-transclude />
</form>
</div>

View file

@ -0,0 +1,31 @@
<div class="linear-workflow-element">
<!-- Contents -->
<div ng-transclude/>
<div class="bottom-controls">
<table class="upcoming-table">
<tr>
<td>
<!-- Next button -->
<button class="btn btn-primary" ng-disabled="!currentSection.valid"
ng-click="nextSection()"
ng-class="{'btn-success': currentSection.index == sections.length - 1, 'btn-lg': currentSection.index == sections.length - 1}">
<span ng-if="currentSection.index != sections.length - 1">Continue</span>
<span ng-if="currentSection.index == sections.length - 1"><i class="fa fa-check-circle"></i>{{ doneTitle }}</span>
</button>
</td>
<td>
<!-- Next sections -->
<div class="upcoming" ng-if="currentSection.index != sections.length - 1">
<b>Next:</b>
<ul>
<li ng-repeat="section in sections" ng-if="section.index > currentSection.index">
{{ section.title }}
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
</div>

View file

@ -0,0 +1,43 @@
<div class="manage-trigger-custom-git-element manage-trigger-control">
<div class="linear-workflow" workflow-state="currentState" done-title="Create Trigger"
workflow-complete="activateTrigger({'config': config})">
<!-- Section: Repository -->
<div class="linear-workflow-section row"
section-id="repo"
section-title="Git Repository"
section-valid="config.build_source">
<div class="col-lg-7 col-md-7 col-sm-12 main-col">
<h3>Enter repository</h3>
<strong>
Please enter the HTTP or SSH style URL used to clone your git repository:
</strong>
<input class="form-control" type="text" placeholder="git@example.com:namespace/repository.git"
ng-model="config.build_source" ng-pattern="/(((http|https):\/\/)(.+)|\w+@(.+):(.+))/">
</div>
<div class="col-lg-5 col-md-5 hidden-sm hidden-xs help-col">
<p>Custom git triggers support any externally accessible git repository, via either the normal git protocol or HTTP.</p>
<p><b>It is the responsibility of the git repository to invoke a webhook to tell <span class="registry-name" short="true"></span> that a commit has been added.</b></p>
</div>
</div><!-- /Section: Repository -->
<!-- Section: Build context -->
<div class="linear-workflow-section row"
section-id="dockerfile"
section-title="Build context"
section-valid="config.subdir">
<div class="col-lg-7 col-md-7 col-sm-12 main-col">
<h3>Select build context directory</h3>
<strong>Please select the build context directory under the git repository:</strong>
<input class="form-control" type="text" placeholder="/"
ng-model="config.subdir" ng-pattern="/^($|\/|\/.+)/">
</div>
<div class="col-lg-5 col-md-5 hidden-sm hidden-xs help-col">
<p>The build context directory is the path of the directory containing the Dockerfile and any other files to be made available when the build is triggered.</p>
<p>If the Dockerfile is located at the root of the git repository, enter <code>/</code> as the build context directory.</p>
</div>
</div><!-- /Section: Build context -->
</div>

View file

@ -0,0 +1,330 @@
<div class="manage-trigger-githost-element manage-trigger-control">
<div class="linear-workflow" workflow-state="currentState" done-title="Create Trigger"
workflow-complete="createTrigger()">
<!-- Section: Namespace -->
<div class="linear-workflow-section row"
section-id="namespace"
section-title="{{ 'Select ' + namespaceTitle }}"
section-valid="local.selectedNamespace">
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.namespaces">
<h3>Select {{ namespaceTitle }}</h3>
<strong>
Please select the {{ namespaceTitle }} under which the repository lives
</strong>
<div class="co-top-bar">
<div class="co-filter-box">
<span class="page-controls" total-count="local.orderedNamespaces.entries.length" current-page="local.namespaceOptions.page" page-size="namespacesPerPage"></span>
<input class="form-control" type="text" ng-model="local.namespaceOptions.filter" placeholder="Filter {{ namespaceTitle }}s...">
</div>
</div>
<table class="co-table" style="margin-top: 20px;">
<thead>
<td class="checkbox-col"></td>
<td ng-class="TableService.tablePredicateClass('id', local.namespaceOptions.predicate, local.namespaceOptions.reverse)">
<a ng-click="TableService.orderBy('id', local.namespaceOptions)">{{ namespaceTitle }}</a>
</td>
<td ng-class="TableService.tablePredicateClass('score', local.namespaceOptions.predicate, local.namespaceOptions.reverse)"
class="importance-col hidden-xs">
<a ng-click="TableService.orderBy('score', local.namespaceOptions)">Importance</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="namespace in local.orderedNamespaces.visibleEntries | slice:(namespacesPerPage * local.namespaceOptions.page):(namespacesPerPage * (local.namespaceOptions.page + 1))"
ng-class="local.selectedNamespace == namespace ? 'checked' : ''"
bindonce>
<td>
<input type="radio" ng-model="local.selectedNamespace" ng-value="namespace">
</td>
<td>
<img class="namespace-avatar" ng-src="{{ namespace.avatar_url }}">
<span class="anchor" href="{{ namespace.url }}" is-text-only="!namespace.url">{{ namespace.id }}</span>
</td>
<td class="importance-col hidden-xs">
<span class="strength-indicator" value="::namespace.score" maximum="::local.maxScore"
log-base="10"></span>
</td>
</tr>
</table>
<div class="empty" ng-if="local.namespaces.length && !local.orderedNamespaces.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching {{ namespaceTitle }} found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.namespaces">
<span class="cor-loader-inline"></span> Retrieving {{ namespaceTitle }}s
</div>
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col" ng-show="local.namespaces">
<p>
<span class="registry-name"></span> has been granted access to read and view these {{ namespaceTitle }}s.
</p>
<p>
Don't see an expected {{ namespaceTitle }} here? Please make sure third-party access is enabled for <span class="registry-name"></span> under that {{ namespaceTitle }}.
</p>
</div>
</div><!-- /Section: Namespace -->
<!-- Section: Repository -->
<div class="linear-workflow-section row"
section-id="repo"
section-title="Select Repository"
section-valid="local.selectedRepository">
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.repositories">
<h3>Select Repository</h3>
<strong>
Select a repository in
<img class="namespace-avatar" ng-src="{{ local.selectedNamespace.avatar_url }}">
{{ local.selectedNamespace.id }}
</strong>
<div class="co-top-bar">
<div class="co-filter-box">
<span class="page-controls" total-count="local.orderedRepositories.entries.length" current-page="local.repositoryOptions.page" page-size="repositoriesPerPage"></span>
<input class="form-control" type="text" ng-model="local.repositoryOptions.filter" placeholder="Filter repositories...">
<div class="filter-options">
<label><input type="checkbox" ng-model="local.repositoryOptions.hideStale">Hide stale repositories</label>
</div>
</div>
</div>
<table class="co-table" style="margin-top: 20px;">
<thead>
<td class="checkbox-col"></td>
<td ng-class="TableService.tablePredicateClass('name', local.repositoryOptions.predicate, local.repositoryOptions.reverse)" class="nowrap-col">
<a ng-click="TableService.orderBy('name', local.repositoryOptions)">Repository Name</a>
</td>
<td ng-class="TableService.tablePredicateClass('last_updated_datetime', local.repositoryOptions.predicate, local.repositoryOptions.reverse)"
class="last-updated-col nowrap-col">
<a ng-click="TableService.orderBy('last_updated_datetime', local.namespaceOptions)">Last Updated</a>
</td>
<td class="hidden-xs">Description</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="repository in local.orderedRepositories.visibleEntries | slice:(repositoriesPerPage * local.repositoryOptions.page):(repositoriesPerPage * (local.repositoryOptions.page + 1))"
ng-class="local.selectedRepository == repository ? 'checked' : ''"
bindonce>
<td>
<span ng-if="!repository.has_admin_permissions">
<i class="fa fa-exclamation-triangle"
data-title="Admin access is required to add the webhook trigger to this repository" bs-tooltip></i>
</span>
<input type="radio" ng-model="local.selectedRepository" ng-value="repository"
ng-if="repository.has_admin_permissions">
</td>
<td class="nowrap-col">
<i class="service-icon fa {{ getTriggerIcon() }}"></i>
<span class="anchor" href="{{ repository.url }}" is-text-only="!repository.url">{{ repository.name }}</span>
</td>
<td class="last-updated-col nowrap-col">
<span am-time-ago="repository.last_updated_datetime"></span>
</td>
<td class="hidden-xs">
<span ng-if="repository.description">{{ repository.description }}</span>
<span class="empty-description" ng-if="!repository.description">(None)</span>
</td>
</tr>
</table>
<div class="empty" ng-if="local.repositories.length && !local.orderedRepositories.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching repositories found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.repositories">
<span class="cor-loader-inline"></span> Retrieving repositories
</div>
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col" ng-show="local.repositories">
<p>
A webhook will be added to the selected repository in order to detect when new commits are made.
</p>
<p>
Don't see an expected repository here? Please make sure you have admin access on that repository.
</p>
</div>
</div><!-- /Section: Repository -->
<!-- Section: Trigger Options -->
<div class="linear-workflow-section row"
section-id="triggeroptions"
section-title="Configure Trigger"
section-valid="local.triggerOptions">
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.repositoryRefs">
<h3>Configure Trigger</h3>
<strong>
Configure trigger options for
<img class="namespace-avatar" ng-src="{{ local.selectedNamespace.avatar_url }}">
{{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }}
</strong>
<div class="radio" style="margin-top: 20px;">
<label>
<input type="radio" name="optionRadio" ng-model="local.triggerOptions.hasBranchTagFilter" ng-value="false">
<div class="title">Trigger for all branches and tags <span class="weak">(default)</span></div>
<div class="description">Build a container image for each commit across all branches and tags</div>
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="optionRadio" ng-model="local.triggerOptions.hasBranchTagFilter" ng-value="true">
<div class="title">Trigger only on branches and tags matching a regular expression</div>
<div class="description">Only build container images for a subset of branches and/or tags.</div>
<div class="extended" ng-if="local.triggerOptions.hasBranchTagFilter">
<table>
<tr>
<td style="white-space: nowrap;">Regular Expression:</td>
<td>
<input type="text" class="form-control" ng-model="local.triggerOptions.branchTagFilter" required>
<div class="description">Examples: heads/master, tags/tagname, heads/.+</div>
<div class="regex-match-view"
items="local.repositoryFullRefs"
regex="local.triggerOptions.branchTagFilter"
ng-if="local.triggerOptions.branchTagFilter"></div>
</td>
</tr>
</table>
</div>
</label>
</div>
</div>
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.repositoryRefs">
<span class="cor-loader-inline"></span> Retrieving repository refs
</div>
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
<p>Do you want to build a new container image for commits across all branches and tags, or limit to a subset?</p>
<p>For example, if you use release branches instead of <code>master</code> for building versions of your software, you can configure the trigger to only build images for these branches.</p>
<p>All images built will be tagged with the name of the branch or tag whose change invoked the trigger</p>
</div>
</div><!-- /Section: Trigger Options -->
<!-- Section: Dockerfile Location -->
<div class="linear-workflow-section row"
section-id="dockerfilelocation"
section-title="Select Dockerfile"
section-valid="local.hasValidDockerfilePath">
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.dockerfileLocations.status == 'error'">
<div class="co-alert co-alert-warning">
{{ local.dockerfileLocations.message }}
</div>
</div>
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.dockerfileLocations.status == 'success'">
<h3>Select Dockerfile</h3>
<strong>
Please select the location of the Dockerfile to build when this trigger is invoked
</strong>
<div class="dockerfile-path-select" current-path="local.dockerfilePath" paths="local.dockerfileLocations.subdir"
supports-full-listing="true" is-valid-path="local.hasValidDockerfilePath"></div>
</div>
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.dockerfileLocations">
<span class="cor-loader-inline"></span> Retrieving Dockerfile locations
</div>
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
<p>Please select the location containing the Dockerfile to be built.</p>
<p>The build context will start at the location selected.</p>
</div>
</div><!-- /Section: Dockerfile Location -->
<!-- Section: Verification and Robot Account -->
<div class="linear-workflow-section row"
section-id="verification"
section-title="Confirm"
section-valid="local.triggerAnalysis.status != 'error' && (local.triggerAnalysis.status != 'requiresrobot' || local.robotAccount != null)">
<!-- Error -->
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'error'">
<h3 class="error"><i class="fa fa-exclamation-circle"></i> Verification Error</h3>
<strong>
There was an error when verifying the state of <img class="namespace-avatar" ng-src="{{ local.selectedNamespace.avatar_url }}">
{{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }}
</strong>
{{ local.triggerAnalysis.message }}
</div>
<!-- Warning -->
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'warning'">
<h3 class="warning"><i class="fa fa-exclamation-triangle"></i> Verification Warning</h3>
{{ local.triggerAnalysis.message }}
</div>
<!-- Public base -->
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'publicbase'">
<h3 class="success"><i class="fa fa-check-circle"></i> Ready to go!</h3>
<strong>Click "Create Trigger" to complete setup of this build trigger</strong>
</div>
<!-- Requires robot and is not admin -->
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'requiresrobot' && !local.triggerAnalysis.is_admin">
<h3>Robot Account Required</h3>
<p>The selected Dockerfile in the selected repository depends upon a private base image</p>
<p>A robot account with access to the base image is required to setup this trigger, but you are not the administrator of this namespace.</p>
<p>Administrative access is required to continue to ensure security of the robot credentials.</p>
</div>
<!-- Requires robot and is admin -->
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'requiresrobot' && local.triggerAnalysis.is_admin">
<h3>Select Robot Account</h3>
<strong>
The selected Dockerfile in the selected repository depends upon a private base image. Select a robot account with access:
</strong>
<div class="co-top-bar">
<div class="co-filter-box">
<span class="page-controls" total-count="local.orderedRobotAccounts.entries.length" current-page="local.robotOptions.page" page-size="robotsPerPage"></span>
<input class="form-control" type="text" ng-model="local.robotOptions.filter" placeholder="Filter robot accounts...">
</div>
</div>
<table class="co-table" style="margin-top: 20px;">
<thead>
<td class="checkbox-col"></td>
<td ng-class="TableService.tablePredicateClass('name', local.robotOptions.predicate, local.robotOptions.reverse)">
<a ng-click="TableService.orderBy('name', local.robotOptions)">Robot Account</a>
</td>
<td ng-class="TableService.tablePredicateClass('can_read', local.robotOptions.predicate, local.robotOptions.reverse)">
<a ng-click="TableService.orderBy('can_read', local.robotOptions)">Has Read Access</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="robot in local.orderedRobotAccounts.visibleEntries | slice:(robotsPerPage * local.namespaceOptions.page):(robotsPerPage * (local.robotOptions.page + 1))"
ng-class="local.robotAccount == robot ? 'checked' : ''"
bindonce>
<td>
<input type="radio" ng-model="local.robotAccount" ng-value="robot">
</td>
<td>
<span class="entity-reference" entity="robot"></span>
</td>
<td>
<span ng-if="robot.can_read" class="success">Can Read</span>
<span ng-if="!robot.can_read">Read access will be added if selected</span>
</td>
</tr>
</table>
<div class="empty" ng-if="local.triggerAnalysis.robots.length && !local.orderedRobotAccounts.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching robot accounts found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col" ng-if="local.triggerAnalysis.status == 'requiresrobot' && local.triggerAnalysis.is_admin">
<p>The Dockerfile you selected utilizes a private base image.</p>
<p>In order for the <span class="registry-name"></span> to pull the base image during the build process, a robot account with access must be selected.</p>
<p>Robot accounts that already have access to this base image are listed first. If you select a robot account that does not currently have access, read permission will be granted to that robot account on trigger creation.</p>
</div>
</div><!-- /Section: Robot Account -->
</div>

View file

@ -0,0 +1,29 @@
<div class="regex-match-view-element">
<div ng-if="filterMatches(regex, items, false) == null">
<i class="fa fa-exclamation-triangle"></i>Invalid Regular Expression!
</div>
<div ng-if="filterMatches(regex, items, false) != null">
<table class="match-table">
<tr>
<td>Matching:</td>
<td>
<ul class="matching match-list">
<li ng-repeat="item in filterMatches(regex, items, true)">
<i class="fa {{ item.icon }}"></i>{{ item.title }}
</li>
</ul>
</td>
</tr>
<tr>
<td>Not Matching:</td>
<td>
<ul class="not-matching match-list">
<li ng-repeat="item in filterMatches(regex, items, false)">
<i class="fa {{ item.icon }}"></i>{{ item.title }}
</li>
</ul>
</td>
</tr>
</table>
</div>
</div>

View file

@ -126,10 +126,8 @@
<tr ng-repeat="trigger in triggers | filter:{'is_active':false}">
<td colspan="5" style="text-align: center">
<span class="cor-loader-inline"></span>
Trigger Setup in progress:
<a ng-click="setupTrigger(trigger)">Resume</a> |
<a ng-click="deleteTrigger(trigger)">Cancel</a>
This build trigger has not had its setup completed:
<a ng-click="deleteTrigger(trigger)">Delete Trigger</a>
</td>
</tr>
@ -185,14 +183,6 @@
build-started="handleBuildStarted(build)">
</div>
<!-- Setup trigger dialog-->
<div class="setup-trigger-dialog"
repository="repository"
trigger="currentSetupTrigger"
canceled="cancelSetupTrigger(trigger)"
counter="showTriggerSetupCounter"
trigger-runner="askRunTrigger(trigger)"></div>
<!-- Manual trigger dialog -->
<div class="manual-trigger-build-dialog"
repository="repository"
@ -201,5 +191,4 @@
build-started="handleBuildStarted(build)"></div>
<!-- /Dialogs -->
</div>

View file

@ -1,133 +0,0 @@
<div class="setup-trigger-directive-element">
<!-- Modal message dialog -->
<div class="modal fade" id="setupTriggerModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Setup new build trigger</h4>
</div>
<div class="modal-body loading" ng-show="currentView == 'activating'">
<span class="cor-loader-inline"></span> Setting up trigger...
</div>
<div class="modal-body" ng-show="currentView != 'activating'">
<!-- Trigger-specific setup -->
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
<div ng-switch-when="custom-git">
<div class="trigger-setup-custom" repository="repository" trigger="trigger"
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div>
</div>
<div ng-switch-default>
<div class="trigger-setup-githost" repository="repository" trigger="trigger"
kind="{{ trigger.service }}"
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div>
</div>
</div>
<!-- Loading pull information -->
<div ng-show="currentView == 'analyzing'" class="loading">
<span class="cor-loader-inline"></span> Checking pull credential requirements...
</div>
<!-- Pull information -->
<div class="trigger-option-section" ng-show="currentView == 'analyzed'">
<!-- Messaging -->
<div ng-switch on="pullInfo.analysis.status">
<div ng-switch-when="error" class="alert alert-danger">{{ pullInfo.analysis.message }}</div>
<div ng-switch-when="warning" class="alert alert-warning">{{ pullInfo.analysis.message }}</div>
<div ng-switch-when="notimplemented" class="alert alert-warning">
<p>For {{ TriggerService.getTitle(trigger.service) }} triggers, we are unable to determine dependencies automatically.</p>
<p>If the git repository being built depends on a private base image, you must manually select a robot account with the proper permissions.</p>
</div>
</div>
<div class="dockerfile-found" ng-if="pullInfo.analysis.is_public === false">
<div class="dockerfile-found-content">
A robot account is <strong>required</strong> for this build trigger because the
Dockerfile found
pulls from the private <span class="registry-name"></span> repository
<a href="/repository/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}" ng-safenewtab>
{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}
</a>
</div>
</div>
<div style="margin-bottom: 12px">
Please select the credentials to use when pulling the base image:
</div>
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
<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>
<!-- Namespace admin -->
<div ng-show="isNamespaceAdmin(repository.namespace)">
<!-- Select credentials -->
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default"
ng-class="pullInfo.is_public ? 'active btn-info' : ''"
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 ci-robot"></i>
Robot account
</button>
</div>
<!-- Robot Select -->
<div ng-show="!pullInfo.is_public" style="margin-top: 10px">
<div class="entity-search" namespace="repository.namespace"
placeholder="'Select robot account for pulling...'"
current-entity="pullInfo.pull_entity"
allowed-entities="['robot']"></div>
<div ng-if="pullInfo.analysis.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
<strong>Note</strong>: We've automatically selected robot account
<span class="entity-reference" entity="pullInfo.analysis.robots[0]"></span>,
since it has access to the private repository.
</div>
<div ng-if="!pullInfo.analysis.robots.length && pullInfo.analysis.name"
style="margin-top: 20px; margin-bottom: 0px;">
<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/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}/admin" ng-safenewtab>
repository's admin panel.
</a>
</div>
</div>
</div>
</div>
<div class="trigger-option-section" ng-show="currentView == 'postActivation'">
<div ng-if="trigger.config.credentials" class="credentials" trigger="trigger"></div>
<div ng-if="!trigger.config.credentials">
<div class="alert alert-success">The trigger has been successfully created.</div>
</div>
</div>
</div>
<div class="modal-footer" ng-show="currentView != 'activating'">
<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"
ng-disabled="!trigger.$ready || (!pullInfo['is_public'] && !pullInfo['pull_entity'])"
ng-click="activate()"
ng-show="currentView == 'analyzed'">Create Trigger</button>
<button type="button" class="btn btn-success" ng-click="runTriggerNow()"
ng-if="currentView == 'postActivation'">Run Trigger Now</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ currentView == 'postActivation' ? 'Done' : 'Cancel' }}</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -1,9 +0,0 @@
<div class="step-view-step-content">
<div ng-show="!loading">
<div ng-transclude></div>
</div>
<div ng-show="loading" class="loading-message">
<span class="cor-loader-inline"></span>
{{ loadMessage }}
</div>
</div>

View file

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

View file

@ -1,40 +0,0 @@
<div class="trigger-setup-custom-element">
<div class="selected-info" ng-show="nextStepCounter > 0">
<table style="width: 100%;">
<tr ng-show="nextStepCounter > 0">
<td width="200px">Repository</td>
<td>{{ state.build_source }}</td>
</tr>
<tr ng-show="nextStepCounter > 1">
<td>Dockerfile Location:</td>
<td>
<div class="dockerfile-location">
<i class="fa fa-folder fa-lg"></i> {{ state.subdir || '/' }}
</div>
</td>
</table>
</div>
<!-- Step view -->
<div class="step-view" next-step-counter="nextStepCounter" current-step-valid="currentStepValid"
steps-completed="stepsCompleted()">
<!-- Git URL Input -->
<!-- TODO(jschorr): make nopLoad(callback) no longer required -->
<div class="step-view-step" complete-condition="trigger['config']['build_source']" load-callback="nopLoad(callback)"
load-message="Loading Git URL Input">
<div style="margin-bottom: 12px;">Please enter an HTTP or SSH style URL used to clone your git repository:</div>
<input class="form-control" type="text" placeholder="git@example.com:namespace/repository.git" style="width: 100%;"
ng-model="state.build_source" ng-pattern="/(((http|https):\/\/)(.+)|\w+@(.+):(.+))/">
</div>
<!-- Dockerfile folder select -->
<div class="step-view-step" complete-condition="trigger.$ready" load-callback="nopLoad(callback)"
load-message="Loading Folder Input">
<div style="margin-bottom: 12px">Dockerfile Location:</div>
<input class="form-control" type="text" placeholder="/" style="width: 100%;"
ng-model="state.subdir" ng-pattern="/^($|\/|\/.+)/">
</div>
</div>
</div>

View file

@ -1,201 +0,0 @@
<div class="trigger-setup-githost-element">
<!-- Current selected info -->
<div class="selected-info" ng-show="nextStepCounter > 0">
<table style="width: 100%;">
<tr ng-show="state.currentRepo && nextStepCounter > 0">
<td width="200px">
Repository:
</td>
<td>
<div class="current-repo">
<i class="dropdown-select-icon org-icon fa" ng-class="scmIcon(kind)"
ng-show="!state.currentRepo.avatar_url"></i>
<img class="dropdown-select-icon org-icon"
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}"
ng-show="state.currentRepo.avatar_url">
{{ state.currentRepo.repo }}
</div>
</td>
</tr>
<tr ng-show="nextStepCounter > 1">
<td>
Branches and Tags:
</td>
<td>
<div class="ref-filter">
<span ng-if="!state.hasBranchTagFilter">(Build All)</span>
<span ng-if="state.hasBranchTagFilter">Regular Expression: <code>{{ state.branchTagFilter }}</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 -->
<div class="step-view-step" complete-condition="state.currentRepo" load-callback="loadRepositories(callback)"
load-message="Loading Repositories">
<div style="margin-bottom: 12px">Please choose the repository that will trigger the build:</div>
<div class="dropdown-select" placeholder="'Enter or select a repository'" selected-item="state.currentRepo"
lookahead-items="repoLookahead" allow-custom-input="true">
<!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-lg" ng-class="scmIcon(kind)"></i>
<img class="dropdown-select-icon org-icon"
ng-show="state.currentRepo.avatar_url"
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}">
<i class="dropdown-select-icon org-icon fa fa-lg" ng-class="scmIcon(kind)"
ng-show="!state.currentRepo.avatar_url"></i>
<!-- Dropdown menu -->
<ul class="dropdown-select-menu scrollable-menu" role="menu">
<li ng-repeat-start="org in orgs" role="presentation" class="dropdown-header org-header">
<img ng-src="{{ org.info.avatar_url }}" class="org-icon">{{ org.info.name }}
</li>
<li ng-repeat="repo in org.repos" class="trigger-repo-listing">
<a ng-click="selectRepo(repo, org)">
<i class="fa fa-lg" ng-class="scmIcon(kind)"></i> {{ repo }}
</a>
</li>
<li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li>
</ul>
</div>
</div>
<!-- Branch/Tag filter/select -->
<div class="step-view-step" complete-condition="!state.hasBranchTagFilter || state.branchTagFilter"
load-callback="loadBranchesAndTags(callback)"
load-message="Loading Branches and Tags">
<div style="margin-bottom: 12px">Please choose the branches and tags 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.hasBranchTagFilter ? '' : 'active btn-info'" ng-click="state.hasBranchTagFilter = false">
All Branches and Tags
</button>
<button type="button" class="btn btn-default"
ng-class="state.hasBranchTagFilter ? 'active btn-info' : ''" ng-click="state.hasBranchTagFilter = true">
Matching Regular Expression
</button>
</div>
<div ng-show="state.hasBranchTagFilter" style="margin-top: 10px;">
<form>
<div class="form-group">
<div class="input-group">
<input class="form-control" type="text" ng-model="state.branchTagFilter"
placeholder="(Regular expression. Examples: heads/branchname, tags/tagname)" required>
<div class="dropdown input-group-addon">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu pull-right">
<li><a ng-click="state.branchTagFilter = 'heads/.+'">
<i class="fa fa-code-fork"></i>All Branches</a>
</li>
<li><a ng-click="state.branchTagFilter = 'tags/.+'">
<i class="fa fa-tag"></i>All Tags</a>
</li>
</ul>
</div>
</div>
</div>
</form>
<div style="margin-top: 10px">
<div class="ref-matches" ng-if="branchNames.length">
<span class="kind">Branches:</span>
<ul class="matching-refs branches">
<li ng-repeat="branchName in branchNames | limitTo:20"
class="ref-reference"
ng-class="isMatching('heads', branchName, state.branchTagFilter) ? 'match' : 'not-match'">
<span ng-click="addRef('heads', branchName)" ng-safenewtab>
{{ branchName }}
</span>
</li>
</ul>
<span ng-if="branchNames.length > 20">...</span>
</div>
<div class="ref-matches" ng-if="tagNames.length">
<span class="kind">Tags:</span>
<ul class="matching-refs tags">
<li ng-repeat="tagName in tagNames | limitTo:20"
class="ref-reference"
ng-class="isMatching('tags', tagName, state.branchTagFilter) ? 'match' : 'not-match'">
<span ng-click="addRef('tags', tagName)" ng-safenewtab>
{{ tagName }}
</span>
</li>
</ul>
<span ng-if="tagNames.length > 20">...</span>
</div>
<div ng-if="state.branchTagFilter && !branchNames.length"
style="margin-top: 10px">
<strong>Warning:</strong> No branches found
</div>
</div>
</div>
</div>
</div>
<!-- Dockerfile folder select -->
<div class="step-view-step" complete-condition="trigger.$ready" load-callback="loadLocations(callback)"
load-message="Loading Folders">
<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"
hide-dropdown="!supportsFullListing">
<!-- Icons -->
<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="!state.isInvalidLocation"></i>
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
<!-- Dropdown menu -->
<ul class="dropdown-select-menu" role="menu">
<li ng-repeat="location in locations">
<a ng-click="setLocation(location)" ng-if="!location">
<i class="fa fa-github fa-lg"></i> Repository Root
</a>
<a 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>
</ul>
</div>
<div class="cor-loader" ng-show="!locations && !locationError"></div>
<div class="alert alert-warning" ng-show="locationError">
{{ locationError }}
</div>
<div class="alert alert-warning" ng-show="locations && !locations.length && supportsFullListing">
Warning: No Dockerfiles were found in {{ state.currentRepo.repo }}
</div>
<div class="alert alert-info" ng-show="locations.length && state.isInvalidLocation && supportsFullListing">
Note: The folder does not currently exist or contain a Dockerfile
</div>
</div>
<!-- /step-view -->
</div>
</div>

View file

@ -1,4 +1,4 @@
<span>
<i class="fa fa-git-square fa-lg" style="margin-right: 6px;" data-title="git" bs-tooltip="tooltip.title"></i>
Push to {{ trigger.config.build_source }}
Push to repository {{ trigger.config.build_source }}
</span>

View file

@ -29,11 +29,9 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$scope.currentFilter = null;
$scope.currentStartTrigger = null;
$scope.currentSetupTrigger = null;
$scope.showBuildDialogCounter = 0;
$scope.showTriggerStartDialogCounter = 0;
$scope.showTriggerSetupCounter = 0;
$scope.triggerCredentialsModalTrigger = null;
$scope.triggerCredentialsModalCounter = 0;
@ -144,16 +142,6 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$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.newtrigger;
if (newTriggerId) {
$scope.triggers.map(function(trigger) {
if (trigger['id'] == newTriggerId && !trigger['is_active']) {
$scope.setupTrigger(trigger);
}
});
}
});
};
@ -208,18 +196,6 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$scope.showTriggerStartDialogCounter++;
};
$scope.cancelSetupTrigger = function(trigger) {
if ($scope.currentSetupTrigger != trigger) { return; }
$scope.currentSetupTrigger = null;
$scope.deleteTrigger(trigger);
};
$scope.setupTrigger = function(trigger) {
$scope.currentSetupTrigger = trigger;
$scope.showTriggerSetupCounter++;
};
$scope.deleteTrigger = function(trigger, opt_callback) {
if (!trigger) { return; }

View file

@ -0,0 +1,54 @@
/**
* An element which displays a list of selectable paths containing Dockerfiles.
*/
angular.module('quay').directive('dockerfilePathSelect', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/dockerfile-path-select.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'currentPath': '=currentPath',
'isValidPath': '=?isValidPath',
'paths': '=paths',
'supportsFullListing': '=supportsFullListing'
},
controller: function($scope, $element) {
$scope.isUnknownPath = true;
$scope.selectedPath = null;
var checkPath = function() {
$scope.isUnknownPath = false;
$scope.isValidPath = false;
var path = $scope.currentPath || '';
if (path.length == 0 || path[0] != '/') {
return;
}
$scope.isValidPath = true;
if (!$scope.paths) {
return;
}
$scope.isUnknownPath = $scope.supportsFullListing && $scope.paths.indexOf(path) < 0;
};
$scope.setPath = function(path) {
$scope.currentPath = path;
$scope.selectedPath = null;
};
$scope.setSelectedPath = function(path) {
$scope.currentPath = path;
$scope.selectedPath = path;
};
$scope.$watch('currentPath', checkPath);
$scope.$watch('paths', checkPath);
}
};
return directiveDefinitionObject;
});

View file

@ -28,36 +28,29 @@ angular.module('quay').directive('dropdownSelect', function ($compile) {
}
$scope.placeholder = $scope.placeholder || '';
$scope.internalItem = null;
$scope.lookaheadSetup = false;
// Setup lookahead.
var input = $($element).find('.lookahead-input');
$scope.$watch('clearValue', function(cv) {
if (cv) {
if (cv && $scope.lookaheadSetup) {
$scope.selectedItem = null;
$(input).val('');
$(input).typeahead('val', '');
$(input).typeahead('close');
}
});
$scope.$watch('selectedItem', function(item) {
if ($scope.selectedItem == $scope.internalItem) {
// The item has already been set due to an internal action.
return;
}
if ($scope.selectedItem != null) {
$(input).val(item.toString());
} else {
$(input).val('');
if (item != null && $scope.lookaheadSetup) {
$(input).typeahead('val', item.toString());
$(input).typeahead('close');
}
});
$scope.$watch('lookaheadItems', function(items) {
$(input).off();
if (!items) {
return;
}
items = items || [];
var formattedItems = [];
for (var i = 0; i < items.length; ++i) {
@ -80,7 +73,10 @@ angular.module('quay').directive('dropdownSelect', function ($compile) {
});
dropdownHound.initialize();
$(input).typeahead({}, {
$(input).typeahead({
'hint': false,
'highlight': false
}, {
source: dropdownHound.ttAdapter(),
templates: {
'suggestion': function (datum) {
@ -92,7 +88,6 @@ angular.module('quay').directive('dropdownSelect', function ($compile) {
$(input).on('input', function(e) {
$scope.$apply(function() {
$scope.internalItem = null;
$scope.selectedItem = null;
if ($scope.handleInput) {
$scope.handleInput({'input': $(input).val()});
@ -102,7 +97,6 @@ angular.module('quay').directive('dropdownSelect', function ($compile) {
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
$scope.internalItem = datum['item'] || datum['value'];
$scope.selectedItem = datum['item'] || datum['value'];
if ($scope.handleItemSelected) {
$scope.handleItemSelected({'datum': datum});
@ -111,6 +105,7 @@ angular.module('quay').directive('dropdownSelect', function ($compile) {
});
$rootScope.__dropdownSelectCounter++;
$scope.lookaheadSetup = true;
});
},
link: function(scope, element, attrs) {

View file

@ -0,0 +1,141 @@
/**
* An element which displays a linear workflow of sections, each completed in order before the next
* step is made visible.
*/
angular.module('quay').directive('linearWorkflow', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/linear-workflow.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'workflowState': '=?workflowState',
'workflowComplete': '&workflowComplete',
'doneTitle': '@doneTitle'
},
controller: function($scope, $element, $timeout) {
$scope.sections = [];
$scope.nextSection = function() {
if (!$scope.currentSection.valid) { return; }
var currentIndex = $scope.currentSection.index;
if (currentIndex + 1 >= $scope.sections.length) {
$scope.workflowComplete();
return;
}
$scope.workflowState = $scope.sections[currentIndex + 1].id;
};
this.registerSection = function(sectionScope, sectionElement) {
// Add the section to the list.
var sectionInfo = {
'index': $scope.sections.length,
'id': sectionScope.sectionId,
'title': sectionScope.sectionTitle,
'scope': sectionScope,
'element': sectionElement
};
$scope.sections.push(sectionInfo);
// Add a watch on the `sectionValid` value on the section itself. If/when this value
// changes, we copy it over to the sectionInfo, so that the overall workflow can watch
// the change.
sectionScope.$watch('sectionValid', function(isValid) {
sectionInfo['valid'] = isValid;
if (!isValid) {
// Reset the sections back to this section.
updateState();
}
});
// Bind the `submitSection` callback to move to the next section when the user hits
// enter (which calls this method on the scope via an ng-submit set on a wrapping
// <form> tag).
sectionScope.submitSection = function() {
$scope.nextSection();
};
// Update the state of the workflow to account for the new section.
updateState();
};
var updateState = function() {
// Find the furthest state we can show.
var foundIndex = 0;
var maxValidIndex = -1;
$scope.sections.forEach(function(section, index) {
if (section.id == $scope.workflowState) {
foundIndex = index;
}
if (maxValidIndex == index - 1 && section.valid) {
maxValidIndex = index;
}
});
var minSectionIndex = Math.min(maxValidIndex + 1, foundIndex);
$scope.sections.forEach(function(section, index) {
section.scope.sectionVisible = index <= minSectionIndex;
section.scope.isCurrentSection = false;
});
$scope.workflowState = null;
if (minSectionIndex >= 0 && minSectionIndex < $scope.sections.length) {
$scope.currentSection = $scope.sections[minSectionIndex];
$scope.workflowState = $scope.currentSection.id;
$scope.currentSection.scope.isCurrentSection = true;
// Focus to the first input (if any) in the section.
$timeout(function() {
var inputs = $scope.currentSection.element.find('input');
if (inputs.length == 1) {
inputs.focus();
}
}, 10);
}
};
$scope.$watch('workflowState', updateState);
}
};
return directiveDefinitionObject;
});
/**
* An element which displays a single section in a linear workflow.
*/
angular.module('quay').directive('linearWorkflowSection', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/linear-workflow-section.html',
replace: false,
transclude: true,
restrict: 'C',
require: '^linearWorkflow',
scope: {
'sectionId': '@sectionId',
'sectionTitle': '@sectionTitle',
'sectionValid': '=?sectionValid',
},
link: function($scope, $element, $attrs, $ctrl) {
$ctrl.registerSection($scope, $element);
},
controller: function($scope, $element) {
$scope.$watch('sectionVisible', function(visible) {
if (visible) {
$element.show();
} else {
$element.hide();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,27 @@
/**
* An element which displays the setup and management workflow for a custom git trigger.
*/
angular.module('quay').directive('manageTriggerCustomGit', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/manage-trigger-custom-git.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'trigger': '=trigger',
'activateTrigger': '&activateTrigger'
},
controller: function($scope, $element) {
$scope.config = {};
$scope.currentState = null;
$scope.$watch('trigger', function(trigger) {
if (trigger) {
$scope.config = trigger['config'] || {};
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,306 @@
/**
* An element which displays the setup and management workflow for a normal SCM git trigger.
*/
angular.module('quay').directive('manageTriggerGithost', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/manage-trigger-githost.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger',
'activateTrigger': '&activateTrigger'
},
controller: function($scope, $element, ApiService, TableService, TriggerService, RolesService) {
$scope.TableService = TableService;
$scope.config = {};
$scope.local = {};
$scope.currentState = null;
$scope.namespacesPerPage = 10;
$scope.repositoriesPerPage = 10;
$scope.robotsPerPage = 10;
$scope.local.namespaceOptions = {
'filter': '',
'predicate': 'score',
'reverse': false,
'page': 0
};
$scope.local.repositoryOptions = {
'filter': '',
'predicate': 'last_updated',
'reverse': false,
'page': 0,
'hideStale': true
};
$scope.local.robotOptions = {
'filter': '',
'predicate': 'can_read',
'reverse': false,
'page': 0
};
$scope.getTriggerIcon = function() {
return TriggerService.getIcon($scope.trigger.service);
};
$scope.createTrigger = function() {
var config = {
'build_source': $scope.local.selectedRepository.full_name,
'subdir': $scope.local.dockerfilePath.substr(1) // Remove starting /
};
if ($scope.local.triggerOptions.hasBranchTagFilter &&
$scope.local.triggerOptions.branchTagFilter) {
config['branchtag_regex'] = $scope.local.triggerOptions.branchTagFilter;
}
var activate = function() {
$scope.activateTrigger({'config': config, 'pull_robot': $scope.local.robotAccount});
};
if ($scope.local.robotAccount) {
if ($scope.local.robotAccount.can_read) {
activate();
} else {
// Add read permission onto the base repository for the robot and then activate the
// trigger.
var robot_name = $scope.local.robotAccount.name;
RolesService.setRepositoryRole($scope.repository, 'read', 'robot', robot_name, activate);
}
} else {
activate();
}
};
var buildOrderedNamespaces = function() {
if (!$scope.local.namespaces) {
return;
}
var namespaces = $scope.local.namespaces || [];
$scope.local.orderedNamespaces = TableService.buildOrderedItems(namespaces,
$scope.local.namespaceOptions,
['id'],
['score'])
$scope.local.maxScore = 0;
namespaces.forEach(function(namespace) {
$scope.local.maxScore = Math.max(namespace.score, $scope.local.maxScore);
});
};
var loadNamespaces = function() {
$scope.local.namespaces = null;
$scope.local.selectedNamespace = null;
$scope.local.orderedNamespaces = null;
$scope.local.selectedRepository = null;
$scope.local.orderedRepositories = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
ApiService.listTriggerBuildSourceNamespaces(null, params).then(function(resp) {
$scope.local.namespaces = resp['namespaces'];
$scope.local.repositories = null;
buildOrderedNamespaces();
}, ApiService.errorDisplay('Could not retrieve the list of ' + $scope.namespaceTitle))
};
var buildOrderedRepositories = function() {
if (!$scope.local.repositories) {
return;
}
var repositories = $scope.local.repositories || [];
repositories.forEach(function(repository) {
repository['last_updated_datetime'] = new Date(repository['last_updated'] * 1000);
});
if ($scope.local.repositoryOptions.hideStale) {
var existingRepositories = repositories;
repositories = repositories.filter(function(repository) {
var older_date = moment(repository['last_updated_datetime']).add(1, 'months');
return !moment().isAfter(older_date);
});
if (existingRepositories.length > 0 && repositories.length == 0) {
repositories = existingRepositories;
}
}
$scope.local.orderedRepositories = TableService.buildOrderedItems(repositories,
$scope.local.repositoryOptions,
['name', 'description'],
[]);
};
var loadRepositories = function(namespace) {
$scope.local.repositories = null;
$scope.local.selectedRepository = null;
$scope.local.repositoryRefs = null;
$scope.local.triggerOptions = {
'hasBranchTagFilter': false
};
$scope.local.orderedRepositories = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var data = {
'namespace': namespace.id
};
ApiService.listTriggerBuildSources(data, params).then(function(resp) {
if (namespace == $scope.local.selectedNamespace) {
$scope.local.repositories = resp['sources'];
buildOrderedRepositories();
}
}, ApiService.errorDisplay('Could not retrieve repositories'));
};
var loadRepositoryRefs = function(repository) {
$scope.local.repositoryRefs = null;
$scope.local.triggerOptions = {
'hasBranchTagFilter': false
};
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id,
'field_name': 'refs'
};
var config = {
'build_source': repository.full_name
};
ApiService.listTriggerFieldValues(config, params).then(function(resp) {
if (repository == $scope.local.selectedRepository) {
$scope.local.repositoryRefs = resp['values'];
$scope.local.repositoryFullRefs = resp['values'].map(function(ref) {
var kind = ref.kind == 'branch' ? 'heads' : 'tags';
var icon = ref.kind == 'branch' ? 'fa-code-fork' : 'fa-tag';
return {
'value': kind + '/' + ref.name,
'icon': icon,
'title': ref.name
};
});
}
}, ApiService.errorDisplay('Could not retrieve repository refs'));
};
var loadDockerfileLocations = function(repository) {
$scope.local.dockerfilePath = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var config = {
'build_source': repository.full_name
};
ApiService.listBuildTriggerSubdirs(config, params).then(function(resp) {
if (repository == $scope.local.selectedRepository) {
$scope.local.dockerfileLocations = resp;
}
}, ApiService.errorDisplay('Could not retrieve Dockerfile locations'));
};
var buildOrderedRobotAccounts = function() {
if (!$scope.local.triggerAnalysis || !$scope.local.triggerAnalysis.robots) {
return;
}
var robots = $scope.local.triggerAnalysis.robots;
$scope.local.orderedRobotAccounts = TableService.buildOrderedItems(robots,
$scope.local.robotOptions,
['name'],
[]);
};
var checkDockerfilePath = function(repository, path) {
$scope.local.triggerAnalysis = null;
$scope.local.robotAccount = null;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger.id
};
var config = {
'build_source': repository.full_name,
'subdir': path.substr(1)
};
var data = {
'config': config
};
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
$scope.local.triggerAnalysis = resp;
buildOrderedRobotAccounts();
}, ApiService.errorDisplay('Could not analyze trigger'));
};
$scope.$watch('trigger', function(trigger) {
if (trigger && $scope.repository) {
$scope.config = trigger['config'] || {};
$scope.namespaceTitle = 'organization';
$scope.local.selectedNamespace = null;
loadNamespaces();
}
});
$scope.$watch('local.selectedNamespace', function(namespace) {
if (namespace) {
loadRepositories(namespace);
}
});
$scope.$watch('local.selectedRepository', function(repository) {
if (repository) {
loadRepositoryRefs(repository);
loadDockerfileLocations(repository);
}
});
$scope.$watch('local.dockerfilePath', function(path) {
if (path && $scope.local.selectedRepository) {
checkDockerfilePath($scope.local.selectedRepository, path);
}
});
$scope.$watch('local.namespaceOptions.predicate', buildOrderedNamespaces);
$scope.$watch('local.namespaceOptions.reverse', buildOrderedNamespaces);
$scope.$watch('local.namespaceOptions.filter', buildOrderedNamespaces);
$scope.$watch('local.repositoryOptions.predicate', buildOrderedRepositories);
$scope.$watch('local.repositoryOptions.reverse', buildOrderedRepositories);
$scope.$watch('local.repositoryOptions.filter', buildOrderedRepositories);
$scope.$watch('local.repositoryOptions.hideStale', buildOrderedRepositories);
$scope.$watch('local.robotOptions.predicate', buildOrderedRobotAccounts);
$scope.$watch('local.robotOptions.reverse', buildOrderedRobotAccounts);
$scope.$watch('local.robotOptions.filter', buildOrderedRobotAccounts);
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,36 @@
/**
* An element which displays the matches and non-matches for a regular expression against a set of
* items.
*/
angular.module('quay').directive('regexMatchView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/regex-match-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'regex': '=regex',
'items': '=items'
},
controller: function($scope, $element) {
$scope.filterMatches = function(regexstr, items, shouldMatch) {
regexstr = regexstr || '.+';
try {
var regex = new RegExp(regexstr);
} catch (ex) {
return null;
}
return items.filter(function(item) {
var value = item['value'];
var m = value.match(regex);
var matches = !!(m && m[0].length == value.length);
return matches == shouldMatch;
});
};
}
};
return directiveDefinitionObject;
});

View file

@ -1,126 +0,0 @@
/**
* An element which displays the steps of the wizard-like dialog, changing them as each step
* is completed.
*/
angular.module('quay').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) {
var currentStepIndex = -1;
var steps = [];
var watcher = null;
// Members on 'this' are accessed by the individual steps.
this.register = function(scope, element) {
element.hide();
steps.push({
'scope': scope,
'element': element
});
nextStep();
};
var getCurrentStep = function() {
return steps[currentStepIndex];
};
var reset = function() {
currentStepIndex = -1;
for (var i = 0; i < steps.length; ++i) {
steps[i].element.hide();
}
$scope.currentStepValid = false;
};
var next = function() {
if (currentStepIndex >= 0) {
var currentStep = getCurrentStep();
if (!currentStep || !currentStep.scope) { return; }
if (!currentStep.scope.completeCondition) {
return;
}
currentStep.element.hide();
if (unwatch) {
unwatch();
unwatch = null;
}
}
currentStepIndex++;
if (currentStepIndex < steps.length) {
var currentStep = getCurrentStep();
currentStep.element.show();
currentStep.scope.load()
unwatch = currentStep.scope.$watch('completeCondition', function(cc) {
$scope.currentStepValid = !!cc;
});
} else {
$scope.stepsCompleted();
}
};
var nextStep = function() {
if (!steps || !steps.length) { return; }
if ($scope.nextStepCounter >= 0) {
next();
} else {
reset();
}
};
$scope.$watch('nextStepCounter', nextStep);
}
};
return directiveDefinitionObject;
});
/**
* A step in the step view.
*/
angular.module('quay').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;
});

View file

@ -1,49 +0,0 @@
/**
* An element which displays custom git-specific setup information for its build triggers.
*/
angular.module('quay').directive('triggerSetupCustom', function() {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-setup-custom.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger',
'nextStepCounter': '=nextStepCounter',
'currentStepValid': '=currentStepValid',
'analyze': '&analyze'
},
controller: function($scope, $element, ApiService) {
$scope.analyzeCounter = 0;
$scope.setupReady = false;
$scope.state = {
'build_source': null,
'subdir': null
};
$scope.stepsCompleted = function() {
$scope.analyze({'isValid': $scope.state.build_source != null && $scope.state.subdir != null});
};
$scope.$watch('state.build_source', function(build_source) {
$scope.trigger['config']['build_source'] = build_source;
});
$scope.$watch('state.subdir', function(subdir) {
$scope.trigger['config']['subdir'] = subdir;
$scope.trigger.$ready = subdir != null;
});
$scope.nopLoad = function(callback) {
callback();
};
}
};
return directiveDefinitionObject;
});

View file

@ -1,242 +0,0 @@
/**
* An element which displays hosted Git (GitHub, Bitbucket)-specific setup information for its build triggers.
*/
angular.module('quay').directive('triggerSetupGithost', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-setup-githost.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger',
'kind': '@kind',
'nextStepCounter': '=nextStepCounter',
'currentStepValid': '=currentStepValid',
'analyze': '&analyze'
},
controller: function($scope, $element, ApiService, TriggerService) {
$scope.analyzeCounter = 0;
$scope.setupReady = false;
$scope.refs = null;
$scope.branchNames = null;
$scope.tagNames = null;
$scope.state = {
'currentRepo': null,
'branchTagFilter': '',
'hasBranchTagFilter': false,
'isInvalidLocation': true,
'currentLocation': null
};
var checkLocation = function() {
var location = $scope.state.currentLocation || '';
$scope.state.isInvalidLocation = $scope.supportsFullListing &&
$scope.locations.indexOf(location) < 0;
};
$scope.isMatching = function(kind, name, filter) {
try {
var patt = new RegExp(filter);
} catch (ex) {
return false;
}
var fullname = (kind + '/' + name);
var m = fullname.match(patt);
return m && m[0].length == fullname.length;
}
$scope.addRef = function(kind, name) {
if ($scope.isMatching(kind, name, $scope.state.branchTagFilter)) {
return;
}
var newFilter = kind + '/' + name;
var existing = $scope.state.branchTagFilter;
if (existing) {
$scope.state.branchTagFilter = '(' + existing + ')|(' + newFilter + ')';
} else {
$scope.state.branchTagFilter = newFilter;
}
}
$scope.stepsCompleted = function() {
$scope.analyze({'isValid': !$scope.state.isInvalidLocation});
};
$scope.loadRepositories = function(callback) {
if (!$scope.trigger || !$scope.repository) { return; }
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.loadBranchesAndTags = function(callback) {
if (!$scope.trigger || !$scope.repository) { return; }
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger['id'],
'field_name': 'refs'
};
ApiService.listTriggerFieldValues($scope.trigger['config'], params).then(function(resp) {
$scope.refs = resp['values'];
$scope.branchNames = [];
$scope.tagNames = [];
for (var i = 0; i < $scope.refs.length; ++i) {
var ref = $scope.refs[i];
if (ref.kind == 'branch') {
$scope.branchNames.push(ref.name);
} else {
$scope.tagNames.push(ref.name);
}
}
callback();
}, ApiService.errorDisplay('Cannot load branch and tag names'));
};
$scope.loadLocations = function(callback) {
if (!$scope.trigger || !$scope.repository) { return; }
$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') {
$scope.locations = [];
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.trigger.$ready = true;
checkLocation();
}
callback();
}, ApiService.errorDisplay('Cannot load locations'));
}
$scope.handleLocationInput = function(location) {
$scope.trigger['config']['subdir'] = location || '';
$scope.trigger.$ready = true;
checkLocation();
};
$scope.handleLocationSelected = function(datum) {
$scope.setLocation(datum['value']);
};
$scope.setLocation = function(location) {
$scope.state.currentLocation = location;
$scope.trigger['config']['subdir'] = location || '';
$scope.trigger.$ready = true;
checkLocation();
};
$scope.selectRepo = function(repo, org) {
$scope.state.currentRepo = {
'repo': repo,
'avatar_url': org['info']['avatar_url'],
'toString': function() {
return this.repo;
}
};
};
$scope.selectRepoInternal = function(currentRepo) {
$scope.trigger.$ready = false;
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': $scope.trigger['id']
};
var repo = currentRepo['repo'];
$scope.trigger['config'] = {
'build_source': repo,
'subdir': ''
};
};
$scope.scmIcon = function(kind) {
return TriggerService.getIcon(kind);
};
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) {
var repoValue = {
'repo': orepos[j],
'avatar_url': org['info']['avatar_url'],
'toString': function() {
return this.repo;
}
};
var datum = {
'name': orepos[j],
'org': org,
'value': orepos[j],
'title': orepos[j],
'item': repoValue
};
repos.push(datum);
}
}
$scope.repoLookahead = repos;
};
$scope.$watch('trigger', function(trigger) {
if (!trigger) { return; }
$scope.supportsFullListing = TriggerService.supportsFullListing(trigger.service)
});
$scope.$watch('state.currentRepo', function(repo) {
if (repo) {
$scope.selectRepoInternal(repo);
}
});
$scope.$watch('state.branchTagFilter', function(bf) {
if (!$scope.trigger) { return; }
if ($scope.state.hasBranchTagFilter) {
$scope.trigger['config']['branchtag_regex'] = bf;
} else {
delete $scope.trigger['config']['branchtag_regex'];
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,89 @@
(function() {
/**
* Trigger setup page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('trigger-setup', 'trigger-setup.html', TriggerSetupCtrl, {
'title': 'Setup build trigger',
'description': 'Setup build trigger',
'newLayout': true
});
}]);
function TriggerSetupCtrl($scope, ApiService, $routeParams, $location, UserService, TriggerService) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var trigger_uuid = $routeParams.triggerid;
var loadRepository = function() {
var params = {
'repository': namespace + '/' + name
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo;
});
};
var loadTrigger = function() {
var params = {
'repository': namespace + '/' + name,
'trigger_uuid': trigger_uuid
};
$scope.triggerResource = ApiService.getBuildTriggerAsResource(params).get(function(trigger) {
$scope.trigger = trigger;
});
};
loadTrigger();
loadRepository();
$scope.state = 'managing';
$scope.activateTrigger = function(config, pull_robot) {
$scope.state = 'activating';
var params = {
'repository': namespace + '/' + name,
'trigger_uuid': trigger_uuid
};
var data = {
'config': config
};
if (pull_robot) {
data['pull_robot'] = pull_robot['name'];
}
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
$scope.state = 'managing';
return ApiService.getErrorMessage(resp) +
'\n\nNote: Errors can occur if you do not have admin access on the repository';
});
ApiService.activateBuildTrigger(data, params).then(function(resp) {
$scope.trigger['is_active'] = true;
$scope.trigger['config'] = resp['config'];
$scope.trigger['pull_robot'] = resp['pull_robot'];
$scope.trigger['repository_url'] = resp['repository_url'];
$scope.state = 'activated';
// If there are no credentials to display, redirect to the builds tab.
if (!$scope.trigger['config'].credentials) {
$location.url('/repository/' + namespace + '/' + name + '?tab=builds');
}
}, errorHandler);
};
$scope.getTriggerIcon = function() {
if (!$scope.trigger) { return ''; }
return TriggerService.getIcon($scope.trigger.service);
};
$scope.getTriggerId = function() {
if (!trigger_uuid) { return ''; }
return trigger_uuid.split('-')[0];
};
}
}());

View file

@ -43,6 +43,9 @@ export function routeConfig(
// Repo Build View
.route('/repository/:namespace/:name/build/:buildid', 'build-view')
// Repo Trigger View
.route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup')
// Create repository notification
.route('/repository/:namespace/:name/create-notification', 'create-repository-notification')

View file

@ -0,0 +1,65 @@
<div class="resource-view trigger-setup-element"
resources="[repositoryResource, triggerResource]"
error-message="'Build trigger not found'">
<div class="page-content">
<div class="cor-title">
<span class="cor-title-link">
<a class="back-link" href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=builds">
<i class="fa fa-hdd-o" style="margin-right: 4px"></i>
{{ repository.namespace }}/{{ repository.name }}
</a>
</span>
<span class="cor-title-content">
<i class="fa" ng-class="getTriggerIcon()"></i>
Setup Build Trigger: {{ getTriggerId() }}
</span>
</div>
<div class="co-main-content-panel" ng-show="state != 'activated' && trigger.is_active">
<div class="co-alert co-alert-info">
Trigger has already been activated.
</div>
</div>
<div class="co-main-content-panel" ng-show="state == 'activated' || !trigger.is_active">
<!-- state = activated -->
<div class="activated" ng-if="state == 'activated'">
<div class="row">
<div class="col-md-offset-3 col-md-6 col-sm-12 col-lg-6 content">
<h3>Trigger has been successfully activated</h3>
<div class="credentials" trigger="trigger"></div>
<div class="button-bar">
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=builds">
Return to {{ repository.namespace }}/{{ repository.name }}
</a>
</div>
</div>
</div>
</div> <!-- /state = activated -->
<!-- state = managing or activating -->
<div ng-if="state == 'managing' || state == 'activating'"
ng-class="{'activating': state == 'activating'}">
<!-- Select the correct flow -->
<div ng-switch on="trigger.service">
<!-- Custom Git -->
<div ng-switch-when="custom-git">
<div class="manage-trigger-custom-git" trigger="trigger"
activate-trigger="activateTrigger(config, pull_robot)"></div>
</div> <!-- /custom-git -->
<!-- Hosted Git (GitHub, Gitlab, BitBucket) -->
<div ng-switch-default>
<div class="manage-trigger-githost" trigger="trigger" repository="repository"
activate-trigger="activateTrigger(config, pull_robot)"></div>
</div> <!-- /hosted -->
</div> <!-- /ngSwitch -->
<div class="activating-message" ng-show="state == 'activating'">
<div class="cor-loader-inline"></div><b>Completing setup of the build trigger</b>
</div>
</div> <!-- /state = managing -->
</div> <!-- /co-main-content-panel -->
</div>
</div>