[gnome-continuous-yocto/gnomeostree-3.28-rocko: 1804/8267] bitbake: toaster: move most recent builds templating to client



commit 952ffb3e1f4a00793e0c9c49bc0c8fb8729424c4
Author: Elliot Smith <elliot smith intel com>
Date:   Wed Jun 29 15:41:56 2016 +0100

    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>

 bitbake/LICENSE                                    |    2 +
 .../migrations/0005_reorder_buildrequest_states.py |   19 ++
 bitbake/lib/toaster/bldcontrol/models.py           |   12 +-
 bitbake/lib/toaster/orm/models.py                  |   44 ++-
 .../toaster/tests/browser/test_all_builds_page.py  |    4 +-
 bitbake/lib/toaster/toastergui/api.py              |  105 +++++++-
 .../lib/toaster/toastergui/static/css/default.css  |    1 +
 .../toaster/toastergui/static/js/jsrender.min.js   |    4 +
 .../lib/toaster/toastergui/static/js/libtoaster.js |   16 +
 .../lib/toaster/toastergui/static/js/mrbsection.js |  178 +++++++-----
 bitbake/lib/toaster/toastergui/templates/base.html |    6 +
 .../toastergui/templates/buildrequestdetails.html  |   64 ----
 .../toaster/toastergui/templates/mrb_section.html  |  308 +++++++++++---------
 .../templates/projectbuilds-toastertable.html      |    2 +-
 .../toaster/toastergui/templatetags/projecttags.py |    8 -
 bitbake/lib/toaster/toastergui/urls.py             |    3 +
 16 files changed, 473 insertions(+), 303 deletions(-)
---
diff --git a/bitbake/LICENSE b/bitbake/LICENSE
index 7a0c272..4a09b0f 100644
--- a/bitbake/LICENSE
+++ b/bitbake/LICENSE
@@ -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.
diff --git a/bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py 
b/bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py
new file mode 100644
index 0000000..4bb9517
--- /dev/null
+++ b/bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py
@@ -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),
+        ),
+    ]
diff --git a/bitbake/lib/toaster/bldcontrol/models.py b/bitbake/lib/toaster/bldcontrol/models.py
index f06c562..f055480 100644
--- a/bitbake/lib/toaster/bldcontrol/models.py
+++ b/bitbake/lib/toaster/bldcontrol/models.py
@@ -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):
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index caacc2a..2df6d49 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -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
-        """
+    def get_buildrequest(self):
+        buildrequest = None
+        if hasattr(self, 'buildrequest'):
+            buildrequest = self.buildrequest
+        return buildrequest
 
+    def is_queued(self):
         from bldcontrol.models import BuildRequest
+        buildrequest = self.get_buildrequest()
+        if buildrequest:
+            return buildrequest.state == BuildRequest.REQ_QUEUED
+        else:
+            return False
 
-        build_request = None
-        if hasattr(self, 'buildrequest'):
-            build_request = self.buildrequest
+    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
 
-        if (build_request
-                and build_request.state != BuildRequest.REQ_INPROGRESS
-                and self.outcome == Build.IN_PROGRESS):
-            return self.buildrequest.get_state_display()
+    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()
 
diff --git a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py 
b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
index 5ea6532..521a280 100644
--- a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
@@ -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')
diff --git a/bitbake/lib/toaster/toastergui/api.py b/bitbake/lib/toaster/toastergui/api.py
index 414afce..aa3cbd8 100644
--- a/bitbake/lib/toaster/toastergui/api.py
+++ b/bitbake/lib/toaster/toastergui/api.py
@@ -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)
diff --git a/bitbake/lib/toaster/toastergui/static/css/default.css 
b/bitbake/lib/toaster/toastergui/static/css/default.css
index 0d3570a..3a0fbb8 100644
--- a/bitbake/lib/toaster/toastergui/static/css/default.css
+++ b/bitbake/lib/toaster/toastergui/static/css/default.css
@@ -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; }
diff --git a/bitbake/lib/toaster/toastergui/static/js/jsrender.min.js 
b/bitbake/lib/toaster/toastergui/static/js/jsrender.min.js
new file mode 100644
index 0000000..87cac4e
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/static/js/jsrender.min.js
@@ -0,0 +1,4 @@
+/*! JsRender v0.9.78 (Beta): http://jsviews.com/#jsrender */
+/*! **VERSION FOR WEB** (For NODE.JS see http://jsviews.com/download/jsrender-node.js) */
+!function(e,t){var n=t.jQuery;"object"==typeof 
exports?module.exports=n?e(t,n):function(n){if(n&&!n.fn)throw"Provide jQuery or null";return 
e(t,n)}:"function"==typeof define&&define.amd?define(function(){return e(t)}):e(t,!1)}(function(e,t){"use 
strict";function n(e,t){return function(){var n,r=this,i=r.base;return 
r.base=e,n=t.apply(r,arguments),r.base=i,n}}function r(e,t){return 
te(t)&&(t=n(e?e._d?e:n(s,e):s,t),t._d=1),t}function i(e,t){for(var n in 
t.props)Re.test(n)&&(e[n]=r(e[n],t.props[n]))}function o(e){return e}function s(){return""}function 
a(e){try{throw console.log("JsRender dbg breakpoint: "+e),"dbg breakpoint"}catch(t){}return 
this.base?this.baseApply(arguments):e}function d(e){this.name=(t.link?"JsViews":"JsRender")+" 
Error",this.message=e||this.name}function u(e,t){for(var n in t)e[n]=t[n];return e}function l(e,t,n){return 
e?(de.delimiters=[e,t,ve=n?n.charAt(0):ve],pe=e.charAt(0),ce=e.charAt(1),fe=t.charAt(0),ge=t.charAt(1),e="\\"+pe+"(\\"+ve+")?\\"+ce,t="\\"+
 
fe+"\\"+ge,G="(?:(\\w+(?=[\\/\\s\\"+fe+"]))|(\\w+)?(:)|(>)|(\\*))\\s*((?:[^\\"+fe+"]|\\"+fe+"(?!\\"+ge+"))*?)",ae.rTag="(?:"+G+")",G=new
 RegExp("(?:"+e+G+"(\\/)?|\\"+pe+"(\\"+ve+")?\\"+ce+"(?:(?:\\/(\\w+))\\s*|!--[\\s\\S]*?--))"+t,"g"),W=new 
RegExp("<.*>|([^\\\\]|^)[{}]|"+e+".*"+t),le):de.delimiters}function p(e,t){t||e===!0||(t=e,e=void 0);var 
n,r,i,o,s=this,a=!t||"root"===t;if(e){if(o=t&&s.type===t&&s,!o)if(n=s.views,s._.useKey){for(r in 
n)if(o=t?n[r].get(e,t):n[r])break}else for(r=0,i=n.length;!o&&i>r;r++)o=t?n[r].get(e,t):n[r]}else 
if(a)for(;s.parent;)o=s,s=s.parent;else for(;s&&!o;)o=s.type===t?s:void 0,s=s.parent;return o}function 
c(){var e=this.get("item");return e?e.index:void 0}function f(){return this.index}function g(t){var 
n,r=this,i=r.linkCtx,o=(r.ctx||{})[t];return void 0===o&&i&&i.ctx&&(o=i.ctx[t]),void 
0===o&&(o=oe[t]),o&&te(o)&&!o._wrp&&(n=function(){return 
o.apply(this&&this!==e?this:r,arguments)},n._wrp=r,u(n,o)),n||o}function v(e){return e&&(e.fn?e:this.g
 etRsc("templates",e)||re(e))}function h(e,t,n,r){var o,s,a="number"==typeof 
n&&t.tmpl.bnds[n-1],d=t.linkCtx;return void 
0!==r?n=r={props:{},args:[r]}:a&&(n=a(t.data,t,ae)),s=n.args[0],(e||a)&&(o=d&&d.tag,o||(o=u(new 
ae._tg,{_:{inline:!d,bnd:a,unlinked:!0},tagName:":",cvt:e,flow:!0,tagCtx:n}),d&&(d.tag=o,o.linkCtx=d),n.ctx=L(n.ctx,(d?d.view:t).ctx)),o._er=r&&s,i(o,n),n.view=t,o.ctx=n.ctx||o.ctx||{},n.ctx=void
 0,s=o.cvtArgs("true"!==e&&e)[0],s=a&&t._.onRender?t._.onRender(s,t,o):s),void 0!=s?s:""}function m(e){var 
t=this,n=t.tagCtx,r=n.view,i=n.args;return e=e||t.convert,e=e&&(""+e===e?r.getRsc("converters",e)||S("Unknown 
converter: 
'"+e+"'"):e),i=i.length||n.index?e?i.slice():i:[r.data],e&&(e.depends&&(t.depends=ae.getDeps(t.depends,t,e.depends,e)),i[0]=e.apply(t,i)),i}function
 w(e,t){for(var n,r,i=this;void 0===n&&i;)r=i.tmpl&&i.tmpl[e],n=r&&r[t],i=i.parent;return n||Y[e][t]}function 
x(e,t,n,r,o,s){t=t||X;var a,d,u,l,p,c,f,g,v,h,m,w,x,b,_,y,k,j,C,A="",T=t.linkCtx||0,V=t.ctx,
 R=n||t.tmpl,M="number"==typeof 
r&&t.tmpl.bnds[r-1];for("tag"===e._is?(a=e,e=a.tagName,r=a.tagCtxs,u=a.template):(d=t.getRsc("tags",e)||S("Unknown
 tag: {{"+e+"}} "),u=d.template),void 
0!==s?(A+=s,r=s=[{props:{},args:[]}]):M&&(r=M(t.data,t,ae)),g=r.length,f=0;g>f;f++)h=r[f],(!T||!T.tag||f&&!T.tag._.inline||a._er)&&((w=R.tmpls&&h.tmpl)&&(w=h.content=R.tmpls[w-1]),h.index=f,h.tmpl=w,h.render=N,h.view=t,h.ctx=L(h.ctx,V)),(n=h.props.tmpl)&&(h.tmpl=t.getTmpl(n)),a||(a=new
 
d._ctr,x=!!a.init,a.parent=c=V&&V.tag,a.tagCtxs=r,C=a.dataMap,T&&(a._.inline=!1,T.tag=a,a.linkCtx=T),(a._.bnd=M||T.fn)?a._.arrVws={}:a.dataBoundOnly&&S("{^{"+e+"}}
 tag must be 
data-bound")),r=a.tagCtxs,C=a.dataMap,h.tag=a,C&&r&&(h.map=r[f].map),a.flow||(m=h.ctx=h.ctx||{},l=a.parents=m.parentTags=V&&L(m.parentTags,V.parentTags)||{},c&&(l[c.tagName]=c),l[a.tagName]=m.tag=a);if(!(a._er=s)){for(i(a,r[0]),a.rendering={},f=0;g>f;f++)h=a.tagCtx=r[f],k=h.props,y=a.cvtArgs(),(b=k.dataMap||C)&&(y.length||k.dataMap)&&(_=h.ma
 p,_&&_.src===y[0]&&!o||(_&&_.src&&_.unmap(),_=h.map=b.map(y[0],k,void 
0,!a._.bnd)),y=[_.tgt]),a.ctx=h.ctx,f||(x&&(j=a.template,a.init(h,T,a.ctx),x=void 
0),T&&(T.attr=a.attr=T.attr||a.attr),p=a.attr,a._.noVws=p&&p!==Ee),v=void 
0,a.render&&(v=a.render.apply(a,y)),y.length||(y=[t]),void 0===v&&(v=h.render(y[0],!0)||(o?void 
0:"")),A=A?A+(v||""):v;a.rendering=void 0}return 
a.tagCtx=r[0],a.ctx=a.tagCtx.ctx,a._.noVws&&a._.inline&&(A="text"===p?ie.html(A):""),M&&t._.onRender?t._.onRender(A,t,a):A}function
 b(e,t,n,r,i,o,s,a){var 
d,u,l,p=this,f="array"===t;p.content=a,p.views=f?[]:{},p.parent=n,p.type=t||"top",p.data=r,p.tmpl=i,l=p._={key:0,useKey:f?0:1,id:""+$e++,onRender:s,bnds:{}},p.linked=!!s,n?(d=n.views,u=n._,u.useKey?(d[l.key="_"+u.useKey++]=p,p.index=Ue,p.getIndex=c):d.length===(l.key=p.index=o)?d.push(p):d.splice(o,0,p),p.ctx=e||n.ctx):p.ctx=e}function
 _(e){var t,n,r,i,o,s,a;for(t in Oe)if(o=Oe[t],(s=o.compile)&&(n=e[t+"s"]))for(r in 
n)i=n[r]=s(r,n[r],e,0),i._is=t,i&&(a=ae.on
 Store[t])&&a(r,i,s)}function y(e,t,n){function i(){var t=this;t._={inline:!0,unlinked:!0},t.tagName=e}var 
o,s,a,d=new 
ae._tg;if(te(t)?t={depends:t.depends,render:t}:""+t===t&&(t={template:t}),s=t.baseTag){t.flow=!!t.flow,t.baseTag=s=""+s===s?n&&n.tags[s]||se[s]:s,d=u(d,s);for(a
 in t)d[a]=r(s[a],t[a])}else d=u(d,t);return void 
0!==(o=d.template)&&(d.template=""+o===o?re[o]||re(o):o),d.init!==!1&&((i.prototype=d).constructor=d._ctr=i),n&&(d._parentTmpl=n),d}function
 k(e){return this.base.apply(this,e)}function j(e,n,r,i){function o(n){var 
o,a;if(""+n===n||n.nodeType>0&&(s=n)){if(!s)if(/^\.\/[^\\:*?"<>]*$/.test(n))(a=re[e=e||n])?n=a:s=document.getElementById(n);else
 
if(t.fn&&!W.test(n))try{s=t(document).find(n)[0]}catch(d){}s&&(i?n=s.innerHTML:(o=s.getAttribute(Se),o?o!==Ie?(n=re[o],delete
 
re[o]):t.fn&&(n=t.data(s)[Ie]):(e=e||(t.fn?Ie:n),n=j(e,s.innerHTML,r,i)),n.tmplName=e=e||o,e!==Ie&&(re[e]=n),s.setAttribute(Se,e),t.fn&&t.data(s,Ie,n))),s=void
 0}else n.fn||(n=void 0);return 
 n}var s,a,d=n=n||"";return 0===i&&(i=void 
0,d=o(d)),i=i||(n.markup?n:{}),i.tmplName=e,r&&(i._parentTmpl=r),!d&&n.markup&&(d=o(n.markup))&&d.fn&&(d=d.markup),void
 0!==d?(d.fn||n.fn?d.fn&&(a=d):(n=V(d,i),U(d.replace(ke,"\\$&"),n)),a||(_(i),a=u(function(){return 
n.render.apply(n,arguments)},n)),e&&!r&&e!==Ie&&(qe[e]=a),a):void 0}function C(e,n){return 
t.isFunction(e)?e.call(n):e}function A(e){var t,n=[],r=e.length;for(t=0;r>t;t++)n.push(e[t].unmap());return 
n}function T(e,n){function r(e){l.apply(this,e)}function i(){return new r(arguments)}function o(e,t){var 
n,r,i,o,s,a=c.length;for(n=0;a>n;n++)o=c[n],r=void 0,o+""!==o&&(r=o,o=r.getter),void 0===(s=e[o])&&r&&void 
0!==(i=r.defaultVal)&&(s=C(i,e)),t(s,r&&p[r.type],o)}function s(n){n=n+""===n?JSON.parse(n):n;var 
r,i,s,u=n,l=[];if(t.isArray(n)){for(n=n||[],i=n.length,r=0;i>r;r++)l.push(this.map(n[r]));return 
l._is=e,l.unmap=d,l.merge=a,l}if(n){o(n,function(e,t){t&&(e=t.map(e)),l.push(e)}),u=this.apply(this,l);for(s 
in n)s===ee||b
 [s]||(u[s]=n[s])}return u}function a(e){e=e+""===e?JSON.parse(e):e;var 
n,r,s,a,d,u,l,p,c,f,v=this;if(t.isArray(v)){for(p={},f=[],s=e.length,a=v.length,n=0;s>n;n++){for(c=e[n],l=!1,r=0;a>r&&!l;r++)p[r]||(u=v[r],g&&(p[r]=l=g+""===g?c[g]&&(b[g]?u[g]():u[g])===c[g]:g(u,c)));l?(u.merge(c),f.push(u)):f.push(i.map(c))}return
 
void(x?x(v).refresh(f,!0):v.splice.apply(v,[0,v.length].concat(f)))}o(e,function(e,t,n){t?v[n]().merge(e):v[n](e)});for(d
 in e)d===ee||b[d]||(v[d]=e[d])}function d(){var e,n,r,i,o,s,a=this;if(t.isArray(a))return 
A(a);for(e={},i=c.length,r=0;i>r;r++)n=c[r],o=void 
0,n+""!==n&&(o=n,n=o.getter),s=a[n](),e[n]=o&&s&&p[o.type]?t.isArray(s)?A(s):s.unmap():s;for(n in 
a)"_is"===n||b[n]||n===ee||"_"===n.charAt(0)&&b[n.slice(1)]||t.isFunction(a[n])||(e[n]=a[n]);return e}var 
u,l,p=this,c=n.getters,f=n.extend,g=n.id,v=t.extend({_is:e||"unnamed",unmap:d,merge:a},f),h="",m="",w=c?c.length:0,x=t.observable,b={};for(r.prototype=v,u=0;w>u;u++)!function(e){e=e.getter||e,b[e]=u+1;v
 ar t="_"+e;h+=(h?",":"")+e,m+="this."+t+" = "+e+";\n",v[e]=v[e]||function(n){return 
arguments.length?void(x?x(this).setProperty(e,n):this[t]=n):this[t]},x&&(v[e].set=v[e].set||function(e){this[t]=e})}(c[u]);return
 l=new 
Function(h,m.slice(0,-1)),l.prototype=v,v.constructor=l,i.map=s,i.getters=c,i.extend=f,i.id=g,i}function 
V(e,n){var r,i=ue._wm||{},o=u({tmpls:[],links:{},bnds:[],_is:"template",render:N},n);return 
o.markup=e,n.htmlTag||(r=Ae.exec(e),o.htmlTag=r?r[1].toLowerCase():""),r=i[o.htmlTag],r&&r!==i.div&&(o.markup=t.trim(o.markup)),o}function
 R(e,t){function n(i,o,s){var a,d,u,l;if(i&&typeof 
i===Fe&&!i.nodeType&&!i.markup&&!i.getTgt&&!("viewModel"===e&&i.getters||i.extend)){for(u in 
i)n(u,i[u],o);return o||Y}return void 0===o&&(o=i,i=void 0),i&&""+i!==i&&(s=o,o=i,i=void 
0),l=s?"viewModel"===e?s:s[r]=s[r]||{}:n,d=t.compile,null===o?i&&delete 
l[i]:(o=d?d.call(l,i,o,s,0):o,i&&(l[i]=o)),d&&o&&(o._is=e),o&&(a=ae.onStore[e])&&a(i,o,d),o}var 
r=e+"s";Y[r]=n}function M(e){le[e
 ]=function(t){return arguments.length?(de[e]=t,le):de[e]}}function $(e){function 
t(t,n){this.tgt=e.getTgt(t,n)}return 
te(e)&&(e={getTgt:e}),e.baseMap&&(e=u(u({},e.baseMap),e)),e.map=function(e,n){return new t(e,n)},e}function 
N(e,t,n,r,i,o){var s,a,d,u,l,p,c,f,g=r,v="";if(t===!0?(n=t,t=void 0):typeof t!==Fe&&(t=void 
0),(d=this.tag)?(l=this,g=g||l.view,u=g.getTmpl(d.template||l.tmpl),arguments.length||(e=g)):u=this,u){if(!g&&e&&"view"===e._is&&(g=e),g&&e===g&&(e=g.data),p=!g,me=me||p,g||((t=t||{}).root=e),!me||ue.useViews||u.useViews||g&&g!==X)v=E(u,e,t,n,g,i,o,d);else{if(g?(c=g.data,f=g.index,g.index=Ue):(g=X,g.data=e,g.ctx=t),ne(e)&&!n)for(s=0,a=e.length;a>s;s++)g.index=s,g.data=e[s],v+=u.fn(e[s],g,ae);else
 g.data=e,v+=u.fn(e,g,ae);g.data=c,g.index=f}p&&(me=void 0)}return v}function E(e,t,n,r,i,o,s,a){function 
d(e){_=u({},n),_[x]=e}var 
l,p,c,f,g,v,h,m,w,x,_,y,k="";if(a&&(w=a.tagName,y=a.tagCtx,n=n?L(n,a.ctx):a.ctx,e===i.content?h=e!==i.ctx._wrp?i.ctx._wrp:void
 0:e!==y.conte
 
nt?e===a.template?(h=y.tmpl,n._wrp=y.content):h=y.content||i.content:h=i.content,y.props.link===!1&&(n=n||{},n.link=!1),(x=y.props.itemVar)&&("~"!==x.charAt(0)&&I("Use
 
itemVar='~myItem'"),x=x.slice(1))),i&&(s=s||i._.onRender,n=L(n,i.ctx)),o===!0&&(v=!0,o=0),s&&(n&&n.link===!1||a&&a._.noVws)&&(s=void
 0),m=s,s===!0&&(m=void 0,s=i._.onRender),n=e.helpers?L(e.helpers,n):n,_=n,ne(t)&&!r)for(c=v?i:void 
0!==o&&i||new 
b(n,"array",i,t,e,o,s),i&&i._.useKey&&(c._.bnd=!a||a._.bnd&&a),x&&(c.it=x),x=c.it,l=0,p=t.length;p>l;l++)x&&d(t[l]),f=new
 b(_,"item",c,t[l],e,(o||0)+l,s,h),g=e.fn(t[l],f,ae),k+=c._.onRender?c._.onRender(g,f):g;else 
x&&d(t),c=v?i:new b(_,w||"data",i,t,e,o,s,h),a&&!a.flow&&(c.tag=a),k+=e.fn(t,c,ae);return m?m(k,c):k}function 
F(e,t,n){var r=void 0!==n?te(n)?n.call(t.data,e,t):n||"":"{Error: "+e.message+"}";return de.onError&&void 
0!==(n=de.onError.call(t.data,e,n&&r,t))&&(r=n),t&&!t.linkCtx?ie.html(r):r}function S(e){throw new 
ae.Err(e)}function I(e){S("Syntax error\n"+e)
 }function U(e,t,n,r,i){function o(t){t-=v,t&&m.push(e.substr(v,t).replace(_e,"\\n"))}function 
s(t,n){t&&(t+="}}",I((n?"{{"+n+"}} block has {{/"+t+" without {{"+t:"Unmatched or missing {{/"+t)+", in 
template:\n"+e))}function 
a(a,d,u,c,g,x,b,_,y,k,j,C){(b&&d||y&&!u||_&&":"===_.slice(-1)||k)&&I(a),x&&(g=":",c=Ee),y=y||n&&!i;var 
A=(d||n)&&[[]],T="",V="",R="",M="",$="",N="",E="",F="",S=!y&&!g;u=u||(_=_||"#data",g),o(C),v=C+a.length,b?f&&m.push(["*","\n"+_.replace(/^:/,"ret+=
 ").replace(ye,"$1")+";\n"]):u?("else"===u&&(Ce.test(_)&&I('for "{{else if expr}}" use "{{else 
expr}}"'),A=w[7]&&[[]],w[8]=e.substring(w[8],C),w=h.pop(),m=w[2],S=!0),_&&O(_.replace(_e," 
"),A,t).replace(je,function(e,t,n,r,i,o,s,a){return 
r="'"+i+"':",s?(V+=o+",",M+="'"+a+"',"):n?(R+=r+o+",",N+=r+"'"+a+"',"):t?E+=o:("trigger"===i&&(F+=o),T+=r+o+",",$+=r+"'"+a+"',",p=p||Re.test(i)),""}).slice(0,-1),A&&A[0]&&A.pop(),l=[u,c||!!r||p||"",S&&[],J(M||(":"===u?"'#data',":""),$,N),J(V||(":"===u?"data,":""),T,R),E,F,A||0
 
],m.push(l),S&&(h.push(w),w=l,w[8]=v)):j&&(s(j!==w[0]&&"else"!==w[0]&&j,w[0]),w[8]=e.substring(w[8],C),w=h.pop()),s(!w&&j),m=w[2]}var
 
d,u,l,p,c,f=de.allowCode||t&&t.allowCode||le.allowCode===!0,g=[],v=0,h=[],m=g,w=[,,g];if(f&&(t.allowCode=f),n&&(void
 
0!==r&&(e=e.slice(0,-r.length-2)+ge),e=pe+e+ge),s(h[0]&&h[0][2].pop()[0]),e.replace(G,a),o(e.length),(v=g[g.length-1])&&s(""+v!==v&&+v[8]===v[8]&&v[0]),n){for(u=B(g,e,n),c=[],d=g.length;d--;)c.unshift(g[d][7]);q(u,c)}else
 u=B(g,t);return u}function q(e,t){var n,r,i=0,o=t.length;for(e.deps=[];o>i;i++){r=t[i];for(n in 
r)"_jsvto"!==n&&r[n].length&&(e.deps=e.deps.concat(r[n]))}e.paths=r}function 
J(e,t,n){return[e.slice(0,-1),t.slice(0,-1),n.slice(0,-1)]}function K(e,t){return"\n     
"+(t?t+":{":"")+"args:["+e[0]+"]"+(e[1]||!t?",\n        props:{"+e[1]+"}":"")+(e[2]?",\n        
ctx:{"+e[2]+"}":"")}function O(e,t,n){function r(r,m,w,x,b,_,y,k,j,C,A,T,V,R,M,$,N,E,F,S){function 
q(e,n,r,s,a,d,p,c){var f="."===r;if(r&&(b=b.slice(n.length),/^\.?constructor$
 
/.test(c||b)&&I(e),f||(e=(s?'view.hlp("'+s+'")':a?"view":"data")+(c?(d?"."+d:s?"":a?"":"."+r)+(p||""):(c=s?"":a?d||"":r,"")),e+=c?"."+c:"",e=n+("view.data"===e.slice(0,9)?e.slice(5):e)),u)){if(O="linkTo"===i?o=t._jsvto=t._jsvto||[]:l.bd,B=f&&O[O.length-1]){if(B._jsv){for(;B.sb;)B=B.sb;B.bnd&&(b="^"+b.slice(1)),B.sb=b,B.bnd=B.bnd||"^"===b.charAt(0)}}else
 O.push(b);h[g]=F+(f?1:0)}return e}x=u&&x,x&&!k&&(b=x+b),_=_||"",w=w||m||T,b=b||j,C=C||N||"";var 
J,K,O,B,L,Q=")";if("["===C&&(C="[j._sq(",Q=")]"),!y||d||a){if(u&&$&&!d&&!a&&(!i||s||o)&&(J=h[g-1],S.length-1>F-(J||0))){if(J=S.slice(J,F+r.length),K!==!0)if(O=o||p[g-1].bd,B=O[O.length-1],B&&B.prm){for(;B.sb&&B.sb.prm;)B=B.sb;L=B.sb={path:B.sb,bnd:B.bnd}}else
 O.push(L={path:O.pop()});$=ce+":"+J+" 
onerror=''"+fe,K=f[$],K||(f[$]=!0,f[$]=K=U($,n,!0)),K!==!0&&L&&(L._jsv=K,L.prm=l.bd,L.bnd=L.bnd||L.path&&L.path.indexOf("^")>=0)}return
 d?(d=!V,d?r:T+'"'):a?(a=!R,a?r:T+'"'):(w?(h[g]=F++,l=p[++g]={bd:[]},w):"")+(E?g?"":(c=S.slice(c,F),(i?(
 
i=s=o=!1,"\b"):"\b,")+c+(c=F+r.length,u&&t.push(l.bd=[]),"\b")):k?(g&&I(e),u&&t.pop(),i=b,s=x,c=F+r.length,x&&(u=l.bd=t[i]=[]),b+":"):b?b.split("^").join(".").replace(xe,q)+(C?(l=p[++g]={bd:[]},v[g]=Q,C):_):_?_:M?(M=v[g]||M,v[g]=!1,l=p[--g],M+(C?(l=p[++g],v[g]=Q,C):"")):A?(v[g]||I(e),","):m?"":(d=V,a=R,'"'))}I(e)}var
 i,o,s,a,d,u=t&&t[0],l={bd:u},p={0:l},c=0,f=n?n.links:u&&(u.links=u.links||{}),g=0,v={},h={},m=(e+(n?" 
":"")).replace(be,r);return!g&&m||I(e)}function B(e,t,n){var 
r,i,o,s,a,d,u,l,p,c,f,g,v,h,m,w,x,b,_,y,k,j,C,A,T,R,M,$,N,E,F=0,S=ue.useViews||t.useViews||t.tags||t.templates||t.helpers||t.converters,U="",J={},O=e.length;for(""+t===t?(b=n?'data-link="'+t.replace(_e,"
 
").slice(1,-1)+'"':t,t=0):(b=t.tmplName||"unnamed",t.allowCode&&(J.allowCode=!0),t.debug&&(J.debug=!0),f=t.bnds,x=t.tmpls),r=0;O>r;r++)if(i=e[r],""+i===i)U+='\n+"'+i+'"';else
 
if(o=i[0],"*"===o)U+=";\n"+i[1]+"\nret=ret";else{if(s=i[1],k=!n&&i[2],a=K(i[3],"params")+"},"+K(v=i[4]),$=i[5],E=i[6],j=i[8]&&i[
 
8].replace(ye,"$1"),(T="else"===o)?g&&g.push(i[7]):(F=0,f&&(g=i[7])&&(g=[g],F=f.push(1))),S=S||v[1]||v[2]||g||/view.(?!index)/.test(v[0]),(R=":"===o)?s&&(o=s===Ee?">":s+o):(k&&(_=V(j,J),_.tmplName=b+"/"+o,_.useViews=_.useViews||S,B(k,_),S=_.useViews,x.push(_)),T||(y=o,S=S||o&&(!se[o]||!se[o].flow),A=U,U=""),C=e[r+1],C=C&&"else"===C[0]),N=$?";\ntry{\nret+=":"\n+",h="",m="",R&&(g||E||s&&s!==Ee)){if(M=new
 Function("data,view,j,u"," // "+b+" "+F+" "+o+"\nreturn {"+a+"};"),M._er=$,M._tag=o,n)return 
M;q(M,g),w='c("'+s+'",view,',c=!0,h=w+F+",",m=")"}if(U+=R?(n?($?"try{\n":"")+"return ":N)+(c?(c=void 
0,S=p=!0,w+(g?(f[F-1]=M,F):"{"+a+"}")+")"):">"===o?(u=!0,"h("+v[0]+")"):(l=!0,"((v="+v[0]+")!=null?v:"+(n?"null)":'"")'))):(d=!0,"\n{view:view,tmpl:"+(k?x.length:"0")+","+a+"},"),y&&!C){if(U="["+U.slice(0,-1)+"]",w='t("'+y+'",view,this,',n||g){if(U=new
 Function("data,view,j,u"," // "+b+" "+F+" "+y+"\nreturn "+U+";"),U._er=$,U._tag=y,g&&q(f[F-1]=U,g),n)return 
U;h=w+F+",undefined,",m=")"}
 U=A+N+w+(F||U)+")",g=0,y=0}$&&(S=!0,U+=";\n}catch(e){ret"+(n?"urn 
":"+=")+h+"j._err(e,view,"+$+")"+m+";}"+(n?"":"ret=ret"))}U="// "+b+"\nvar 
v"+(d?",t=j._tag":"")+(p?",c=j._cnvt":"")+(u?",h=j._html":"")+(n?";\n":',ret=""\n')+(J.debug?"debugger;":"")+U+(n?"\n":";\nreturn
 ret;"),de.debugMode!==!1&&(U="try {\n"+U+"\n}catch(e){\nreturn j._err(e, view);\n}");try{U=new 
Function("data,view,j,u",U)}catch(L){I("Compiled template code:\n\n"+U+'\n: "'+L.message+'"')}return 
t&&(t.fn=U,t.useViews=!!S),U}function L(e,t){return e&&e!==t?t?u(u({},t),e):e:t&&u({},t)}function Q(e){return 
Ne[e]||(Ne[e]="&#"+e.charCodeAt(0)+";")}function H(e){var t,n,r=[];if(typeof e===Fe)for(t in 
e)n=e[t],t===ee||te(n)||r.push({key:t,prop:n});return r}function P(e,n,r){var 
i=this.jquery&&(this[0]||S('Unknown template: "'+this.selector+'"')),o=i.getAttribute(Se);return 
N.call(o?t.data(i)[Ie]:re(i),e,n,r)}function D(e){return void 0!=e?Ve.test(e)&&(""+e).replace(Me,Q)||e:""}var 
Z=t===!1;t=t&&t.fn?t:e.jQuery;var 
 
z,G,W,X,Y,ee,te,ne,re,ie,oe,se,ae,de,ue,le,pe,ce,fe,ge,ve,he,me,we="v0.9.78",xe=/^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,be=/(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*[.^]|\s*$|[^([])|[)\]])([([]?))|(\s+)/g,_e=/[
 
\t]*(\r\n|\n|\r)/g,ye=/\\(['"])/g,ke=/['"\\]/g,je=/(?:\x08|^)(onerror:)?(?:(~?)(([\w$_\.]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi,Ce=/^if\s/,Ae=/<(\w+)[>\s]/,Te=/[\x00`><"'&=]/g,Ve=/[\x00`><\"'&=]/,Re=/^on[A-Z]|^convert(Back)?$/,Me=Te,$e=0,Ne={"&":"&amp;","<":"&lt;",">":"&gt;","\x00":"&#0;","'":"&#39;",'"':"&#34;","`":"&#96;","=":"&#61;"},Ee="html",Fe="object",Se="data-jsv-tmpl",Ie="jsvTmpl",Ue="For
 #index in nested block use 
#getIndex().",qe={},Je=e.jsrender,Ke=Je&&t&&!t.render,Oe={template:{compile:j},tag:{compile:y},viewModel:{compile:T},helper:{}
 
,converter:{}};if(Y={jsviews:we,sub:{View:b,Err:d,tmplFn:U,parse:O,extend:u,extendCtx:L,syntaxErr:I,onStore:{},addSetting:M,settings:{allowCode:!1},advSet:s,_ths:i,_tg:function(){},_cnvt:h,_tag:x,_er:S,_err:F,_html:D,_sq:function(e){return"constructor"===e&&I(""),e}},settings:{delimiters:l,advanced:function(e){return
 e?(u(ue,e),ae.advSet(),le):ue}},map:$},(d.prototype=new 
Error).constructor=d,c.depends=function(){return[this.get("item"),"index"]},f.depends="index",b.prototype={get:p,getIndex:f,getRsc:w,getTmpl:v,hlp:g,_is:"view"},ae=Y.sub,le=Y.settings,!(Je||t&&t.render)){for(z
 in 
Oe)R(z,Oe[z]);ie=Y.converters,oe=Y.helpers,se=Y.tags,ae._tg.prototype={baseApply:k,cvtArgs:m},X=ae.topView=new
 
b,t?(t.fn.render=P,ee=t.expando,t.observable&&(u(ae,t.views.sub),Y.map=t.views.map)):(t={},Z&&(e.jsrender=t),t.renderFile=t.__express=t.compile=function(){throw"Node.js:
 use npm jsrender, or jsrender-node.js"},t.isFunction=function(e){return"function"==typeof 
e},t.isArray=Array.isArray||fu
 nction(e){return"[object 
Array]"==={}.toString.call(e)},ae._jq=function(e){e!==t&&(u(e,t),t=e,t.fn.render=P,delete 
t.jsrender,ee=t.expando)},t.jsrender=we),de=ae.settings,de.allowCode=!1,te=t.isFunction,ne=t.isArray,t.render=qe,t.views=Y,t.templates=re=Y.templates;for(he
 in de)M(he);(le.debugMode=function(e){return void 0===e?de.debugMode:(de.debugMode=e,de.onError=e+""===e?new 
Function("","return '"+e+"';"):te(e)?e:void 
0,le)})(!1),ue=de.advanced={useViews:!1,_jsv:!1},se({"if":{render:function(e){var 
t=this,n=t.tagCtx,r=t.rendering.done||!e&&(arguments.length||!n.index)?"":(t.rendering.done=!0,t.selected=n.index,n.render(n.view,!0));return
 r},flow:!0},"for":{render:function(e){var t,n=!arguments.length,r=this,i=r.tagCtx,o="",s=0;return 
r.rendering.done||(t=n?i.view.data:e,void 
0!==t&&(o+=i.render(t,n),s+=ne(t)?t.length:1),(r.rendering.done=s)&&(r.selected=i.index)),o},flow:!0},props:{baseTag:"for",dataMap:$(H),flow:!0},include:{flow:!0},"*":{render:o,flow:!0},":*":{render:o
 ,flow:!0},dbg:oe.dbg=ie.dbg=a}),ie({html:D,attr:D,url:function(e){return void 
0!=e?encodeURI(""+e):null===e?e:""}})}return 
de=ae.settings,le.delimiters("{{","}}","^"),Ke&&Je.views.sub._jq(t),t||Je},window);
+//# sourceMappingURL=jsrender.min.js.map
diff --git a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js 
b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
index eafe70d..a61b10e 100644
--- a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
@@ -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,
diff --git a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js 
b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
index 9a76ee6..d8c3bf7 100644
--- a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
+++ b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
@@ -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;
-
-  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
-
-          for (var i in prjInfo.builds){
-            var build = prjInfo.builds[i];
-
-            if (build.outcomeText === "In Progress" ||
-               $(".progress .bar").length > 0){
-              /* Update the build progress */
-              var percentDone;
-
-              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++;
-              }
-
-              $("#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) + "%");
-
-              allPercentDone += percentDone;
-            }
-          }
+  // returns the cached version of this build, or {} is there isn't a cached one
+  function getCached(build) {
+    return buildData[build.id] || {};
+  }
+
+  // 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');
+  }
 
-          if (allPercentDone === (100 * prjInfo.builds.length) && !inProgress)
+  // returns true if the state changed
+  function stateChanged(build) {
+    var cached = getCached(build);
+    return (cached.state !== build.state);
+  }
+
+  // returns true if the complete_percentage changed
+  function progressChanged(build) {
+    var cached = getCached(build);
+    return (cached.tasks_complete_percentage !== build.tasks_complete_percentage);
+  }
+
+  function refreshMostRecentBuilds(){
+    libtoaster.getMostRecentBuilds(
+      libtoaster.ctx.mostRecentBuildsUrl,
+
+      // success callback
+      function (data) {
+        var build;
+        var tmpl;
+        var container;
+        var selector;
+        var colourClass;
+        var elements;
+
+        // 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';
+            }
 
-          /* Our progress bar is not still showing so shutdown the polling. */
-          if ($(".progress .bar").length === 0)
-            window.clearInterval(progressTimer);
+            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 + '%');
+          }
 
-      });
-    }, 1500);
+          buildData[build.id] = build;
+        }
+      },
+
+      // fail callback
+      function (data) {
+        console.error(data);
+      }
+    );
   }
-}
 
+  window.setInterval(refreshMostRecentBuilds, 1000);
+  refreshMostRecentBuilds();
+}
diff --git a/bitbake/lib/toaster/toastergui/templates/base.html 
b/bitbake/lib/toaster/toastergui/templates/base.html
index 8a9f690..58491eb 100644
--- a/bitbake/lib/toaster/toastergui/templates/base.html
+++ b/bitbake/lib/toaster/toastergui/templates/base.html
@@ -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,
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html 
b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index b164269..302b4b0 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -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>
+        </div>
+      </div>
+    {% endfor %}
+  </div>
+{% 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 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>
+  <%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>
 
-            <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>
+<!-- queued build -->
+<script id="queued-build-template" type="text/x-jsrender">
+  <div class="col-md-5">
+    Build queued
+  </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 %}
+  <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>
 
-          {% if build.outcome == build.IN_PROGRESS %}
-            <div class="col-md-4" style="display:none" id="cancelling-msg-{{build.buildrequest.pk}}">
-              Cancelling the build ...
-            </div>
+<!-- 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 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">
+    <!-- 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>
 
-            <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 #}
+<!-- cancelling build -->
+<script id="cancelling-build-template" type="text/x-jsrender">
+  <div class="col-md-9">
+    Cancelling the build ...
+  </div>
+</script>
 
-          {% if build.outcome == build.CANCELLED %}
-            <div class="col-md-6">
-              Build cancelled
-            </div>
+<!-- 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>
 
-            <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 %}
+  <!-- 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>
-{% endif %}
\ No newline at end of file
+
+  <!-- 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>
diff --git a/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html 
b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
index 1fe76a7..a5fed2d 100644
--- a/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
@@ -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 %}
diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py 
b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
index dc75f22..b170a16 100644
--- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
+++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
@@ -271,14 +271,6 @@ def get_dict_value(dictionary, key):
         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
     """
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index 1c0ccbb..9892d2a 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -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)),
 )


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]