Start on the new build view

This commit is contained in:
Joseph Schorr 2015-02-26 17:45:28 -05:00
parent 5cc1c90021
commit e227d7e526
30 changed files with 816 additions and 11 deletions

View file

@ -93,7 +93,11 @@ def build_status_view(build_obj, can_write=False):
'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key,
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
'repository': {
'namespace': build_obj.repository.namespace_user.username,
'name': build_obj.repository.name
}
}
if can_write:
@ -214,6 +218,18 @@ class RepositoryBuildList(RepositoryParamResource):
@path_param('build_uuid', 'The UUID of the build')
class RepositoryBuildResource(RepositoryParamResource):
""" Resource for dealing with repository builds. """
@require_repo_read
@nickname('getRepoBuild')
def get(self, namespace, repository, build_uuid):
""" Returns information about a build. """
try:
build = model.get_repository_build(build_uuid)
except model.InvalidRepositoryBuildException:
raise NotFound()
can_write = ModifyRepositoryPermission(namespace, repository).can()
return build_status_view(build, can_write)
@require_repo_admin
@nickname('cancelRepoBuild')
def delete(self, namespace, repository, build_uuid):

View file

@ -421,7 +421,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
pull_robot_name = model.get_pull_robot_name(trigger)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
pull_robot_name=pull_robot_name, trigger_metadata=metadata)
trigger=trigger, pull_robot_name=pull_robot_name,
trigger_metadata=metadata)
except TriggerStartException as tse:
raise InvalidRequest(tse.message)

View file

@ -224,7 +224,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
'docker_tags': tags,
'registry': host,
'build_subdir': subdir,
'trigger_metadata': trigger_metadata or {}
'trigger_metadata': trigger_metadata or {},
'is_manual': manual
}
with app.config['DB_TRANSACTION_FACTORY'](db):

View file

@ -310,6 +310,30 @@ class GithubBuildTrigger(BuildTrigger):
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
raise RepositoryReadException(message)
@staticmethod
def _build_commit_info(repo, commit_sha):
try:
commit = repo.get_commit(commit_sha)
except GithubException:
logger.exception('Could not load data for commit')
return
return {
'url': commit.html_url,
'message': commit.commit.message,
'author': {
'username': commit.author.login,
'avatar_url': commit.author.avatar_url,
'url': commit.author.html_url
},
'committer': {
'username': commit.committer.login,
'avatar_url': commit.committer.avatar_url,
'url': commit.committer.html_url
},
'date': commit.last_modified
}
@staticmethod
def _prepare_build(config, repo, commit_sha, build_name, ref):
# Prepare the download and upload URLs
@ -360,9 +384,14 @@ class GithubBuildTrigger(BuildTrigger):
metadata = {
'commit_sha': commit_sha,
'ref': ref,
'default_branch': repo.default_branch
'default_branch': repo.default_branch,
}
# add the commit info.
commit_info = GithubBuildTrigger._build_commit_info(repo, commit_sha)
if commit_info is not None:
metadata['commit_info'] = commit_info
return dockerfile_id, list(tags), build_name, joined_subdir, metadata
@staticmethod
@ -417,6 +446,7 @@ class GithubBuildTrigger(BuildTrigger):
branch_name = run_parameters.get('branch_name') or repo.default_branch
branch = repo.get_branch(branch_name)
branch_sha = branch.commit.sha
commit_info = branch.commit
short_sha = GithubBuildTrigger.get_display_name(branch_sha)
ref = 'refs/heads/%s' % (branch_name)

View file

@ -411,6 +411,11 @@ def populate_database():
'repository': repo,
'docker_tags': ['latest'],
'build_subdir': '',
'trigger_metadata': {
'commit_sha': '3482adc5822c498e8f7db2e361e8d57b3d77ddd9',
'ref': 'refs/heads/master',
'default_branch': 'master'
}
}
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple',

View file

@ -717,4 +717,24 @@
.cor-container {
padding-left: 15px;
padding-right: 15px;
}
}
.cor-title-link {
line-height: 30px;
margin-top: 22px;
margin-bottom: 10px;
font-size: 16px;
}
.cor-title-link a {
color: lightblue;
}
.cor-title-link a.back-link:before {
content: "\f060";
color: white;
display: inline-block;
margin-right: 10px;
vertical-align: middle;
font-family: FontAwesome;
}

View file

@ -0,0 +1,34 @@
.build-info-bar-element {
position: relative;
padding: 6px;
padding-left: 20px;
}
.build-info-bar-element .phase-icon {
position: absolute;
top: 4px;
left: 4px;
bottom: 0px;
height: auto;
border-radius: 0px;
width: 5px;
}
.build-info-bar-element .build-side-info {
float: right;
}
.build-info-bar-element .build-side-info .build-side-id {
font-size: 12px;
color: #ccc;
margin-bottom: 10px;
}
.build-info-bar-element .build-side-info .timing .fa {
display: inline-block;
margin-right: 4px;
}
.build-info-bar-element .build-side-info .timing {
text-align: right;
}

View file

@ -0,0 +1,128 @@
.build-logs-view {
position: relative;
color: white;
padding: 10px;
overflow: auto;
box-shadow: inset 10px 10px 5px -9px rgba(0,0,0,0.75);
background: linear-gradient(30deg, #263945, #061C2A) no-repeat left top fixed;
}
.build-logs-view .container-header {
padding: 2px;
}
.build-logs-view .container-logs {
margin: 4px;
padding-bottom: 4px;
}
.build-logs-view .command-title,
.build-logs-view .log-entry .message,
.build-logs-view .log-entry .message span {
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 13px;
}
.build-logs-view .container-header {
cursor: pointer;
position: relative;
}
.build-logs-view .container-header i.fa.chevron {
color: #666;
margin-right: 4px;
width: 14px;
text-align: center;
position: absolute;
top: 6px;
left: 0px;
}
.build-logs-view .log-container.command {
margin-left: 22px;
}
.build-logs-view .container-header.building {
margin-bottom: 10px;
}
.build-logs-view .container-header.pushing {
margin-top: 10px;
}
.build-logs-view .build-log-error-element .error-message-container {
position: relative;
display: inline-block;
margin: 10px;
padding: 10px;
background: rgba(255, 0, 0, 0.17);
border-radius: 10px;
margin-left: 22px;
}
.build-logs-view .build-log-error-element .error-message-container i.fa {
color: red;
position: absolute;
top: 13px;
left: 11px;
}
.build-logs-view .build-log-error-element .error-message {
display: inline-block;
margin-left: 25px;
}
.build-logs-view .container-header .label {
padding-top: 4px;
text-align: right;
margin-right: 4px;
width: 86px;
display: inline-block;
border-right: 4px solid #aaa;
background-color: #717171;
position: absolute;
top: 4px;
left: 24px;
}
.build-logs-view .dockerfile-command {
position: inherit;
}
.build-logs-view .dockerfile-command .command-title {
padding-left: 0px;
}
.build-logs-view .container-header .container-content {
display: block;
padding-left: 20px;
}
.build-logs-view .container-header .container-content.build-log-command {
padding-left: 120px;
}
.build-logs-view .log-entry {
position: relative;
}
.build-logs-view .log-entry .message {
display: inline-block;
margin-left: 46px;
}
.build-logs-view .log-entry .id {
color: #aaa;
padding-right: 6px;
margin-right: 6px;
text-align: right;
font-size: 12px;
width: 40px;
position: absolute;
top: 4px;
left: 4px;
}

View file

@ -0,0 +1,4 @@
.source-commit-link-element .fa {
margin-right: 4px;
display: inline-block;
}

View file

@ -0,0 +1,4 @@
.source-ref-link-element .fa {
margin-right: 4px;
display: inline-block;
}

View file

@ -0,0 +1,37 @@
.triggered-build-description-element .commit-message {
font-size: 16px;
display: block;
}
.triggered-build-description-element .commit-who img {
width: 16px;
height: 16px;
}
.triggered-build-description-element .fa-github {
display: inline-block;
margin-left: 6px;
margin-right: 4px;
}
.triggered-build-description-element .commit-information {
margin-top: 6px;
font-size: 14px;
color: #999;
}
.triggered-build-description-element .source-commit-link a {
color: #999;
}
.triggered-build-description-element .source-ref-link a {
color: #999;
}
.triggered-build-description-element .commit-information > span {
margin-right: 20px;
}
.triggered-build-description-element .commit-who:before {
content: "by ";
}

View file

@ -0,0 +1,46 @@
.build-view .build-header .repo-breadcrumb {
font-size: 18px;
margin-left: 12px;
}
.build-view .build-status-header {
padding: 4px;
}
.build-view .build-status-header .cor-loader-inline {
margin-right: 10px;
}
.build-view .build-logs-view {
margin-top: 10px;
}
.build-view .build-status-header {
font-size: 17px;
margin-top: 10px;
}
.build-view .build-status-header .fa {
margin-right: 6px;
}
.build-view .build-icon-message.error {
color: red;
}
.build-view .build-icon-message.internalerror {
color: #DFFF00;
}
.build-view .build-icon-message.complete {
color: #2fcc66;
}
.build-view .build-status-header .timing {
float: right;
}
.build-view .build-status-header .timing .fa {
margin-left: 10px;
margin-right: 4px;
}

View file

@ -866,7 +866,7 @@ i.toggle-icon:hover {
}
.phase-icon.complete {
background-color: rgba(66, 139, 202, 1);
background-color: #2fcc66;
}
.build-status {

View file

@ -0,0 +1,18 @@
<div class="build-info-bar-element" ng-class="build.phase">
<!-- Phase icon/color -->
<span class="phase-icon" ng-class="build.phase"></span>
<!-- Side information -->
<div class="build-side-info">
<!-- Build ID -->
<div class="build-side-id">{{ build.id }}</div>
<!-- Timing -->
<div class="timing" ng-if="showTime">
<i class="fa fa-clock-o"></i><span am-time-ago="build.started || 0"></span>
</div>
</div>
<div class="triggered-build-description" build="build" ng-if="build.trigger"></div>
<div ng-if="!build.trigger">Manually Started Build</div>
</div>

View file

@ -0,0 +1,30 @@
<div class="build-logs-view-element">
<span class="cor-loader" ng-if="!logEntries"></span>
<span ng-if="!logEntries.length">(No Logs)</span>
<div class="log-container" ng-class="container.type" ng-repeat="container in logEntries">
<div class="container-header" ng-class="container.type == 'phase' ? container.message : ''"
ng-switch on="container.type" ng-click="container.logs.toggle()">
<i class="fa chevron"
ng-class="container.logs.isVisible ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)"></i>
<div ng-switch-when="phase">
<span class="container-content build-log-phase" phase="container"></span>
</div>
<div ng-switch-when="error">
<span class="container-content build-log-error" error="container" entries="logEntries"></span>
</div>
<div ng-switch-when="command">
<span class="container-content build-log-command" command="container"></span>
</div>
</div>
<!-- Display the entries for the container -->
<div class="container-logs" ng-show="container.logs.isVisible">
<div class="log-entry" bindonce ng-repeat="entry in container.logs.visibleEntries">
<span class="id" bo-text="$index + container.index + 1"></span>
<span class="message" bo-html="processANSI(entry.message, container)"></span>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,4 @@
<div class="logs-view-element">
<div class="header">
<span class="header-text">
<span ng-show="!performer">Usage Logs</span>

View file

@ -1,9 +1,9 @@
<div class="resource-view-element">
<div class="quay-spinner" ng-show="resource.loading"></div>
<div class="resource-error" ng-show="!resource.loading && resource.hasError">
<div class="cor-loader" ng-show="getState(resources) == 'loading'"></div>
<div class="resource-error alert alert-info" ng-show="getState(resources) == 'error'">
{{ errorMessage }}
</div>
<div class="resource-content" ng-class="(!resource.loading && !resource.hasError) ? 'visible' : ''">
<div class="resource-content" ng-class="getState(resources) == 'ready' ? 'visible' : ''">
<span ng-transclude></span>
</div>
</div>

View file

@ -0,0 +1,4 @@
<span class="source-commit-link-elememt">
<i class="fa fa-dot-circle-o"></i>
<a ng-href="{{ getUrl(commitSha, urlTemplate) }}" target="_blank">{{ commitSha.substring(0, 8) }}</a>
</span>

View file

@ -0,0 +1,15 @@
<span class="source-ref-link-element">
<span ng-switch on="getKind(ref)">
<!-- Branch -->
<span ng-switch-when="heads">
<i class="fa fa-code-fork" data-title="Branch" bs-tooltip></i>
<a ng-href="{{ getUrl(ref, branchTemplate, 'branch') }}" target="_blank">{{ getTitle(ref) }}</a>
</span>
<!-- Tag -->
<span ng-switch-when="tags">
<i class="fa fa-tag" data-title="Tag" bs-tooltip></i>
<a ng-href="{{ getUrl(ref, tagTemplate, 'tag') }}" target="_blank">{{ getTitle(ref) }}</a>
</span>
</span>
</span>

View file

@ -0,0 +1,50 @@
<div class="triggered-build-description-element">
<span ng-switch on="build.trigger.service">
<!-- GitHub -->
<span ng-switch-when="github">
<!-- Full Commit Information -->
<span ng-if="build.job_config.trigger_metadata.commit_info">
<div class="commit-message">{{ build.job_config.trigger_metadata.commit_info.message }}</div>
<div class="commit-information">
<span class="commit-who-when">
<span am-time-ago="build.job_config.trigger_metadata.commit_info.date"></span>
<span class="commit-who">
<img ng-src="{{ build.job_config.trigger_metadata.commit_info.author.avatar_url }}">
<a ng-href="{{ build.job_config.trigger_metadata.commit_info.author.url }}"
target="_blank">
{{ build.job_config.trigger_metadata.commit_info.author.username }}
</a>
</span>
</span>
<span class="source-commit-link"
commit-sha="build.job_config.trigger_metadata.commit_sha"
url-template="getGitHubRepoURL(build) + '/commit/{sha}'"></span>
<span class="source-ref-link"
ref="build.job_config.trigger_metadata.ref"
branch-template="getGitHubRepoURL(build) + '/tree/{branch}'"
tag-template="getGitHubRepoURL(build) + '/releases/tag/{tag}'"></span>
</div>
</span>
<!-- Just commit SHA -->
<span ng-if="!build.job_config.trigger_metadata.commit_info">
Triggered by commit
<span class="source-commit-link"
commit-sha="build.job_config.trigger_metadata.commit_sha"
url-template="getGitHubRepoURL(build) + '/commit/{sha}'"></span>
</span>
<!-- No information -->
<span ng-if="!build.job_config.trigger_metadata">
Triggered by commit to
<i class="fa fa-github fa-lg" data-title="GitHub" bs-tooltip="tooltip.title"></i>
<a ng-href="{{ getGitHubRepoURL(build) }}" target="_new">
{{ build.trigger.config.build_source }}
</a>
</span>
</span>
<!-- Unknown -->
<span ng-switch-default></span>
</span>
</div>

View file

@ -96,6 +96,9 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
// Repo Builds
.route('/repository/:namespace/:name/build', 'repo-build')
// Repo Build View
.route('/repository/:namespace/:name/build/:buildid', 'build-view')
// Repo Build Package
.route('/repository/:namespace/:name/build/:buildid/buildpack', 'build-package')

View file

@ -0,0 +1,19 @@
/**
* An element which displays the status of a build in a nice compact bar.
*/
angular.module('quay').directive('buildInfoBar', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-info-bar.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'build': '=build',
'showTime': '=showTime'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,139 @@
/**
* An element which displays and auto-updates the logs from a build.
*/
angular.module('quay').directive('buildLogsView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-logs-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'build': '=build',
'buildUpdated': '&buildUpdated'
},
controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray,
AngularPollChannel, ApiService, Restangular) {
$scope.logEntries = null;
$scope.currentParentEntry = null;
$scope.logStartIndex = 0;
var pollChannel = null;
var currentBuild = null;
var processLogs = function(logs, startIndex, endIndex) {
if (!$scope.logEntries) { $scope.logEntries = []; }
// If the start index given is less than that requested, then we've received a larger
// pool of logs, and we need to only consider the new ones.
if (startIndex < $scope.logStartIndex) {
logs = logs.slice($scope.logStartIndex - startIndex);
}
for (var i = 0; i < logs.length; ++i) {
var entry = logs[i];
var type = entry['type'] || 'entry';
if (type == 'command' || type == 'phase' || type == 'error') {
entry['logs'] = AngularViewArray.create();
entry['index'] = $scope.logStartIndex + i;
$scope.logEntries.push(entry);
$scope.currentParentEntry = entry;
} else if ($scope.currentParentEntry) {
$scope.currentParentEntry['logs'].push(entry);
}
}
return endIndex;
};
var getBuildStatusAndLogs = function(build, callback) {
var params = {
'repository': build.repository.namespace + '/' + build.repository.name,
'build_uuid': build.id
};
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
if (resp.id != $scope.build.id) { callback(false); return; }
// Call the build updated handler.
$scope.buildUpdated({'build': resp});
// Save the current build.
currentBuild = resp;
// Load the updated logs for the build.
var options = {
'start': $scope.logStartIndex
};
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
// Process the logs we've received.
$scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']);
// If the build status is an error, open the last two log entries.
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
var openLogEntries = function(entry) {
if (entry.logs) {
entry.logs.setVisible(true);
}
};
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
}
// If the build phase is an error or a complete, then we mark the channel
// as closed.
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
}, function() {
callback(false);
});
}, function() {
callback(false);
});
};
var startWatching = function(build) {
// Create a new channel for polling the build status and logs.
var conductStatusAndLogRequest = function(callback) {
getBuildStatusAndLogs(build, callback);
};
pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
pollChannel.start();
};
var stopWatching = function() {
if (pollChannel) {
pollChannel.stop();
pollChannel = null;
}
};
$scope.$watch('build', function(build) {
if (build) {
startWatching(build);
} else {
stopWatching();
}
});
$scope.hasLogs = function(container) {
return container.logs.hasEntries;
};
$scope.processANSI = function(message, container) {
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
// Note: order is important here.
var setup = filter.getSetupHtml();
var stream = filter.addInputToStream(message);
var teardown = filter.getTeardownHtml();
return setup + stream + teardown;
};
}
};
return directiveDefinitionObject;
});

View file

@ -11,9 +11,29 @@ angular.module('quay').directive('resourceView', function () {
restrict: 'C',
scope: {
'resource': '=resource',
'resources': '=resources',
'errorMessage': '=errorMessage'
},
controller: function($scope, $element) {
$scope.getState = function() {
if (!$scope.resources && !$scope.resource) {
return 'loading';
}
var resources = $scope.resources || [$scope.resource];
for (var i = 0; i < resources.length; ++i) {
var current = resources[i];
if (current.loading) {
return 'loading';
}
if (current.error) {
return 'error';
}
}
return 'ready';
};
}
};
return directiveDefinitionObject;

View file

@ -0,0 +1,22 @@
/**
* An element which displays a link to a commit in Git or Mercurial or another source control.
*/
angular.module('quay').directive('sourceCommitLink', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/source-commit-link.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'commitSha': '=commitSha',
'urlTemplate': '=urlTemplate'
},
controller: function($scope, $element) {
$scope.getUrl = function(sha, template) {
return template.replace('{sha}', sha);
};
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,41 @@
/**
* An element which displays a link to a branch or tag in source control.
*/
angular.module('quay').directive('sourceRefLink', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/source-ref-link.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'ref': '=ref',
'branchTemplate': '=branchTemplate',
'tagTemplate': '=tagTemplate'
},
controller: function($scope, $element) {
$scope.getKind = function(ref) {
var parts = (ref || '').split('/');
if (parts.length < 3) {
return '';
}
return parts[1];
};
$scope.getTitle = function(ref) {
var parts = (ref || '').split('/');
if (parts.length < 3) {
return '';
}
return parts[2];
};
$scope.getUrl = function(ref, template, kind) {
return template.replace('{' + kind + '}', $scope.getTitle(ref));
};
}
};
return directiveDefinitionObject;
});

View file

@ -1,5 +1,5 @@
/**
* An element which displays information about a build trigger.
* DEPRECATED: An element which displays information about a build trigger.
*/
angular.module('quay').directive('triggerDescription', function () {
var directiveDefinitionObject = {

View file

@ -0,0 +1,21 @@
/**
* An element which displays information about a build that was triggered from an outside source.
*/
angular.module('quay').directive('triggeredBuildDescription', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/triggered-build-description.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'build': '=build'
},
controller: function($scope, $element, KeyService, TriggerService) {
$scope.getGitHubRepoURL = function(build) {
return KeyService['githubTriggerEndpoint'] + build.trigger.config.build_source + '/';
};
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,51 @@
(function() {
/**
* Build view page. Displays the view of a particular build for a repository.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('build-view', 'build-view.html', BuildViewCtrl, {
newLayout: true
});
}]);
function BuildViewCtrl($scope, ApiService, $routeParams, AngularPollChannel) {
$scope.namespace = $routeParams.namespace;
$scope.name = $routeParams.name;
$scope.build_uuid = $routeParams.buildid;
var loadBuild = function() {
var params = {
'repository': $scope.namespace + '/' + $scope.name,
'build_uuid': $scope.build_uuid
};
$scope.buildResource = ApiService.getRepoBuildAsResource(params).get(function(build) {
$scope.build = build;
$scope.originalBuild = build;
});
};
var loadRepository = function() {
var params = {
'repository': $scope.namespace + '/' + $scope.name
};
$scope.repoResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repo = repo;
}, ApiService.errorDisplay('Cannot load repository'));
};
// Page startup:
loadRepository();
loadBuild();
$scope.setUpdatedBuild = function(build) {
$scope.build = build;
};
$scope.isBuilding = function(build) {
if (!build) { return true; }
return build.phase != 'complete' && build.phase != 'error';
};
}
})();

View file

@ -0,0 +1,47 @@
<div class="build-view">
<div class="resource-view" resources="[buildResource, repoResource]"
error-message="'Build not found'">
<div class="page-content">
<div class="cor-title">
<span class="cor-title-link">
<a class="back-link" href="/repository/{{ namespace }}/{{ name }}?tab=builds">
<i class="fa fa-hdd-o" style="margin-right: 4px"></i>
{{ namespace }}/{{ name }}
</a>
</span>
<span class="cor-title-content">
<i class="fa fa-tasks fa-lg" style="margin-right: 10px"></i> {{ build.display_name }}
</span>
</div>
<div class="co-main-content-panel">
<!-- Build Information -->
<div class="build-info-bar" build="build" show-time="false"></div>
<!-- Current Status -->
<div class="build-status-header">
<span class="build-icon-message" 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 class="build-message" phase="build.phase"></span>
</span>
<div class="timing">
<i class="fa fa-clock-o"></i>
Build started
<span am-time-ago="build.started || 0"></span>
</div>
</div>
<!-- Build Logs -->
<div class="build-logs-view" build="originalBuild"
build-updated="setUpdatedBuild(build)"></div>
</div>
</div>
</div>
</div>