Add the builds tab
This commit is contained in:
parent
347bf31f2d
commit
333e0acd6d
25 changed files with 668 additions and 60 deletions
|
@ -2084,11 +2084,14 @@ def get_repository_build(build_uuid):
|
|||
|
||||
|
||||
def list_repository_builds(namespace_name, repository_name, limit,
|
||||
include_inactive=True):
|
||||
include_inactive=True, since=None):
|
||||
query = (_get_build_base_query()
|
||||
.where(Repository.name == repository_name, Namespace.username == namespace_name)
|
||||
.limit(limit))
|
||||
|
||||
if since is not None:
|
||||
query = query.where(RepositoryBuild.started >= since)
|
||||
|
||||
if not include_inactive:
|
||||
query = query.where(RepositoryBuild.phase != 'error',
|
||||
RepositoryBuild.phase != 'complete')
|
||||
|
|
|
@ -148,12 +148,17 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
@require_repo_read
|
||||
@parse_args
|
||||
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
|
||||
@query_param('since', 'Returns all builds since the given unix timecode', type=int, default=None)
|
||||
@nickname('getRepoBuilds')
|
||||
def get(self, args, namespace, repository):
|
||||
""" Get the list of repository builds. """
|
||||
limit = args['limit']
|
||||
builds = list(model.list_repository_builds(namespace, repository, limit))
|
||||
limit = args.get('limit', 5)
|
||||
since = args.get('since', None)
|
||||
|
||||
if since is not None:
|
||||
since = datetime.datetime.utcfromtimestamp(since)
|
||||
|
||||
builds = model.list_repository_builds(namespace, repository, limit, since=since)
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
return {
|
||||
'builds': [build_status_view(build, can_write) for build in builds]
|
||||
|
|
|
@ -247,6 +247,7 @@ def github_oauth_attach():
|
|||
|
||||
|
||||
@callback.route('/github/callback/trigger/<path:repository>', methods=['GET'])
|
||||
@callback.route('/github/callback/trigger/<path:repository>/__new', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_BUILD)
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
|
@ -260,9 +261,18 @@ def attach_github_build_trigger(namespace, repository):
|
|||
abort(404, message=msg)
|
||||
|
||||
trigger = model.create_build_trigger(repo, 'github', token, current_user.db_user())
|
||||
|
||||
# TODO(jschorr): Remove once the new layout is in place.
|
||||
admin_path = '%s/%s/%s' % (namespace, repository, 'admin')
|
||||
full_url = '%s%s%s' % (url_for('web.repository', path=admin_path), '?tab=trigger&new_trigger=',
|
||||
trigger.uuid)
|
||||
|
||||
if '__new' in request.url:
|
||||
repo_path = '%s/%s' % (namespace, repository)
|
||||
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
|
||||
trigger.uuid)
|
||||
|
||||
|
||||
logger.debug('Redirecting to full url: %s' % full_url)
|
||||
return redirect(full_url)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ from app import app, oauth_apps, dockerfile_build_queue, LoginWrappedDBUser
|
|||
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
from auth import scopes
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from endpoints.api.discovery import swagger_route_data
|
||||
from werkzeug.routing import BaseConverter
|
||||
from functools import wraps
|
||||
|
@ -225,7 +226,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
|||
'registry': host,
|
||||
'build_subdir': subdir,
|
||||
'trigger_metadata': trigger_metadata or {},
|
||||
'is_manual': manual
|
||||
'is_manual': manual,
|
||||
'manual_user': get_authenticated_user().username if get_authenticated_user() else None
|
||||
}
|
||||
|
||||
with app.config['DB_TRANSACTION_FACTORY'](db):
|
||||
|
@ -250,7 +252,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
|||
'repo': repository.name,
|
||||
'namespace': repository.namespace_user.username,
|
||||
'fileid': dockerfile_id,
|
||||
'manual': manual,
|
||||
'is_manual': manual,
|
||||
'manual_user': get_authenticated_user().username if get_authenticated_user() else None
|
||||
}
|
||||
|
||||
if trigger:
|
||||
|
@ -267,7 +270,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
|||
'build_id': build_request.uuid,
|
||||
'build_name': build_name,
|
||||
'docker_tags': tags,
|
||||
'is_manual': manual
|
||||
'is_manual': manual,
|
||||
'manual_user': get_authenticated_user().username if get_authenticated_user() else None
|
||||
}
|
||||
|
||||
if trigger:
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
width: 24px;
|
||||
}
|
||||
|
||||
.co-panel .co-panel-heading i.fa {
|
||||
.co-panel .co-panel-heading > i.fa {
|
||||
margin-right: 6px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
|
@ -887,3 +887,21 @@
|
|||
.co-check-bar .co-filter-box input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.empty-primary-msg {
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-secondary-msg {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
|
29
static/css/directives/repo-view/repo-panel-builds.css
Normal file
29
static/css/directives/repo-view/repo-panel-builds.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
.repo-panel-builds .status-col {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.repo-panel-builds .building-tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.repo-panel-builds .building-tag .fa {
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.repo-panel-builds .heading-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.repo-panel-builds .heading-controls {
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.repo-panel-builds .heading-controls .btn {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.repo-panel-builds .heading-controls .btn .fa {
|
||||
margin-right: 6px;
|
||||
}
|
|
@ -10,14 +10,6 @@
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.build-mini-status .build-status-icon {
|
||||
width: 42px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.build-mini-status .timing {
|
||||
display: inline-block;
|
||||
margin-left: 30px;
|
||||
|
@ -38,16 +30,4 @@
|
|||
line-height: 33px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.build-mini-status .build-status-icon.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.build-mini-status .build-status-icon.internalerror {
|
||||
color: #DFFF00;
|
||||
}
|
||||
|
||||
.build-mini-status .build-status-icon.complete {
|
||||
color: #2fcc66;
|
||||
}
|
19
static/css/directives/ui/build-state-icon.css
Normal file
19
static/css/directives/ui/build-state-icon.css
Normal file
|
@ -0,0 +1,19 @@
|
|||
.build-state-icon {
|
||||
width: 42px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.build-state-icon .error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.build-state-icon .internalerror {
|
||||
color: #DFFF00;
|
||||
}
|
||||
|
||||
.build-state-icon .complete {
|
||||
color: #2fcc66;
|
||||
}
|
15
static/css/directives/ui/filter-control.css
Normal file
15
static/css/directives/ui/filter-control.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.filter-control {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.filter-control a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.filter-control .not-selected a {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.filter-control .selected {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -30,19 +30,6 @@
|
|||
right: 2px;
|
||||
}
|
||||
|
||||
.empty-primary-msg {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-secondary-msg {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.repo-list-title {
|
||||
margin-bottom: 30px;
|
||||
margin-top: 10px;
|
||||
|
|
|
@ -41,4 +41,12 @@
|
|||
|
||||
.triggered-build-description-element .commit-who:before {
|
||||
content: "by ";
|
||||
}
|
||||
|
||||
.triggered-build-description-element .manual {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.triggered-build-description-element .fa-user {
|
||||
margin-right: 4px;
|
||||
}
|
|
@ -12,5 +12,13 @@
|
|||
|
||||
.repository-view .tab-header {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.repository-view .tab-header-controls {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.repository-view .tab-header-controls .btn .fa {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
<a class="build-mini-status-element" href="/repository/{{ build.repository.namespace }}/{{ build.repository.name }}/build/{{ build.id }}">
|
||||
<div>
|
||||
<span class="build-status-icon" ng-class="build.phase">
|
||||
<span class="cor-loader-inline" ng-if="isBuilding(build)"></span>
|
||||
<span ng-if="!isBuilding(build)">
|
||||
<i class="fa fa-check-circle" ng-if="build.phase == 'complete'"></i>
|
||||
<i class="fa fa-times-circle" ng-if="build.phase == 'error'"></i>
|
||||
<i class="fa fa-exclamation-circle" ng-if="build.phase == 'internalerror'"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="build-state-icon" build="build"></span>
|
||||
<span class="timing">
|
||||
<i class="fa fa-clock-o"></i><span am-time-ago="build.started || 0"></span>
|
||||
</span>
|
||||
|
||||
<div class="build-description triggered-build-description" build="build" ng-if="build.trigger"></div>
|
||||
<div class="build-description" ng-if="!build.trigger">Manually Started Build</div>
|
||||
<div class="build-description triggered-build-description" build="build"></div>
|
||||
</div>
|
||||
</a>
|
8
static/directives/build-state-icon.html
Normal file
8
static/directives/build-state-icon.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<span class="build-state-icon-element" ng-class="build.phase">
|
||||
<span class="cor-loader-inline" ng-if="isBuilding(build)"></span>
|
||||
<span ng-if="!isBuilding(build)">
|
||||
<i class="fa fa-check-circle" ng-if="build.phase == 'complete'"></i>
|
||||
<i class="fa fa-times-circle" ng-if="build.phase == 'error'"></i>
|
||||
<i class="fa fa-exclamation-circle" ng-if="build.phase == 'internalerror'"></i>
|
||||
</span>
|
||||
</span>
|
3
static/directives/filter-control.html
Normal file
3
static/directives/filter-control.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<span class="filter-control-element" ng-class="filter == value ? 'selected': 'not-selected'">
|
||||
<a href="javascript:void(0)" ng-click="setFilter()"><span ng-transclude/></a>
|
||||
</span>
|
185
static/directives/repo-view/repo-panel-builds.html
Normal file
185
static/directives/repo-view/repo-panel-builds.html
Normal file
|
@ -0,0 +1,185 @@
|
|||
<div class="repo-panel-builds-element">
|
||||
<div class="tab-header-controls">
|
||||
<button class="btn btn-primary" ng-click="showNewBuildDialog()">
|
||||
<i class="fa fa-plus"></i>Start Build
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="tab-header">Repository Builds</h3>
|
||||
|
||||
<!-- Builds -->
|
||||
<div class="co-panel">
|
||||
<!-- Builds header controls -->
|
||||
<div class="co-panel-heading">
|
||||
<div class="heading-controls hidden-sm hidden-xs">
|
||||
<span class="filter-control" filter="options.filter" value="recent">Recent Builds</span>
|
||||
<span class="filter-control" filter="options.filter" value="48hour">Last 48 Hours</span>
|
||||
<span class="filter-control" filter="options.filter" value="30day">Last 30 days</span>
|
||||
</div>
|
||||
<div class="heading-title">
|
||||
<i class="fa fa-tasks"></i>
|
||||
Build History
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Builds list content -->
|
||||
<div class="panel-body">
|
||||
<div class="resource-view" resource="buildsResource" error-message="'Could not load build information'">
|
||||
|
||||
<!-- No builds found -->
|
||||
<div class="empty" ng-if="!fullBuilds.length">
|
||||
<div class="empty-primary-msg">No matching builds found</div>
|
||||
<div class="empty-secondary-msg">
|
||||
Please change the filter above to search for more builds.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Builds list table -->
|
||||
<table class="co-table" ng-if="fullBuilds.length">
|
||||
<thead>
|
||||
<td class="status-col"></td>
|
||||
<td ng-class="tablePredicateClass('id', options.predicate, options.reverse)">
|
||||
<a href="javascript:void(0)" ng-click="orderBy('id')">Build ID</a>
|
||||
</td>
|
||||
<td ng-class="tablePredicateClass('tags', options.predicate, options.reverse)">
|
||||
<a href="javascript:void(0)" ng-click="orderBy('tags')">Tags</a>
|
||||
</td>
|
||||
<td ng-class="tablePredicateClass('commit_sha', options.predicate, options.reverse)">
|
||||
<a href="javascript:void(0)" ng-click="orderBy('commit_sha')">Triggered By</a>
|
||||
</td>
|
||||
<td ng-class="tablePredicateClass('started_datetime', options.predicate, options.reverse)">
|
||||
<a href="javascript:void(0)" ng-click="orderBy('started_datetime')">Date Started</a>
|
||||
</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="build in fullBuilds">
|
||||
<td><span class="build-state-icon" build="build"></span></td>
|
||||
<td>
|
||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/build/{{ build.id }}">{{ build.id.substr(0, 8) }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="building-tag" ng-repeat="tag in build.building_tags">
|
||||
<i class="fa fa-tag"></i>{{ tag }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="triggered-build-description" build="build"></div>
|
||||
</td>
|
||||
<td>{{ build.started | amCalendar }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /Builds -->
|
||||
|
||||
<!-- Build Triggers -->
|
||||
<div class="co-panel" ng-if="repository.can_admin && TriggerService.getTypes().length">
|
||||
<!-- Builds header controls -->
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-flash"></i>
|
||||
Build Triggers
|
||||
|
||||
<div class="heading-controls hidden-sm hidden-xs">
|
||||
<!-- Add Build Trigger -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
|
||||
Create Build Trigger
|
||||
<b class="caret"></b>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right pull-right">
|
||||
<li ng-repeat="type in TriggerService.getTypes()">
|
||||
<a href="{{ TriggerService.getRedirectUrl(type, repository.namespace, repository.name) }}">
|
||||
<i class="fa fa-lg" ng-class="TriggerService.getMetadata(type).icon"></i>
|
||||
{{ TriggerService.getTitle(type) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Builds list content -->
|
||||
<div class="panel-body">
|
||||
<div class="resource-view" resource="triggersResource" error-message="'Could not load build triggers'">
|
||||
<!-- No Triggers defined -->
|
||||
<div class="empty" ng-if="!triggers.length">
|
||||
<div class="empty-primary-msg">No build triggers defined</div>
|
||||
<div class="empty-secondary-msg">
|
||||
Build triggers invoke builds whenever the triggered condition is met (source control push, webhook, etc)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Triggers list -->
|
||||
<table class="co-table" ng-if="triggers.length">
|
||||
<thead>
|
||||
<td>Trigger Name</td>
|
||||
<td>Dockerfile Location</td>
|
||||
<td>Branches/Tags</td>
|
||||
<td>Pull Robot</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<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 href="javascript:void(0)" ng-click="deleteTrigger(trigger)">Cancel</a>)
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="trigger in triggers | filter:{'is_active':true}">
|
||||
<td><div class="trigger-description" trigger="trigger" short="true"></div></td>
|
||||
<td>{{ trigger.subdir || '(Root Directory)' }}</td>
|
||||
<td>{{ trigger.config.branchtag_regex || '(All)' }}</td>
|
||||
<td>
|
||||
<span class="entity-reference" entity="trigger.pull_robot" ng-if="trigger.pull_robot"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="askRunTrigger(trigger)">
|
||||
<i class="fa fa-chevron-right"></i> Run Trigger Now
|
||||
</span>
|
||||
<span class="cor-option" option-click="askDeleteTrigger(trigger)">
|
||||
<i class="fa fa-times"></i> Delete Trigger
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /Build Triggers -->
|
||||
|
||||
<!-- Delete Tag Confirm -->
|
||||
<div class="cor-confirm-dialog"
|
||||
dialog-context="deleteTriggerInfo"
|
||||
dialog-action="deleteTrigger(info.trigger, callback)"
|
||||
dialog-title="Delete Build Trigger"
|
||||
dialog-action-title="Delete Trigger">
|
||||
Are you sure you want to delete this build trigger? No further builds will be automatically
|
||||
started.
|
||||
</div>
|
||||
|
||||
<!-- Dockerfile build dialog -->
|
||||
<div class="dockerfile-build-dialog"
|
||||
show-now="showBuildDialogCounter"
|
||||
repository="repository"
|
||||
build-started="handleBuildStarted(build)">
|
||||
</div>
|
||||
|
||||
<!-- Setup trigger dialog-->
|
||||
<div class="setup-trigger-dialog"
|
||||
repository="repository"
|
||||
trigger="currentSetupTrigger"
|
||||
canceled="cancelSetupTrigger(trigger)"
|
||||
counter="showTriggerSetupCounter"></div>
|
||||
|
||||
<!-- Manual trigger dialog -->
|
||||
<div class="manual-trigger-build-dialog"
|
||||
repository="repository"
|
||||
trigger="currentStartTrigger"
|
||||
counter="showTriggerStartDialogCounter"
|
||||
start-build="startTrigger(trigger, parameters)"></div>
|
||||
|
||||
</div>
|
|
@ -41,7 +41,7 @@
|
|||
<!-- No Builds -->
|
||||
<div class="empty" ng-if="builds && !builds.length">
|
||||
<div class="empty-primary-msg">No builds have been run for this repository.</div>
|
||||
<div class="empty-secondary-msg" ng-if="repository.can_admin">
|
||||
<div class="empty-secondary-msg" ng-if="repository.can_write">
|
||||
Click on the <i class="fa fa-tasks" style="margin-left: 6px"></i> Builds tab to start a new build.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,9 @@
|
|||
<a href="javascript:void(0)" class="btn btn-default" ng-click="setTab('changes')">
|
||||
<i class="fa fa-code-fork"></i> Visualize
|
||||
</a>
|
||||
<button class="btn btn-default" ng-click="askDeleteMultipleTags(checkedTags.checked)">
|
||||
<button class="btn btn-default"
|
||||
ng-click="askDeleteMultipleTags(checkedTags.checked)"
|
||||
ng-if="repository.can_write">
|
||||
<i class="fa fa-times"></i> Delete
|
||||
</button>
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
<div class="triggered-build-description-element">
|
||||
<span ng-switch on="build.trigger.service">
|
||||
|
||||
<span class="manual" ng-if="!build.trigger && !build.job_config.manual_user">
|
||||
(Manually Triggered Build)
|
||||
</span>
|
||||
|
||||
<span ng-if="!build.trigger && build.job_config.manual_user">
|
||||
<i class="fa fa-user"></i> {{ build.job_config.manual_user }}
|
||||
</span>
|
||||
|
||||
<span ng-switch on="build.trigger.service" ng-if="build.trigger">
|
||||
<!-- GitHub -->
|
||||
<span ng-switch-when="github">
|
||||
<!-- Full Commit Information -->
|
||||
|
|
232
static/js/directives/repo-view/repo-panel-builds.js
Normal file
232
static/js/directives/repo-view/repo-panel-builds.js
Normal file
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
* An element which displays the builds panel for a repository view.
|
||||
*/
|
||||
angular.module('quay').directive('repoPanelBuilds', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/repo-view/repo-panel-builds.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'builds': '=builds'
|
||||
},
|
||||
controller: function($scope, $element, $filter, $routeParams, ApiService, TriggerService) {
|
||||
var orderBy = $filter('orderBy');
|
||||
|
||||
$scope.TriggerService = TriggerService;
|
||||
|
||||
$scope.options = {
|
||||
'filter': 'recent',
|
||||
'reverse': false,
|
||||
'predicate': 'started_datetime'
|
||||
};
|
||||
|
||||
$scope.currentFilter = null;
|
||||
|
||||
$scope.currentStartTrigger = null;
|
||||
$scope.currentSetupTrigger = null;
|
||||
|
||||
$scope.showBuildDialogCounter = 0;
|
||||
$scope.showTriggerStartDialogCounter = 0;
|
||||
$scope.showTriggerSetupCounter = 0;
|
||||
|
||||
var updateBuilds = function() {
|
||||
if (!$scope.allBuilds) { return; }
|
||||
|
||||
var unordered = $scope.allBuilds.map(function(build_info) {
|
||||
var commit_sha = null;
|
||||
|
||||
if (build_info.job_config.trigger_metadata) {
|
||||
commit_sha = build_info.job_config.trigger_metadata.commit_sha;
|
||||
}
|
||||
|
||||
return $.extend(build_info, {
|
||||
'started_datetime': (new Date(build_info.started)).valueOf() * (-1),
|
||||
'building_tags': build_info.job_config.docker_tags,
|
||||
'commit_sha': commit_sha
|
||||
});
|
||||
});
|
||||
|
||||
$scope.fullBuilds = orderBy(unordered, $scope.options.predicate, $scope.options.reverse);
|
||||
};
|
||||
|
||||
var loadBuilds = function() {
|
||||
if (!$scope.builds || !$scope.repository || !$scope.options.filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: We only refresh if the filter has changed.
|
||||
var filter = $scope.options.filter;
|
||||
if ($scope.buildsResource && filter == $scope.currentFilter) { return; }
|
||||
|
||||
var since = null;
|
||||
|
||||
if ($scope.options.filter == '48hours') {
|
||||
since = Math.floor(moment().subtract(2, 'days').valueOf() / 1000);
|
||||
} else if ($scope.options.filter == '30days') {
|
||||
since = Math.floor(moment().subtract(30, 'days').valueOf() / 1000);
|
||||
} else {
|
||||
since = null;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'limit': 100,
|
||||
'since': since
|
||||
};
|
||||
|
||||
$scope.buildsResource = ApiService.getRepoBuildsAsResource(params).get(function(resp) {
|
||||
$scope.allBuilds = resp.builds;
|
||||
$scope.currentFilter = filter;
|
||||
updateBuilds();
|
||||
});
|
||||
};
|
||||
|
||||
var buildsChanged = function() {
|
||||
if (!$scope.allBuilds) {
|
||||
loadBuilds();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.builds || !$scope.repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace any build records with updated records from the server.
|
||||
$scope.builds.map(function(build) {
|
||||
for (var i = 0; i < $scope.allBuilds.length; ++i) {
|
||||
var current = $scope.allBuilds[i];
|
||||
if (current.id == build.id && current.phase != build.phase) {
|
||||
$scope.allBuilds[i] = build;
|
||||
break
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateBuilds();
|
||||
};
|
||||
|
||||
var loadBuildTriggers = function() {
|
||||
if (!$scope.repository || !$scope.repository.can_admin) { return; }
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
$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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('repository', loadBuildTriggers);
|
||||
$scope.$watch('repository', loadBuilds);
|
||||
|
||||
$scope.$watch('builds', buildsChanged);
|
||||
|
||||
$scope.$watch('options.filter', loadBuilds);
|
||||
$scope.$watch('options.predicate', updateBuilds);
|
||||
$scope.$watch('options.reverse', updateBuilds);
|
||||
|
||||
$scope.tablePredicateClass = function(name, predicate, reverse) {
|
||||
if (name != predicate) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'current ' + (reverse ? 'reversed' : '');
|
||||
};
|
||||
|
||||
$scope.orderBy = function(predicate) {
|
||||
if (predicate == $scope.options.predicate) {
|
||||
$scope.options.reverse = !$scope.options.reverse;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.options.reverse = false;
|
||||
$scope.options.predicate = predicate;
|
||||
};
|
||||
|
||||
$scope.askDeleteTrigger = function(trigger) {
|
||||
$scope.deleteTriggerInfo = {
|
||||
'trigger': trigger
|
||||
};
|
||||
};
|
||||
|
||||
$scope.askRunTrigger = function(trigger) {
|
||||
$scope.currentStartTrigger = trigger;
|
||||
$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.startTrigger = function(trigger, opt_custom) {
|
||||
var parameters = TriggerService.getRunParameters(trigger.service);
|
||||
if (parameters.length && !opt_custom) {
|
||||
$scope.currentStartTrigger = trigger;
|
||||
$scope.showTriggerStartDialogCounter++;
|
||||
return;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': trigger.id
|
||||
};
|
||||
|
||||
ApiService.manuallyStartBuildTrigger(opt_custom || {}, params).then(function(resp) {
|
||||
$scope.allBuilds.push(resp);
|
||||
updateBuilds();
|
||||
}, ApiService.errorDisplay('Could not start build'));
|
||||
};
|
||||
|
||||
$scope.deleteTrigger = function(trigger, opt_callback) {
|
||||
if (!trigger) { return; }
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': trigger.id
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not delete build trigger', function() {
|
||||
opt_callback && opt_callback(false);
|
||||
});
|
||||
|
||||
ApiService.deleteBuildTrigger(null, params).then(function(resp) {
|
||||
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
|
||||
opt_callback && opt_callback(true);
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.showBuildDialogCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
$scope.allBuilds.push(build);
|
||||
updateBuilds();
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
22
static/js/directives/ui/build-state-icon.js
Normal file
22
static/js/directives/ui/build-state-icon.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which displays an icon representing the state of the build.
|
||||
*/
|
||||
angular.module('quay').directive('buildStateIcon', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-state-icon.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.isBuilding = function(build) {
|
||||
if (!build) { return true; }
|
||||
return build.phase != 'complete' && build.phase != 'error';
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
22
static/js/directives/ui/filter-control.js
Normal file
22
static/js/directives/ui/filter-control.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which displays a link to change a lookup filter, and shows whether it is selected.
|
||||
*/
|
||||
angular.module('quay').directive('filterControl', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/filter-control.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'filter': '=filter',
|
||||
'value': '@value'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.setFilter = function() {
|
||||
$scope.filter = $scope.value;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -2,8 +2,8 @@
|
|||
* Helper service for defining the various kinds of build triggers and retrieving information
|
||||
* about them.
|
||||
*/
|
||||
angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
|
||||
function(UtilService, $sanitize, KeyService) {
|
||||
angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'KeyService', 'Features', 'CookieService',
|
||||
function(UtilService, $sanitize, KeyService, Features, CookieService) {
|
||||
var triggerService = {};
|
||||
|
||||
var triggerTypes = {
|
||||
|
@ -28,15 +28,46 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
|
|||
var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' +
|
||||
namespace + '/' + repository;
|
||||
|
||||
// TODO(jschorr): Remove once the new layout is in place.
|
||||
if (CookieService.get('quay.exp-new-layout') == 'true') {
|
||||
redirect_uri += '/__new';
|
||||
}
|
||||
|
||||
var authorize_url = KeyService['githubTriggerAuthorizeUrl'];
|
||||
var client_id = KeyService['githubTriggerClientId'];
|
||||
|
||||
return authorize_url + 'client_id=' + client_id +
|
||||
'&scope=repo,user:email&redirect_uri=' + redirect_uri;
|
||||
},
|
||||
|
||||
'is_enabled': function() {
|
||||
return Features.GITHUB_BUILD;
|
||||
},
|
||||
|
||||
'icon': 'fa-github',
|
||||
|
||||
'title': function() {
|
||||
var isEnterprise = KeyService.isEnterprise('github-trigger');
|
||||
if (isEnterprise) {
|
||||
return 'GitHub Enterprise Repository Push';
|
||||
}
|
||||
|
||||
return 'GitHub Repository Push';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
triggerService.getTypes = function() {
|
||||
var types = [];
|
||||
for (var key in triggerTypes) {
|
||||
if (!triggerTypes.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
types.push(key);
|
||||
}
|
||||
return types;
|
||||
};
|
||||
|
||||
triggerService.getRedirectUrl = function(name, namespace, repository) {
|
||||
var type = triggerTypes[name];
|
||||
if (!type) {
|
||||
|
@ -45,6 +76,14 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
|
|||
return type['get_redirect_url'](namespace, repository);
|
||||
};
|
||||
|
||||
triggerService.getTitle = function(name) {
|
||||
var type = triggerTypes[name];
|
||||
if (!type) {
|
||||
return 'Unknown';
|
||||
}
|
||||
return type['title']();
|
||||
};
|
||||
|
||||
triggerService.getDescription = function(name, config) {
|
||||
var type = triggerTypes[name];
|
||||
if (!type) {
|
||||
|
@ -53,6 +92,10 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
|
|||
return type['description'](config);
|
||||
};
|
||||
|
||||
triggerService.getMetadata = function(name) {
|
||||
return triggerTypes[name];
|
||||
};
|
||||
|
||||
triggerService.getRunParameters = function(name, config) {
|
||||
var type = triggerTypes[name];
|
||||
if (!type) {
|
||||
|
|
3
static/lib/angular-moment.min.js
vendored
3
static/lib/angular-moment.min.js
vendored
|
@ -1 +1,2 @@
|
|||
angular.module("angularMoment",[]).directive("amTimeAgo",["$window","$timeout",function(a,b){"use strict";return function(c,d,e){function f(){k&&(b.cancel(k),k=null)}function g(c){d.text(c.fromNow());var e=a.moment().diff(c,"minute"),f=3600;1>e?f=1:60>e?f=30:180>e&&(f=300),k=b(function(){g(c)},1e3*f,!1)}function h(){f(),g(a.moment(i,j))}var i,j,k=null;c.$watch(e.amTimeAgo,function(a){"undefined"!=typeof a&&null!==a&&(angular.isNumber(a)&&(a=new Date(a)),i=a,h())}),e.$observe("amFormat",function(a){j=a,h()}),c.$on("$destroy",function(){f()})}}]).filter("amDateFormat",["$window",function(a){"use strict";return function(b,c){return"undefined"==typeof b||null===b?"":(angular.isNumber(b)&&(b=new Date(b)),a.moment(b).format(c))}}]);
|
||||
"format global";"deps angular";"deps moment";!function(){"use strict";function a(a,b){return a.module("angularMoment",[]).constant("angularMomentConfig",{preprocess:null,timezone:"",format:null,statefulFilters:!0}).constant("moment",b).constant("amTimeAgoConfig",{withoutSuffix:!1,serverTime:null,titleFormat:null}).directive("amTimeAgo",["$window","moment","amMoment","amTimeAgoConfig","angularMomentConfig",function(b,c,d,e,f){return function(g,h,i){function j(){var a;if(e.serverTime){var b=(new Date).getTime(),d=b-u+e.serverTime;a=c(d)}else a=c();return a}function k(){q&&(b.clearTimeout(q),q=null)}function l(a){if(h.text(a.from(j(),s)),t&&!h.attr("title")&&h.attr("title",a.local().format(t)),!x){var c=Math.abs(j().diff(a,"minute")),d=3600;1>c?d=1:60>c?d=30:180>c&&(d=300),q=b.setTimeout(function(){l(a)},1e3*d)}}function m(a){y&&h.attr("datetime",a)}function n(){if(k(),o){var a=d.preprocessDate(o,v,r);l(a),m(a.toISOString())}}var o,p,q=null,r=f.format,s=e.withoutSuffix,t=e.titleFormat,u=(new Date).getTime(),v=f.preprocess,w=i.amTimeAgo.replace(/^::/,""),x=0===i.amTimeAgo.indexOf("::"),y="TIME"===h[0].nodeName.toUpperCase();p=g.$watch(w,function(a){return"undefined"==typeof a||null===a||""===a?(k(),void(o&&(h.text(""),m(""),o=null))):(o=a,n(),void(void 0!==a&&x&&p()))}),a.isDefined(i.amWithoutSuffix)&&g.$watch(i.amWithoutSuffix,function(a){"boolean"==typeof a?(s=a,n()):s=e.withoutSuffix}),i.$observe("amFormat",function(a){"undefined"!=typeof a&&(r=a,n())}),i.$observe("amPreprocess",function(a){v=a,n()}),g.$on("$destroy",function(){k()}),g.$on("amMoment:localeChanged",function(){n()})}}]).service("amMoment",["moment","$rootScope","$log","angularMomentConfig",function(b,c,d,e){this.preprocessors={utc:b.utc,unix:b.unix},this.changeLocale=function(d,e){var f=b.locale(d,e);return a.isDefined(d)&&c.$broadcast("amMoment:localeChanged"),f},this.changeTimezone=function(a){e.timezone=a,c.$broadcast("amMoment:timezoneChanged")},this.preprocessDate=function(c,f,g){return a.isUndefined(f)&&(f=e.preprocess),this.preprocessors[f]?this.preprocessors[f](c,g):(f&&d.warn("angular-moment: Ignoring unsupported value for preprocess: "+f),!isNaN(parseFloat(c))&&isFinite(c)?b(parseInt(c,10)):b(c,g))},this.applyTimezone=function(a){var b=e.timezone;return a&&b&&(a.tz?a=a.tz(b):d.warn("angular-moment: timezone specified but moment.tz() is undefined. Did you forget to include moment-timezone.js?")),a}}]).filter("amCalendar",["moment","amMoment","angularMomentConfig",function(a,b,c){function d(c,d){if("undefined"==typeof c||null===c)return"";c=b.preprocessDate(c,d);var e=a(c);return e.isValid()?b.applyTimezone(e).calendar():""}return d.$stateful=c.statefulFilters,d}]).filter("amDateFormat",["moment","amMoment","angularMomentConfig",function(a,b,c){function d(c,d,e){if("undefined"==typeof c||null===c)return"";c=b.preprocessDate(c,e);var f=a(c);return f.isValid()?b.applyTimezone(f).format(d):""}return d.$stateful=c.statefulFilters,d}]).filter("amDurationFormat",["moment","angularMomentConfig",function(a,b){function c(b,c,d){return"undefined"==typeof b||null===b?"":a.duration(b,c).humanize(d)}return c.$stateful=b.statefulFilters,c}]).filter("amTimeAgo",["moment","amMoment","angularMomentConfig",function(a,b,c){function d(c,d,e){if("undefined"==typeof c||null===c)return"";c=b.preprocessDate(c,d);var f=a(c);return f.isValid()?b.applyTimezone(f).fromNow(e):""}return d.$stateful=c.statefulFilters,d}])}"function"==typeof define&&define.amd?define(["angular","moment"],a):"undefined"!=typeof module&&module&&module.exports?a(angular,require("moment")):a(angular,window.moment)}();
|
||||
//# sourceMappingURL=angular-moment.min.js.map
|
|
@ -21,7 +21,8 @@
|
|||
<i class="fa fa-tags"></i>
|
||||
</span>
|
||||
|
||||
<span class="cor-tab" tab-title="Builds" tab-target="#builds">
|
||||
<span class="cor-tab" tab-title="Builds" tab-target="#builds"
|
||||
quay-show="viewScope.repository.can_write && Features.BUILD_SUPPORT">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</span>
|
||||
|
||||
|
@ -59,7 +60,9 @@
|
|||
|
||||
<!-- Builds -->
|
||||
<div id="builds" class="tab-pane">
|
||||
builds
|
||||
<div class="repo-panel-builds"
|
||||
repository="viewScope.repository"
|
||||
builds="viewScope.builds"></div>
|
||||
</div>
|
||||
|
||||
<!-- Changes -->
|
||||
|
|
Reference in a new issue