bitbake: toaster: move most recent builds templating to client
The most recent builds area of the all builds and project builds table needs to update as a build progresses. It also needs additional functionality to show other states (e.g. recipe parsing, queued) which again needs to update on the client side. Rather than add to the existing mix of server-side templating with client-side DOM updating, translate all of the server-side templates to client-side ones (jsrender), and add logic which updates the most recent builds area as the state of a build changes. Add a JSON API for mostrecentbuilds, which returns the state of all "recent" builds. Fetch this via Ajax from the build dashboard (rather than fetching the ad hoc API as in the previous version). Then, as new states for builds are fetched via Ajax, determine whether the build state has changed completely, or whether the progress has just updated. If the state completely changed, re-render the template on the client side for that build. If only the progress changed, just update the progress bar. (NB this fixes the task progress bar so it works for the project builds and all builds pages.) In cases where the builds table needs to update as the result of a build finishing, reload the whole page. This work highlighted a variety of other issues, such as build requests not being able to change state as necessary. This was one part of the cause of the "cancelling build..." state being fragile and disappearing entirely when the page refreshed. The cancelling state now persists between page reloads, as the logic for determining whether a build is cancelling is now on the Build object itself. Note that jsrender is redistributed as part of Toaster, so a note was added to LICENSE to that effect. [YOCTO #9631] (Bitbake rev: c868ea036aa34b387a72ec5116a66b2cd863995b) Signed-off-by: Elliot Smith <elliot.smith@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
parent
c471740f5b
commit
952ffb3e1f
|
@ -10,4 +10,6 @@ Foundation and individual contributors.
|
|||
|
||||
* Twitter typeahead.js redistributed under the MIT license. Note that the JS source has one small modification, so the full unminified file is currently included to make it obvious where this is.
|
||||
|
||||
* jsrender is redistributed under the MIT license.
|
||||
|
||||
* QUnit is redistributed under the MIT license.
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0004_auto_20160523_1446'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='buildrequest',
|
||||
name='state',
|
||||
field=models.IntegerField(choices=[(0, 'created'), (1, 'queued'), (2, 'in progress'), (3, 'failed'), (4, 'deleted'), (5, 'cancelling'), (6, 'completed'), (7, 'archive')], default=0),
|
||||
),
|
||||
]
|
|
@ -63,20 +63,20 @@ class BuildRequest(models.Model):
|
|||
REQ_CREATED = 0
|
||||
REQ_QUEUED = 1
|
||||
REQ_INPROGRESS = 2
|
||||
REQ_COMPLETED = 3
|
||||
REQ_FAILED = 4
|
||||
REQ_DELETED = 5
|
||||
REQ_CANCELLING = 6
|
||||
REQ_FAILED = 3
|
||||
REQ_DELETED = 4
|
||||
REQ_CANCELLING = 5
|
||||
REQ_COMPLETED = 6
|
||||
REQ_ARCHIVE = 7
|
||||
|
||||
REQUEST_STATE = (
|
||||
(REQ_CREATED, "created"),
|
||||
(REQ_QUEUED, "queued"),
|
||||
(REQ_INPROGRESS, "in progress"),
|
||||
(REQ_COMPLETED, "completed"),
|
||||
(REQ_FAILED, "failed"),
|
||||
(REQ_DELETED, "deleted"),
|
||||
(REQ_CANCELLING, "cancelling"),
|
||||
(REQ_COMPLETED, "completed"),
|
||||
(REQ_ARCHIVE, "archive"),
|
||||
)
|
||||
|
||||
|
@ -91,7 +91,7 @@ class BuildRequest(models.Model):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BuildRequest, self).__init__(*args, **kwargs)
|
||||
# Save the old state incase it's about to be modified
|
||||
# Save the old state in case it's about to be modified
|
||||
self.old_state = self.state
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
|
@ -592,22 +592,42 @@ class Build(models.Model):
|
|||
|
||||
return target_labels
|
||||
|
||||
def get_current_status(self):
|
||||
"""
|
||||
get the status string from the build request if the build
|
||||
has one, or the text for the build outcome if it doesn't
|
||||
"""
|
||||
|
||||
from bldcontrol.models import BuildRequest
|
||||
|
||||
build_request = None
|
||||
def get_buildrequest(self):
|
||||
buildrequest = None
|
||||
if hasattr(self, 'buildrequest'):
|
||||
build_request = self.buildrequest
|
||||
buildrequest = self.buildrequest
|
||||
return buildrequest
|
||||
|
||||
if (build_request
|
||||
and build_request.state != BuildRequest.REQ_INPROGRESS
|
||||
and self.outcome == Build.IN_PROGRESS):
|
||||
return self.buildrequest.get_state_display()
|
||||
def is_queued(self):
|
||||
from bldcontrol.models import BuildRequest
|
||||
buildrequest = self.get_buildrequest()
|
||||
if buildrequest:
|
||||
return buildrequest.state == BuildRequest.REQ_QUEUED
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_cancelling(self):
|
||||
from bldcontrol.models import BuildRequest
|
||||
buildrequest = self.get_buildrequest()
|
||||
if buildrequest:
|
||||
return self.outcome == Build.IN_PROGRESS and \
|
||||
buildrequest.state == BuildRequest.REQ_CANCELLING
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_state(self):
|
||||
"""
|
||||
Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
|
||||
'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
|
||||
dependent on the BuildRequest state).
|
||||
|
||||
This works around the fact that we have BuildRequest states as well
|
||||
as Build states, but really we just want to know the state of the build.
|
||||
"""
|
||||
if self.is_cancelling():
|
||||
return 'Cancelling';
|
||||
elif self.is_queued():
|
||||
return 'Queued'
|
||||
else:
|
||||
return self.get_outcome_text()
|
||||
|
||||
|
|
|
@ -97,13 +97,13 @@ class TestAllBuildsPage(SeleniumTestCase):
|
|||
self.get(url)
|
||||
|
||||
# shouldn't see a rebuild button for command-line builds
|
||||
selector = 'div[data-latest-build-result="%s"] a.run-again-btn' % default_build.id
|
||||
selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
|
||||
run_again_button = self.find_all(selector)
|
||||
self.assertEqual(len(run_again_button), 0,
|
||||
'should not see a rebuild button for cli builds')
|
||||
|
||||
# should see a rebuild button for non-command-line builds
|
||||
selector = 'div[data-latest-build-result="%s"] a.run-again-btn' % build1.id
|
||||
selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id
|
||||
run_again_button = self.find_all(selector)
|
||||
self.assertEqual(len(run_again_button), 1,
|
||||
'should see a rebuild button for non-cli builds')
|
||||
|
|
|
@ -27,7 +27,10 @@ from bldcontrol import bbcontroller
|
|||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.generic import View
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from django.core import serializers
|
||||
from django.utils import timezone
|
||||
from django.template.defaultfilters import date
|
||||
from toastergui.templatetags.projecttags import json, sectohms, get_tasks
|
||||
|
||||
def error_response(error):
|
||||
return JsonResponse({"error": error})
|
||||
|
@ -208,3 +211,103 @@ class XhrLayer(View):
|
|||
"error": "ok",
|
||||
"redirect": reverse('project', args=(kwargs['pid'],))
|
||||
})
|
||||
|
||||
class MostRecentBuildsView(View):
|
||||
def _was_yesterday_or_earlier(self, completed_on):
|
||||
now = timezone.now()
|
||||
delta = now - completed_on
|
||||
|
||||
if delta.days >= 1:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns a list of builds in JSON format.
|
||||
"""
|
||||
mrb_type = 'all'
|
||||
project = None
|
||||
|
||||
project_id = request.GET.get('project_id', None)
|
||||
if project_id:
|
||||
try:
|
||||
mrb_type = 'project'
|
||||
project = Project.objects.get(pk=project_id)
|
||||
except:
|
||||
# if project lookup fails, assume no project
|
||||
pass
|
||||
|
||||
recent_build_objs = Build.get_recent(project)
|
||||
recent_builds = []
|
||||
|
||||
# for timezone conversion
|
||||
tz = timezone.get_current_timezone()
|
||||
|
||||
for build_obj in recent_build_objs:
|
||||
dashboard_url = reverse('builddashboard', args=(build_obj.pk,))
|
||||
buildtime_url = reverse('buildtime', args=(build_obj.pk,))
|
||||
rebuild_url = \
|
||||
reverse('xhr_buildrequest', args=(build_obj.project.pk,))
|
||||
cancel_url = \
|
||||
reverse('xhr_buildrequest', args=(build_obj.project.pk,))
|
||||
|
||||
build = {}
|
||||
build['id'] = build_obj.pk
|
||||
build['dashboard_url'] = dashboard_url
|
||||
|
||||
tasks_complete_percentage = 0
|
||||
if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
|
||||
tasks_complete_percentage = 100
|
||||
elif build_obj.outcome == Build.IN_PROGRESS:
|
||||
tasks_complete_percentage = build_obj.completeper()
|
||||
build['tasks_complete_percentage'] = tasks_complete_percentage
|
||||
|
||||
build['state'] = build_obj.get_state()
|
||||
|
||||
build['errors'] = build_obj.errors.count()
|
||||
build['dashboard_errors_url'] = dashboard_url + '#errors'
|
||||
|
||||
build['warnings'] = build_obj.warnings.count()
|
||||
build['dashboard_warnings_url'] = dashboard_url + '#warnings'
|
||||
|
||||
build['buildtime'] = sectohms(build_obj.timespent_seconds)
|
||||
build['buildtime_url'] = buildtime_url
|
||||
|
||||
build['rebuild_url'] = rebuild_url
|
||||
build['cancel_url'] = cancel_url
|
||||
|
||||
build['is_default_project_build'] = build_obj.project.is_default
|
||||
|
||||
build['build_targets_json'] = \
|
||||
json(get_tasks(build_obj.target_set.all()))
|
||||
|
||||
# convert completed_on time to user's timezone
|
||||
completed_on = timezone.localtime(build_obj.completed_on)
|
||||
|
||||
completed_on_template = '%H:%M'
|
||||
if self._was_yesterday_or_earlier(completed_on):
|
||||
completed_on_template = '%d/%m/%Y ' + completed_on_template
|
||||
build['completed_on'] = completed_on.strftime(completed_on_template)
|
||||
|
||||
targets = []
|
||||
target_objs = build_obj.get_sorted_target_list()
|
||||
for target_obj in target_objs:
|
||||
if target_obj.task:
|
||||
targets.append(target_obj.target + ':' + target_obj.task)
|
||||
else:
|
||||
targets.append(target_obj.target)
|
||||
build['targets'] = ' '.join(targets)
|
||||
|
||||
# abbreviated form of the full target list
|
||||
abbreviated_targets = ''
|
||||
num_targets = len(targets)
|
||||
if num_targets > 0:
|
||||
abbreviated_targets = targets[0]
|
||||
if num_targets > 1:
|
||||
abbreviated_targets += (' +%s' % (num_targets - 1))
|
||||
build['targets_abbreviated'] = abbreviated_targets
|
||||
|
||||
recent_builds.append(build)
|
||||
|
||||
return JsonResponse(recent_builds, safe=False)
|
||||
|
|
|
@ -45,6 +45,7 @@ img.logo { height: 30px; vertical-align: bottom; }
|
|||
.alert-link.build-warnings,
|
||||
.glyphicon-warning-sign.build-warnings { color: #8a6d3b; }
|
||||
.build-result .project-name { margin-top: -10px; margin-bottom: 5px; }
|
||||
.rebuild-btn, .cancel-build-btn { cursor: pointer; }
|
||||
|
||||
/* Styles for the help information */
|
||||
.get-help { color: #CCCCCC; }
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -148,6 +148,21 @@ var libtoaster = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
function _getMostRecentBuilds(url, onsuccess, onfail) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
data : {format: 'json'},
|
||||
headers: {'X-CSRFToken': $.cookie('csrftoken')},
|
||||
success: function (data) {
|
||||
onsuccess ? onsuccess(data) : console.log(data);
|
||||
},
|
||||
error: function (data) {
|
||||
onfail ? onfail(data) : console.error(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Get a project's configuration info */
|
||||
function _getProjectInfo(url, onsuccess, onfail){
|
||||
$.ajax({
|
||||
|
@ -426,6 +441,7 @@ var libtoaster = (function () {
|
|||
reload_params : reload_params,
|
||||
startABuild : _startABuild,
|
||||
cancelABuild : _cancelABuild,
|
||||
getMostRecentBuilds: _getMostRecentBuilds,
|
||||
makeTypeahead : _makeTypeahead,
|
||||
getProjectInfo: _getProjectInfo,
|
||||
getLayerDepsForProject : _getLayerDepsForProject,
|
||||
|
|
|
@ -1,33 +1,19 @@
|
|||
|
||||
function mrbSectionInit(ctx){
|
||||
|
||||
var projectBuilds;
|
||||
|
||||
if (ctx.mrbType === 'project')
|
||||
projectBuilds = true;
|
||||
|
||||
$(".cancel-build-btn").click(function(e){
|
||||
$('#latest-builds').on('click', '.cancel-build-btn', function(e){
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var url = $(this).data('request-url');
|
||||
var buildReqIds = $(this).data('buildrequest-id');
|
||||
var banner = $(this).parents(".alert");
|
||||
|
||||
banner.find(".progress-info").fadeOut().promise().done(function(){
|
||||
$("#cancelling-msg-" + buildReqIds).show();
|
||||
console.log("cancel build");
|
||||
libtoaster.cancelABuild(url, buildReqIds, function(){
|
||||
if (projectBuilds == false){
|
||||
/* the all builds page is not 'self updating' like thei
|
||||
* project Builds
|
||||
*/
|
||||
window.location.reload();
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
libtoaster.cancelABuild(url, buildReqIds, function () {
|
||||
window.location.reload();
|
||||
}, null);
|
||||
});
|
||||
|
||||
$(".run-again-btn").click(function(e){
|
||||
$('#latest-builds').on('click', '.rebuild-btn', function(e){
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var url = $(this).data('request-url');
|
||||
|
@ -38,58 +24,110 @@ function mrbSectionInit(ctx){
|
|||
}, null);
|
||||
});
|
||||
|
||||
// cached version of buildData, so we can determine whether a build has
|
||||
// changed since it was last fetched, and update the DOM appropriately
|
||||
var buildData = {};
|
||||
|
||||
var progressTimer;
|
||||
// returns the cached version of this build, or {} is there isn't a cached one
|
||||
function getCached(build) {
|
||||
return buildData[build.id] || {};
|
||||
}
|
||||
|
||||
if (projectBuilds === true){
|
||||
progressTimer = window.setInterval(function() {
|
||||
libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl,
|
||||
function(prjInfo){
|
||||
/* These two are needed because a build can be 100% and still
|
||||
* in progress due to the fact that the % done is updated at the
|
||||
* start of a task so it can be doing the last task at 100%
|
||||
*/
|
||||
var inProgress = 0;
|
||||
var allPercentDone = 0;
|
||||
if (prjInfo.builds.length === 0)
|
||||
return
|
||||
// returns true if a build's state changed to "Succeeded" or "Failed"
|
||||
// from some other value
|
||||
function buildFinished(build) {
|
||||
var cached = getCached(build);
|
||||
return cached.state &&
|
||||
cached.state !== build.state &&
|
||||
(build.state == 'Succeeded' || build.state == 'Failed' ||
|
||||
build.state == 'Cancelled');
|
||||
}
|
||||
|
||||
for (var i in prjInfo.builds){
|
||||
var build = prjInfo.builds[i];
|
||||
// returns true if the state changed
|
||||
function stateChanged(build) {
|
||||
var cached = getCached(build);
|
||||
return (cached.state !== build.state);
|
||||
}
|
||||
|
||||
if (build.outcomeText === "In Progress" ||
|
||||
$(".progress .bar").length > 0){
|
||||
/* Update the build progress */
|
||||
var percentDone;
|
||||
// returns true if the complete_percentage changed
|
||||
function progressChanged(build) {
|
||||
var cached = getCached(build);
|
||||
return (cached.tasks_complete_percentage !== build.tasks_complete_percentage);
|
||||
}
|
||||
|
||||
if (build.outcomeText !== "In Progress"){
|
||||
/* We have to ignore the value when it's Succeeded because it
|
||||
* goes back to 0
|
||||
*/
|
||||
percentDone = 100;
|
||||
} else {
|
||||
percentDone = build.percentDone;
|
||||
inProgress++;
|
||||
}
|
||||
function refreshMostRecentBuilds(){
|
||||
libtoaster.getMostRecentBuilds(
|
||||
libtoaster.ctx.mostRecentBuildsUrl,
|
||||
|
||||
$("#build-pc-done-" + build.id).text(percentDone);
|
||||
$("#build-pc-done-title-" + build.id).attr("title", percentDone);
|
||||
$("#build-pc-done-bar-" + build.id).css("width",
|
||||
String(percentDone) + "%");
|
||||
// success callback
|
||||
function (data) {
|
||||
var build;
|
||||
var tmpl;
|
||||
var container;
|
||||
var selector;
|
||||
var colourClass;
|
||||
var elements;
|
||||
|
||||
allPercentDone += percentDone;
|
||||
// classes on the parent which signify the build state and affect
|
||||
// the colour of the container for the build
|
||||
var buildStateClasses = 'alert-info alert-success alert-danger';
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
build = data[i];
|
||||
|
||||
if (buildFinished(build)) {
|
||||
// a build finished: reload the whole page so that the build
|
||||
// shows up in the builds table
|
||||
window.location.reload();
|
||||
}
|
||||
else if (stateChanged(build)) {
|
||||
// update the whole template
|
||||
tmpl = $.templates("#build-template");
|
||||
|
||||
html = tmpl.render(build);
|
||||
|
||||
selector = '[data-latest-build-result="' + build.id + '"] ' +
|
||||
'[data-role="build-status-container"]';
|
||||
container = $(selector);
|
||||
|
||||
container.html(html);
|
||||
|
||||
// style the outermost container for this build to reflect
|
||||
// the new build state (red, green, blue);
|
||||
// NB class set here should be in buildStateClasses
|
||||
colourClass = 'alert-info';
|
||||
if (build.state == 'Succeeded') {
|
||||
colourClass = 'alert-success';
|
||||
}
|
||||
else if (build.state == 'Failed') {
|
||||
colourClass = 'alert-danger';
|
||||
}
|
||||
|
||||
elements = $('[data-latest-build-result="' + build.id + '"]');
|
||||
elements.removeClass(buildStateClasses);
|
||||
elements.addClass(colourClass);
|
||||
}
|
||||
else if (progressChanged(build)) {
|
||||
// update the progress text
|
||||
selector = '#build-pc-done-' + build.id;
|
||||
$(selector).html(build.tasks_complete_percentage);
|
||||
|
||||
// update the progress bar
|
||||
selector = '#build-pc-done-bar-' + build.id;
|
||||
$(selector).width(build.tasks_complete_percentage + '%');
|
||||
}
|
||||
|
||||
if (allPercentDone === (100 * prjInfo.builds.length) && !inProgress)
|
||||
window.location.reload();
|
||||
buildData[build.id] = build;
|
||||
}
|
||||
},
|
||||
|
||||
/* Our progress bar is not still showing so shutdown the polling. */
|
||||
if ($(".progress .bar").length === 0)
|
||||
window.clearInterval(progressTimer);
|
||||
|
||||
});
|
||||
}, 1500);
|
||||
// fail callback
|
||||
function (data) {
|
||||
console.error(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.setInterval(refreshMostRecentBuilds, 1000);
|
||||
refreshMostRecentBuilds();
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
</script>
|
||||
<script src="{% static 'js/typeahead.jquery.js' %}">
|
||||
</script>
|
||||
<script src="{% static 'js/jsrender.min.js' %}">
|
||||
</script>
|
||||
<script src="{% static 'js/prettify.js' %}">
|
||||
</script>
|
||||
<script src="{% static 'js/libtoaster.js' %}">
|
||||
|
@ -32,6 +34,8 @@
|
|||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
$.views.settings.delimiters("<%", "%>");
|
||||
|
||||
libtoaster.ctx = {
|
||||
jsUrl : "{% static 'js/' %}",
|
||||
htmlUrl : "{% static 'html/' %}",
|
||||
|
@ -48,7 +52,9 @@
|
|||
xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
|
||||
projectId : {{project.id}},
|
||||
xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
|
||||
mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}",
|
||||
{% else %}
|
||||
mostRecentBuildsUrl: "{% url 'most_recent_builds' %}",
|
||||
projectId : undefined,
|
||||
projectPageUrl : undefined,
|
||||
projectName : undefined,
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
{% extends "baseprojectpage.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load projecttags %}
|
||||
{% load humanize %}
|
||||
|
||||
|
||||
{% block projectinfomain %}
|
||||
<!-- begin content -->
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- end left sidebar container -->
|
||||
<!-- Begin right container -->
|
||||
<div class="col-md-10">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<span data-toggle="tooltip" {%if buildrequest.brtarget_set.all.count > 1%}title="Targets: {%for target in buildrequest.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{buildrequest.brtarget_set.all.0.target}} {%if buildrequest.brtarget_set.all.count > 1%}(+ {{buildrequest.brtarget_set.all.count|add:"-1"}}){%endif%} {{buildrequest.get_machine}} </span>
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div class="alert alert-error">
|
||||
<p class="lead">
|
||||
<strong>Failed</strong>
|
||||
on {{ buildrequest.updated|date:'d/m/y H:i' }}
|
||||
with
|
||||
|
||||
<i class="icon-minus-sign error" style="margin-left:6px;"></i>
|
||||
<strong><a class="error accordion-toggle toggle-errors" href="#errors">
|
||||
{{buildrequest.brerror_set.all.count}} error{{buildrequest.brerror_set.all.count|pluralize}}
|
||||
</a></strong>
|
||||
<span class="pull-right">Build time: {{buildrequest.get_duration|sectohms}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="errors">
|
||||
<div class="accordion-group">
|
||||
<div class="accordion-heading">
|
||||
<a class="accordion-toggle error toggle-errors">
|
||||
<h2>
|
||||
<i class="icon-minus-sign"></i>
|
||||
{{buildrequest.brerror_set.all.count}} error{{buildrequest.brerror_set.all.count|pluralize}}
|
||||
</h2>
|
||||
</a>
|
||||
</div>
|
||||
<div class="accordion-body collapse in" id="collapse-errors">
|
||||
<div class="accordion-inner">
|
||||
<div class="col-md-10">
|
||||
{% for error in buildrequest.brerror_set.all %}
|
||||
<div class="alert alert-error">
|
||||
ERROR: <div class="air well"><pre>{{error.errmsg}}</pre></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end of row -->
|
||||
|
||||
|
||||
{%endblock%}
|
|
@ -1,26 +1,9 @@
|
|||
{% load static %}
|
||||
{% load projecttags %}
|
||||
{% load project_url_tag %}
|
||||
{% load humanize %}
|
||||
{% load project_url_tag %}
|
||||
<script src="{% static 'js/mrbsection.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var ctx = {
|
||||
mrbType : "{{mrb_type}}",
|
||||
}
|
||||
|
||||
try {
|
||||
mrbSectionInit(ctx);
|
||||
} catch (e) {
|
||||
document.write("Sorry, An error has occurred loading this page");
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% if mru %}
|
||||
|
||||
{% if mrb_type == 'project' %}
|
||||
<h2>
|
||||
Latest project builds
|
||||
|
@ -38,6 +21,7 @@
|
|||
<div id="latest-builds">
|
||||
{% for build in mru %}
|
||||
<div data-latest-build-result="{{build.id}}" class="alert build-result {% if build.outcome == build.SUCCEEDED %}alert-success{% elif build.outcome == build.FAILED %}alert-danger{% else %}alert-info{% endif %}">
|
||||
<!-- project title -->
|
||||
{% if mrb_type != 'project' %}
|
||||
<div class="row project-name">
|
||||
<div class="col-md-12">
|
||||
|
@ -48,134 +32,180 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
|
||||
<a href="{% url 'builddashboard' build.pk %}" class="alert-link">
|
||||
{% endif %}
|
||||
|
||||
{% if build.target_set.all.count > 0 %}
|
||||
<span data-toggle="tooltip"
|
||||
{% if build.target_set.all.count > 1 %}
|
||||
{{build.get_sorted_target_list.0.target}}
|
||||
title="Recipes:
|
||||
{% for target in build.get_sorted_target_list %}
|
||||
{% if target.task %}
|
||||
{{target.target}}:{{target.task}}
|
||||
{% else %}
|
||||
{{target.target}}
|
||||
{% endif %}
|
||||
{% endfor %}"
|
||||
{% endif %}
|
||||
>
|
||||
{% if build.target_set.all.0.task %}
|
||||
{{build.get_sorted_target_list.0.target}}:{{build.target_set.all.0.task}}
|
||||
{% else %}
|
||||
{{build.get_sorted_target_list.0.target}}
|
||||
{% endif %}
|
||||
|
||||
{% if build.target_set.all.count > 1 %}
|
||||
(+{{build.target_set.all.count|add:"-1"}})
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="row" data-role="build-status-container">
|
||||
<div class="col-md-12">
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
|
||||
<div class="col-md-2">
|
||||
{% if build.completed_on|format_build_date %}
|
||||
{{build.completed_on|date:'d/m/y H:i'}}
|
||||
{% else %}
|
||||
{{ build.completed_on|date:'H:i' }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
|
||||
<div class="col-md-2">
|
||||
{% if build.errors.count %}
|
||||
<span class="glyphicon glyphicon-minus-sign"></span>
|
||||
<a href="{%url 'builddashboard' build.pk%}#errors" class="alert-link">
|
||||
{{build.errors.count}} error{{build.errors.count|pluralize}}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
{% if build.warnings.count %}
|
||||
<span class="glyphicon glyphicon-warning-sign build-warnings"></span>
|
||||
<a href="{%url 'builddashboard' build.pk%}#warnings" class="alert-link build-warnings">
|
||||
{{build.warnings.count}} warning{{build.warnings.count|pluralize}}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
Build time: <a class="alert-link" href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}
|
||||
</a>
|
||||
|
||||
{% if build.project.is_default %}
|
||||
<span class="pull-right glyphicon glyphicon-question-sign get-help {% if build.outcome == build.SUCCEEDED %}get-help-green{% elif build.outcome == build.FAILED %}get-help-red{% else %}get-help-blue{% endif %}"
|
||||
title="Builds in this project cannot be started from Toaster: they are started from the command line">
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="#" class="run-again-btn alert-link {% if build.outcome == build.SUCCEEDED %}success{% elif build.outcome == build.FAILED %}danger{% else %}info{% endif %} pull-right"
|
||||
data-request-url="{% url 'xhr_buildrequest' build.project.pk %}"
|
||||
data-target='{{build.target_set.all|get_tasks|json}}'>
|
||||
<span class="glyphicon glyphicon-repeat"></span>
|
||||
Rebuild
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if build.outcome == build.IN_PROGRESS %}
|
||||
<div class="col-md-4" style="display:none" id="cancelling-msg-{{build.buildrequest.pk}}">
|
||||
Cancelling the build ...
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-md-offset-1 progress-info">
|
||||
<div class="progress" id="build-pc-done-title-{{build.pk}}">
|
||||
<div id="build-pc-done-bar-{{build.pk}}" style="width: {{build.completeper}}%;" class="progress-bar">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 progress-info">
|
||||
<span id="build-pc-done-{{build.pk}}">{{build.completeper}}</span>% of tasks complete
|
||||
{# No build cancel for command line builds project #}
|
||||
{% if build.project.is_default %}
|
||||
<span class="glyphicon glyphicon-question-sign get-help get-help-blue pull-right" title="Builds in this project cannot be cancelled from Toaster: they can only be cancelled from the command line"></span>
|
||||
{% else %}
|
||||
<a href="#" class="cancel-build-btn pull-right alert-link"
|
||||
data-buildrequest-id={{build.buildrequest.pk}}
|
||||
data-request-url="{% url 'xhr_buildrequest' build.project.pk %}">
|
||||
<span class="glyphicon glyphicon-remove-circle"></span>
|
||||
Cancel
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %} {# end if in progress #}
|
||||
|
||||
{% if build.outcome == build.CANCELLED %}
|
||||
<div class="col-md-6">
|
||||
Build cancelled
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<a href="#" class="info pull-right run-again-btn alert-link"
|
||||
data-request-url="{% url 'xhr_buildrequest' build.project.pk %}"
|
||||
data-target='{{build.target_set.all|get_tasks|json}}'>
|
||||
<span class="glyphicon glyphicon-repeat"></span>
|
||||
Rebuild
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- build main template -->
|
||||
<script id="build-template" type="text/x-jsrender">
|
||||
<div class="col-md-3">
|
||||
<!-- only show link for completed builds -->
|
||||
<%if state == 'Succeeded' || state == 'Failed'%>
|
||||
<a class="alert-link" href="<%:dashboard_url%>">
|
||||
<span data-toggle="tooltip" data-role="targets-text" title="Recipes: <%:targets%>">
|
||||
<%:targets_abbreviated%>
|
||||
</span>
|
||||
</a>
|
||||
<%else%>
|
||||
<span data-toggle="tooltip" data-role="targets-text" title="Recipes: <%:targets%>">
|
||||
<%:targets_abbreviated%>
|
||||
</span>
|
||||
<%/if%>
|
||||
</div>
|
||||
|
||||
<%if state == 'Queued'%>
|
||||
<%include tmpl='#queued-build-template'/%>
|
||||
<%else state == 'Succeeded' || state == 'Failed'%>
|
||||
<%include tmpl='#succeeded-or-failed-build-template'/%>
|
||||
<%else state == 'Cancelling'%>
|
||||
<%include tmpl='#cancelling-build-template'/%>
|
||||
<%else state == 'In Progress'%>
|
||||
<%include tmpl='#in-progress-build-template'/%>
|
||||
<%else state == 'Cancelled'%>
|
||||
<%include tmpl='#cancelled-build-template'/%>
|
||||
<%/if%>
|
||||
</script>
|
||||
|
||||
<!-- queued build -->
|
||||
<script id="queued-build-template" type="text/x-jsrender">
|
||||
<div class="col-md-5">
|
||||
Build queued
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<%if is_default_project_build%>
|
||||
<!-- no cancel icon -->
|
||||
<span class="glyphicon glyphicon-question-sign get-help get-help-blue pull-right" title="Builds in this project cannot be cancelled from Toaster: they can only be cancelled from the command line"></span>
|
||||
<%else%>
|
||||
<!-- cancel button -->
|
||||
<span class="cancel-build-btn pull-right alert-link"
|
||||
data-buildrequest-id="<%:id%>" data-request-url="<%:cancel_url%>">
|
||||
<span class="glyphicon glyphicon-remove-circle"></span>
|
||||
Cancel
|
||||
</span>
|
||||
<%/if%>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- in progress build -->
|
||||
<script id="in-progress-build-template" type="text/x-jsrender">
|
||||
<!-- progress bar and task completion percentage -->
|
||||
<div data-role="build-status" class="col-md-4 col-md-offset-1 progress-info">
|
||||
<!-- progress bar -->
|
||||
<div class="progress" id="build-pc-done-title-<%:id%>">
|
||||
<div id="build-pc-done-bar-<%:id%>"
|
||||
style="width: <%:tasks_complete_percentage%>%;"
|
||||
class="progress-bar">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 progress-info">
|
||||
<!-- task completion percentage -->
|
||||
<span id="build-pc-done-<%:id%>"><%:tasks_complete_percentage%></span>% of
|
||||
tasks complete
|
||||
<%if is_default_project_build%>
|
||||
<!-- no cancel icon -->
|
||||
<span class="glyphicon glyphicon-question-sign get-help get-help-blue pull-right" title="Builds in this project cannot be cancelled from Toaster: they can only be cancelled from the command line"></span>
|
||||
<%else%>
|
||||
<!-- cancel button -->
|
||||
<span class="cancel-build-btn pull-right alert-link"
|
||||
data-buildrequest-id="<%:id%>" data-request-url="<%:cancel_url%>">
|
||||
<span class="glyphicon glyphicon-remove-circle"></span>
|
||||
Cancel
|
||||
</span>
|
||||
<%/if%>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- cancelling build -->
|
||||
<script id="cancelling-build-template" type="text/x-jsrender">
|
||||
<div class="col-md-9">
|
||||
Cancelling the build ...
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- succeeded or failed build -->
|
||||
<script id="succeeded-or-failed-build-template" type="text/x-jsrender">
|
||||
<!-- completed_on -->
|
||||
<div class="col-md-2">
|
||||
<%:completed_on%>
|
||||
</div>
|
||||
|
||||
<!-- errors -->
|
||||
<div class="col-md-2">
|
||||
<%if errors%>
|
||||
<span class="glyphicon glyphicon-minus-sign"></span>
|
||||
<a href="<%:dashboard_errors_url%>" class="alert-link">
|
||||
<%:errors%> error<%:errors_pluralize%>
|
||||
</a>
|
||||
<%/if%>
|
||||
</div>
|
||||
|
||||
<!-- warnings -->
|
||||
<div class="col-md-2">
|
||||
<%if warnings%>
|
||||
<span class="glyphicon glyphicon-minus-sign"></span>
|
||||
<a href="<%:dashboard_warnings_url%>" class="alert-link">
|
||||
<%:warnings%> warning<%:warnings_pluralize%>
|
||||
</a>
|
||||
<%/if%>
|
||||
</div>
|
||||
|
||||
<!-- build time -->
|
||||
<div class="col-md-3">
|
||||
Build time: <a class="alert-link" href="<%:buildtime_url%>"><%:buildtime%></a>
|
||||
|
||||
<%if is_default_project_build%>
|
||||
<!-- info icon -->
|
||||
<span class="pull-right glyphicon glyphicon-question-sign get-help <%if state == 'Success'%>get-help-green<%else state == 'Failed'%>get-help-red<%else%>get-help-blue<%/if%>"
|
||||
title="Builds in this project cannot be started from Toaster: they are started from the command line">
|
||||
</span>
|
||||
<%else%>
|
||||
<!-- rebuild button -->
|
||||
<span class="rebuild-btn alert-link <%if state == 'Success'%>success<%else state == 'Failed'%>danger<%else%>info<%/if%> pull-right"
|
||||
data-request-url="<%:rebuild_url%>" data-target='<%:build_targets_json%>'>
|
||||
<span class="glyphicon glyphicon-repeat"></span>
|
||||
Rebuild
|
||||
</span>
|
||||
<%/if%>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- cancelled build -->
|
||||
<script id="cancelled-build-template" type="text/x-jsrender">
|
||||
<!-- build cancelled message -->
|
||||
<div class="col-md-6">
|
||||
Build cancelled
|
||||
</div>
|
||||
|
||||
<!-- rebuild button -->
|
||||
<div class="col-md-3">
|
||||
<span class="info pull-right rebuild-btn alert-link"
|
||||
data-request-url="<%:rebuild_url%>" data-target='<%:build_targets_json%>'>
|
||||
<span class="glyphicon glyphicon-repeat"></span>
|
||||
Rebuild
|
||||
</span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var ctx = {
|
||||
mrbType : "{{mrb_type}}",
|
||||
}
|
||||
|
||||
try {
|
||||
mrbSectionInit(ctx);
|
||||
} catch (e) {
|
||||
document.write("Sorry, An error has occurred loading this page");
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
<h2 class="top-air" data-role="page-title"></h2>
|
||||
|
||||
{% if not build_in_progress_none_completed %}
|
||||
{% if not build_in_progress_none_completed %}
|
||||
{% url 'projectbuilds' project.id as xhr_table_url %}
|
||||
{% include 'toastertable.html' %}
|
||||
{% endif %}
|
||||
|
|
|
@ -270,14 +270,6 @@ def get_dict_value(dictionary, key):
|
|||
except (KeyError, IndexError):
|
||||
return ''
|
||||
|
||||
@register.filter
|
||||
def format_build_date(completed_on):
|
||||
now = timezone.now()
|
||||
delta = now - completed_on
|
||||
|
||||
if delta.days >= 1:
|
||||
return True
|
||||
|
||||
@register.filter
|
||||
def is_shaid(text):
|
||||
""" return True if text length is 40 characters and all hex-digits
|
||||
|
|
|
@ -214,6 +214,9 @@ urlpatterns = patterns('toastergui.views',
|
|||
api.XhrBuildRequest.as_view(),
|
||||
name='xhr_buildrequest'),
|
||||
|
||||
url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(),
|
||||
name='most_recent_builds'),
|
||||
|
||||
# default redirection
|
||||
url(r'^$', RedirectView.as_view(url='landing', permanent=True)),
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue