Merge branch 'newbuildview'
This commit is contained in:
commit
43ab838998
33 changed files with 1095 additions and 36 deletions
|
@ -94,7 +94,11 @@ def build_status_view(build_obj, can_write=False):
|
||||||
'is_writer': can_write,
|
'is_writer': can_write,
|
||||||
'trigger': trigger_view(build_obj.trigger),
|
'trigger': trigger_view(build_obj.trigger),
|
||||||
'resource_key': build_obj.resource_key,
|
'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:
|
if can_write:
|
||||||
|
@ -215,6 +219,18 @@ class RepositoryBuildList(RepositoryParamResource):
|
||||||
@path_param('build_uuid', 'The UUID of the build')
|
@path_param('build_uuid', 'The UUID of the build')
|
||||||
class RepositoryBuildResource(RepositoryParamResource):
|
class RepositoryBuildResource(RepositoryParamResource):
|
||||||
""" Resource for dealing with repository builds. """
|
""" 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
|
@require_repo_admin
|
||||||
@nickname('cancelRepoBuild')
|
@nickname('cancelRepoBuild')
|
||||||
def delete(self, namespace, repository, build_uuid):
|
def delete(self, namespace, repository, build_uuid):
|
||||||
|
|
|
@ -421,7 +421,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
pull_robot_name = model.get_pull_robot_name(trigger)
|
pull_robot_name = model.get_pull_robot_name(trigger)
|
||||||
|
|
||||||
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
|
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:
|
except TriggerStartException as tse:
|
||||||
raise InvalidRequest(tse.message)
|
raise InvalidRequest(tse.message)
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
'docker_tags': tags,
|
'docker_tags': tags,
|
||||||
'registry': host,
|
'registry': host,
|
||||||
'build_subdir': subdir,
|
'build_subdir': subdir,
|
||||||
'trigger_metadata': trigger_metadata or {}
|
'trigger_metadata': trigger_metadata or {},
|
||||||
|
'is_manual': manual
|
||||||
}
|
}
|
||||||
|
|
||||||
with app.config['DB_TRANSACTION_FACTORY'](db):
|
with app.config['DB_TRANSACTION_FACTORY'](db):
|
||||||
|
|
|
@ -310,6 +310,30 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
|
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
|
||||||
raise RepositoryReadException(message)
|
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
|
@staticmethod
|
||||||
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
||||||
# Prepare the download and upload URLs
|
# Prepare the download and upload URLs
|
||||||
|
@ -360,9 +384,14 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
metadata = {
|
metadata = {
|
||||||
'commit_sha': commit_sha,
|
'commit_sha': commit_sha,
|
||||||
'ref': ref,
|
'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
|
return dockerfile_id, list(tags), build_name, joined_subdir, metadata
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -417,6 +446,7 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
branch_name = run_parameters.get('branch_name') or repo.default_branch
|
branch_name = run_parameters.get('branch_name') or repo.default_branch
|
||||||
branch = repo.get_branch(branch_name)
|
branch = repo.get_branch(branch_name)
|
||||||
branch_sha = branch.commit.sha
|
branch_sha = branch.commit.sha
|
||||||
|
commit_info = branch.commit
|
||||||
short_sha = GithubBuildTrigger.get_display_name(branch_sha)
|
short_sha = GithubBuildTrigger.get_display_name(branch_sha)
|
||||||
ref = 'refs/heads/%s' % (branch_name)
|
ref = 'refs/heads/%s' % (branch_name)
|
||||||
|
|
||||||
|
|
|
@ -418,6 +418,11 @@ def populate_database():
|
||||||
'repository': repo,
|
'repository': repo,
|
||||||
'docker_tags': ['latest'],
|
'docker_tags': ['latest'],
|
||||||
'build_subdir': '',
|
'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',
|
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple',
|
||||||
|
|
|
@ -733,4 +733,24 @@
|
||||||
.cor-container {
|
.cor-container {
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
padding-right: 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;
|
||||||
|
}
|
||||||
|
|
34
static/css/directives/ui/build-info-bar.css
Normal file
34
static/css/directives/ui/build-info-bar.css
Normal 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;
|
||||||
|
}
|
179
static/css/directives/ui/build-logs-view.css
Normal file
179
static/css/directives/ui/build-logs-view.css
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
.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-color: #263945;
|
||||||
|
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view .no-logs {
|
||||||
|
color: #8C8C8C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view .log-entry:hover .timestamp {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view .log-entry .timestamp {
|
||||||
|
float: right;
|
||||||
|
visibility: hidden;
|
||||||
|
color: #aaa;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 10px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 40px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view .copy-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
z-index: 2;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view .copy-button:not(.zeroclipboard-is-hover) {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view .copy-button i.fa {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view-element.with-timestamps .log-entry .message {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 146px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-view-element.with-timestamps .log-entry .id {
|
||||||
|
width: 140px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
4
static/css/directives/ui/source-commit-link.css
Normal file
4
static/css/directives/ui/source-commit-link.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.source-commit-link-element .fa {
|
||||||
|
margin-right: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
4
static/css/directives/ui/source-ref-link.css
Normal file
4
static/css/directives/ui/source-ref-link.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.source-ref-link-element .fa {
|
||||||
|
margin-right: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
44
static/css/directives/ui/triggered-build-description.css
Normal file
44
static/css/directives/ui/triggered-build-description.css
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
.triggered-build-description-element .commit-message {
|
||||||
|
font-size: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggered-build-description-element .commit-message a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggered-build-description-element .commit-who img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 ";
|
||||||
|
}
|
51
static/css/pages/build-view.css
Normal file
51
static/css/pages/build-view.css
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
.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 .cor-options-menu {
|
||||||
|
float: right;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-view .build-status-header .timing {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-view .build-status-header .timing .fa {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
|
@ -912,7 +912,7 @@ i.toggle-icon:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-icon.complete {
|
.phase-icon.complete {
|
||||||
background-color: rgba(66, 139, 202, 1);
|
background-color: #2fcc66;
|
||||||
}
|
}
|
||||||
|
|
||||||
.build-status {
|
.build-status {
|
||||||
|
|
18
static/directives/build-info-bar.html
Normal file
18
static/directives/build-info-bar.html
Normal 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>
|
37
static/directives/build-logs-view.html
Normal file
37
static/directives/build-logs-view.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<div class="build-logs-view-element" ng-class="useTimestamps ? 'with-timestamps' : ''">
|
||||||
|
<button id="copyButton" class="btn btn-primary copy-button" data-clipboard-text="{{ buildLogsText }}">
|
||||||
|
<i class="fa fa-clipboard"></i>Copy Logs
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="cor-loader" ng-if="!logEntries"></span>
|
||||||
|
|
||||||
|
<span class="no-logs" ng-if="!logEntries.length">(Waiting for build to start)</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" ng-if="!useTimestamps"></span>
|
||||||
|
<span class="id" bo-text="formatDatetime(entry.datetime)" ng-if="useTimestamps"></span>
|
||||||
|
<span class="message" bo-html="processANSI(entry.message, container)"></span>
|
||||||
|
<span class="timestamp" bo-text="formatDatetime(entry.datetime)" ng-if="!useTimestamps"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,5 +1,4 @@
|
||||||
<div class="logs-view-element">
|
<div class="logs-view-element">
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-text">
|
<span class="header-text">
|
||||||
<span ng-show="!performer">Usage Logs</span>
|
<span ng-show="!performer">Usage Logs</span>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<div class="resource-view-element">
|
<div class="resource-view-element">
|
||||||
<div class="quay-spinner" ng-show="resource.loading"></div>
|
<div class="cor-loader" ng-show="getState(resources) == 'loading'"></div>
|
||||||
<div class="resource-error" ng-show="!resource.loading && resource.hasError">
|
<div class="resource-error alert alert-info" ng-show="getState(resources) == 'error'">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</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>
|
<span ng-transclude></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
4
static/directives/source-commit-link.html
Normal file
4
static/directives/source-commit-link.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<span class="source-commit-link-elememt">
|
||||||
|
<i class="fa fa-dot-circle-o" data-title="Commit" data-container="body" bs-tooltip></i>
|
||||||
|
<a ng-href="{{ getUrl(commitSha, urlTemplate) }}" target="_blank">{{ commitSha.substring(0, 8) }}</a>
|
||||||
|
</span>
|
15
static/directives/source-ref-link.html
Normal file
15
static/directives/source-ref-link.html
Normal 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-container="body" 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-container="body" data-title="Tag" bs-tooltip></i>
|
||||||
|
<a ng-href="{{ getUrl(ref, tagTemplate, 'tag') }}" target="_blank">{{ getTitle(ref) }}</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
56
static/directives/triggered-build-description.html
Normal file
56
static/directives/triggered-build-description.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<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">
|
||||||
|
<a ng-href="{{ getGitHubRepoURL(build) }}/commit/{{ build.job_config.trigger_metadata.commit_sha }}"
|
||||||
|
target="_blank">
|
||||||
|
{{ build.job_config.trigger_metadata.commit_info.message }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="commit-information">
|
||||||
|
<span class="commit-who-when">
|
||||||
|
Authored
|
||||||
|
<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 && !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>
|
|
@ -96,6 +96,9 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
|
||||||
// Repo Builds
|
// Repo Builds
|
||||||
.route('/repository/:namespace/:name/build', 'repo-build')
|
.route('/repository/:namespace/:name/build', 'repo-build')
|
||||||
|
|
||||||
|
// Repo Build View
|
||||||
|
.route('/repository/:namespace/:name/build/:buildid', 'build-view')
|
||||||
|
|
||||||
// Repo Build Package
|
// Repo Build Package
|
||||||
.route('/repository/:namespace/:name/build/:buildid/buildpack', 'build-package')
|
.route('/repository/:namespace/:name/build/:buildid/buildpack', 'build-package')
|
||||||
|
|
||||||
|
@ -203,8 +206,8 @@ if (window.__config && window.__config.SENTRY_PUBLIC_DSN) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the application.
|
// Run the application.
|
||||||
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService',
|
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', 'MetaService',
|
||||||
function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService) {
|
function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService) {
|
||||||
|
|
||||||
var title = window.__config['REGISTRY_TITLE'] || 'Quay.io';
|
var title = window.__config['REGISTRY_TITLE'] || 'Quay.io';
|
||||||
|
|
||||||
|
@ -296,34 +299,39 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||||
$rootScope.pageClass = '';
|
|
||||||
$rootScope.current = current.$$route;
|
$rootScope.current = current.$$route;
|
||||||
|
$rootScope.currentPage = current;
|
||||||
|
|
||||||
|
$rootScope.pageClass = '';
|
||||||
|
|
||||||
if (!current.$$route) { return; }
|
if (!current.$$route) { return; }
|
||||||
|
|
||||||
if (current.$$route.title) {
|
$rootScope.pageClass = current.$$route.pageClass || '';
|
||||||
$rootScope.title = current.$$route.title;
|
|
||||||
} else {
|
|
||||||
$rootScope.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.$$route.pageClass) {
|
|
||||||
$rootScope.pageClass = current.$$route.pageClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.newLayout = !!current.$$route.newLayout;
|
$rootScope.newLayout = !!current.$$route.newLayout;
|
||||||
|
|
||||||
if (current.$$route.description) {
|
|
||||||
$rootScope.description = current.$$route.description;
|
|
||||||
} else {
|
|
||||||
$rootScope.description = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
||||||
|
|
||||||
|
MetaService.getInitialTitle(current, function(title) {
|
||||||
|
$rootScope.title = title;
|
||||||
|
});
|
||||||
|
|
||||||
|
MetaService.getInitialDescription(current, function(description) {
|
||||||
|
$rootScope.description = description
|
||||||
|
});
|
||||||
|
|
||||||
$anchorScroll();
|
$anchorScroll();
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$on('$viewContentLoaded', function(event, current) {
|
$rootScope.$on('$viewContentLoaded', function(event) {
|
||||||
|
var current = $rootScope.currentPage;
|
||||||
|
|
||||||
|
MetaService.getTitle(current, function(title) {
|
||||||
|
$rootScope.title = title;
|
||||||
|
});
|
||||||
|
|
||||||
|
MetaService.getDescription(current, function(description) {
|
||||||
|
$rootScope.description = description;
|
||||||
|
});
|
||||||
|
|
||||||
var activeTab = $location.search()['tab'];
|
var activeTab = $location.search()['tab'];
|
||||||
|
|
||||||
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
||||||
|
|
19
static/js/directives/ui/build-info-bar.js
Normal file
19
static/js/directives/ui/build-info-bar.js
Normal 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;
|
||||||
|
});
|
168
static/js/directives/ui/build-logs-view.js
Normal file
168
static/js/directives/ui/build-logs-view.js
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
'useTimestamps': '=useTimestamps',
|
||||||
|
'buildUpdated': '&buildUpdated'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray,
|
||||||
|
AngularPollChannel, ApiService, Restangular) {
|
||||||
|
|
||||||
|
var result = $element.find('#copyButton').clipboardCopy();
|
||||||
|
if (!result) {
|
||||||
|
$element.find('#copyButton').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.logEntries = null;
|
||||||
|
$scope.currentParentEntry = null;
|
||||||
|
$scope.logStartIndex = 0;
|
||||||
|
$scope.buildLogsText = '';
|
||||||
|
|
||||||
|
var pollChannel = null;
|
||||||
|
var currentBuild = null;
|
||||||
|
|
||||||
|
var appendToTextLog = function(type, message) {
|
||||||
|
if (type == 'phase') {
|
||||||
|
text = 'Starting phase: ' + message + '\n';
|
||||||
|
} else {
|
||||||
|
text = message + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.buildLogsText += text.replace(new RegExp("\\033\\[[^m]+m"), '');
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendToTextLog(type, entry['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('useTimestamps', function() {
|
||||||
|
if (!$scope.logEntries) { return; }
|
||||||
|
$scope.logEntries = $scope.logEntries.slice();
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('build', function(build) {
|
||||||
|
if (build) {
|
||||||
|
startWatching(build);
|
||||||
|
} else {
|
||||||
|
stopWatching();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.hasLogs = function(container) {
|
||||||
|
return container.logs.hasEntries;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formatDatetime = function(datetimeString) {
|
||||||
|
var dt = new Date(datetimeString);
|
||||||
|
return dt.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
});
|
|
@ -23,6 +23,9 @@ $.fn.clipboardCopy = function() {
|
||||||
ZeroClipboard.on('aftercopy', function(e) {
|
ZeroClipboard.on('aftercopy', function(e) {
|
||||||
var container = e.target.parentNode.parentNode.parentNode;
|
var container = e.target.parentNode.parentNode.parentNode;
|
||||||
var message = $(container).find('.clipboard-copied-message')[0];
|
var message = $(container).find('.clipboard-copied-message')[0];
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Resets the animation.
|
// Resets the animation.
|
||||||
var elem = message;
|
var elem = message;
|
||||||
|
|
|
@ -11,9 +11,29 @@ angular.module('quay').directive('resourceView', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'resource': '=resource',
|
'resource': '=resource',
|
||||||
|
'resources': '=resources',
|
||||||
'errorMessage': '=errorMessage'
|
'errorMessage': '=errorMessage'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
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;
|
return directiveDefinitionObject;
|
||||||
|
|
22
static/js/directives/ui/source-commit-link.js
Normal file
22
static/js/directives/ui/source-commit-link.js
Normal 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;
|
||||||
|
});
|
41
static/js/directives/ui/source-ref-link.js
Normal file
41
static/js/directives/ui/source-ref-link.js
Normal 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;
|
||||||
|
});
|
|
@ -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 () {
|
angular.module('quay').directive('triggerDescription', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
|
|
21
static/js/directives/ui/triggered-build-description.js
Normal file
21
static/js/directives/ui/triggered-build-description.js
Normal 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;
|
||||||
|
});
|
75
static/js/pages/build-view.js
Normal file
75
static/js/pages/build-view.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
(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,
|
||||||
|
title: 'Build {{ build.display_name }}',
|
||||||
|
description: 'Logs and status for build {{ build.display_name }}'
|
||||||
|
});
|
||||||
|
}]);
|
||||||
|
|
||||||
|
function BuildViewCtrl($scope, ApiService, $routeParams, AngularPollChannel, CookieService) {
|
||||||
|
$scope.namespace = $routeParams.namespace;
|
||||||
|
$scope.name = $routeParams.name;
|
||||||
|
$scope.build_uuid = $routeParams.buildid;
|
||||||
|
|
||||||
|
$scope.showLogTimestamps = CookieService.get('quay.showBuildLogTimestamps') == 'true';
|
||||||
|
|
||||||
|
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.askCancelBuild = function(build) {
|
||||||
|
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
|
||||||
|
if (r) {
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.namespace + '/' + $scope.name,
|
||||||
|
'build_uuid': build.id
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.cancelRepoBuild(null, params).then(function() {
|
||||||
|
document.location = '/repository/' + $scope.namespace + '/' + $scope.name;
|
||||||
|
}, ApiService.errorDisplay('Cannot cancel build'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.toggleTimestamps = function() {
|
||||||
|
$scope.showLogTimestamps = !$scope.showLogTimestamps;
|
||||||
|
CookieService.putPermanent('quay.showBuildLogTimestamps', $scope.showLogTimestamps);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setUpdatedBuild = function(build) {
|
||||||
|
$scope.build = build;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.isBuilding = function(build) {
|
||||||
|
if (!build) { return true; }
|
||||||
|
return build.phase != 'complete' && build.phase != 'error';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
76
static/js/services/meta-service.js
Normal file
76
static/js/services/meta-service.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Service which helps set the contents of the <meta> tags (and the <title> of a page).
|
||||||
|
*/
|
||||||
|
angular.module('quay').factory('MetaService', ['$interpolate', 'Config', '$rootScope', '$interval',
|
||||||
|
function($interpolate, Config, $rootScope, $interval) {
|
||||||
|
var metaService = {};
|
||||||
|
var intervals = [];
|
||||||
|
|
||||||
|
var interpolate = function(page, expr, callback) {
|
||||||
|
var previous = '';
|
||||||
|
|
||||||
|
var currentInterval = $interval(function() {
|
||||||
|
var inter = $interpolate(expr, true, null, true);
|
||||||
|
var result = inter(page.scope)
|
||||||
|
|
||||||
|
if (result != previous) {
|
||||||
|
$interval.cancel(currentInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = result;
|
||||||
|
callback(result);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
intervals.push(currentInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
var initial = function(value, default_value, callback) {
|
||||||
|
for (var i = 0; i < intervals.length; ++i) {
|
||||||
|
$interval.cancel(intervals[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
intervals = [];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
callback(default_value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.indexOf('{{') < 0) {
|
||||||
|
callback(default_value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback('Loading...');
|
||||||
|
};
|
||||||
|
|
||||||
|
metaService.getInitialTitle = function(page, callback) {
|
||||||
|
var route = page.$$route;
|
||||||
|
initial(route && route.title, Config.REGISTRY_TITLE_SHORT, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
metaService.getInitialDescription = function(page, callback) {
|
||||||
|
var route = page.$$route;
|
||||||
|
initial(route && route.description, Config.REGISTRY_TITLE_SHORT, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
metaService.getTitle = function(page, callback) {
|
||||||
|
var route = page.$$route;
|
||||||
|
if (!route || !route.title || route.title.indexOf('{{') < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate(page, route.title, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
metaService.getDescription = function(page, callback) {
|
||||||
|
var route = page.$$route;
|
||||||
|
if (!route || !route.description || route.description.indexOf('{{') < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate(page, route.description, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
return metaService;
|
||||||
|
}]);
|
64
static/partials/build-view.html
Normal file
64
static/partials/build-view.html
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<span class="cor-options-menu">
|
||||||
|
<span class="cor-option" option-click="toggleTimestamps()">
|
||||||
|
<span ng-if="showLogTimestamps">
|
||||||
|
<i class="fa fa-clock-o"></i> Hide Timestamps
|
||||||
|
</span>
|
||||||
|
<span ng-if="!showLogTimestamps">
|
||||||
|
<i class="fa fa-clock-o"></i> Show Timestamps
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="cor-option" option-click="askCancelBuild(build)"
|
||||||
|
ng-if="build.phase == 'waiting'">
|
||||||
|
<i class="fa fa-times"></i> Cancel Build
|
||||||
|
</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"
|
||||||
|
use-timestamps="showLogTimestamps"
|
||||||
|
build-updated="setUpdatedBuild(build)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
|
@ -108,7 +109,13 @@ class TestBuildLogs(RedisBuildLogs):
|
||||||
return script
|
return script
|
||||||
|
|
||||||
def _generate_phase(self, start_weight, phase_name):
|
def _generate_phase(self, start_weight, phase_name):
|
||||||
return (start_weight, {'message': phase_name, 'type': self.PHASE},
|
message = {
|
||||||
|
'message': phase_name,
|
||||||
|
'type': self.PHASE,
|
||||||
|
'datetime': str(datetime.datetime.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
return (start_weight, message,
|
||||||
(phase_name, deepcopy(self.STATUS_TEMPLATE)))
|
(phase_name, deepcopy(self.STATUS_TEMPLATE)))
|
||||||
|
|
||||||
def _generate_command(self, command_num, total_commands, command_weight):
|
def _generate_command(self, command_num, total_commands, command_weight):
|
||||||
|
@ -123,6 +130,7 @@ class TestBuildLogs(RedisBuildLogs):
|
||||||
msg = {
|
msg = {
|
||||||
'message': 'Step %s: %s %s' % (command_num, command, sentence),
|
'message': 'Step %s: %s %s' % (command_num, command, sentence),
|
||||||
'type': self.COMMAND,
|
'type': self.COMMAND,
|
||||||
|
'datetime': str(datetime.datetime.now())
|
||||||
}
|
}
|
||||||
status = deepcopy(self.STATUS_TEMPLATE)
|
status = deepcopy(self.STATUS_TEMPLATE)
|
||||||
status['total_commands'] = total_commands
|
status['total_commands'] = total_commands
|
||||||
|
@ -133,10 +141,26 @@ class TestBuildLogs(RedisBuildLogs):
|
||||||
def _generate_logs(count):
|
def _generate_logs(count):
|
||||||
others = []
|
others = []
|
||||||
if random.randint(0, 10) <= 8:
|
if random.randint(0, 10) <= 8:
|
||||||
count = count - 2
|
premessage = {
|
||||||
others = [(1, {'message': '\x1b[91m' + get_sentence()}, None), (1, {'message': '\x1b[0m'}, None)]
|
'message': '\x1b[91m' + get_sentence(),
|
||||||
|
'datetime': str(datetime.datetime.now())
|
||||||
|
}
|
||||||
|
|
||||||
return others + [(1, {'message': get_sentence()}, None) for _ in range(count)]
|
postmessage = {
|
||||||
|
'message': '\x1b[0m',
|
||||||
|
'datetime': str(datetime.datetime.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
count = count - 2
|
||||||
|
others = [(1, premessage, None), (1, postmessage, None)]
|
||||||
|
|
||||||
|
def get_message():
|
||||||
|
return {
|
||||||
|
'message': get_sentence(),
|
||||||
|
'datetime': str(datetime.datetime.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
return others + [(1, get_message(), None) for _ in range(count)]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _compute_total_completion(statuses, total_images):
|
def _compute_total_completion(statuses, total_images):
|
||||||
|
|
Reference in a new issue