Add the builds tab

This commit is contained in:
Joseph Schorr 2015-03-13 15:34:28 -07:00
parent 347bf31f2d
commit 333e0acd6d
25 changed files with 668 additions and 60 deletions

View file

@ -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')

View file

@ -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]

View file

@ -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)

View file

@ -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:

View file

@ -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;
}

View 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;
}

View file

@ -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;
}

View 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;
}

View 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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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 -->

View 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;
});

View 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;
});

View 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;
});

View file

@ -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) {

View file

@ -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

View file

@ -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 -->