diff options
11 files changed, 499 insertions, 281 deletions
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py index b30e405..ff26c7d 100644 --- a/bitbake/lib/toaster/orm/models.py +++ b/bitbake/lib/toaster/orm/models.py @@ -31,8 +31,8 @@ class Build(models.Model): (IN_PROGRESS, 'In Progress'), ) - search_allowed_fields = ['machine', - 'cooker_log_path'] + search_allowed_fields = ['machine', 'image_fstypes', + 'cooker_log_path', "target__target"] machine = models.CharField(max_length=100) image_fstypes = models.CharField(max_length=100) @@ -102,6 +102,8 @@ class Task(models.Model): (OUTCOME_NA, 'Not Available'), ) + search_allowed_fields = [ "recipe__name", "task_name" ] + build = models.ForeignKey(Build, related_name='task_build') order = models.IntegerField(null=True) task_executed = models.BooleanField(default=False) # True means Executed, False means Prebuilt @@ -217,6 +219,8 @@ class Layer_Version(models.Model): class Variable(models.Model): + search_allowed_fields = ['variable_name', 'variable_value', + 'variablehistory__file_name', "description"] build = models.ForeignKey(Build, related_name='variable_build') variable_name = models.CharField(max_length=100) variable_value = models.TextField(blank=True) @@ -225,7 +229,7 @@ class Variable(models.Model): description = models.TextField(blank=True) class VariableHistory(models.Model): - variable = models.ForeignKey(Variable) + variable = models.ForeignKey(Variable, related_name='vhistory') file_name = models.FilePathField(max_length=255) line_number = models.IntegerField(null=True) operation = models.CharField(max_length=16) diff --git a/bitbake/lib/toaster/toastergui/static/css/default.css b/bitbake/lib/toaster/toastergui/static/css/default.css index 844f6dc..53c5004 100644 --- a/bitbake/lib/toaster/toastergui/static/css/default.css +++ b/bitbake/lib/toaster/toastergui/static/css/default.css @@ -171,4 +171,7 @@ dd p {line-height:20px;} .tooltip { z-index: 2000 !important; } /* this makes tooltips work inside modal dialogs */ .tooltip code { background-color:transparent; color:#FFFFFF; font-weight:normal; border:none; font-size: 1em; } .manual { margin-top:11px;} -.heading-help { font-size:14px;}
\ No newline at end of file +.heading-help { font-size:14px;} + + +.no-results { margin: 10px 0 0; } diff --git a/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html b/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html index 00703fe..3e4b0cc 100644 --- a/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html +++ b/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html @@ -1,3 +1,4 @@ + </tbody> </table> <!-- Show pagination controls --> @@ -8,15 +9,15 @@ <ul class="pagination" style="display: block-inline"> {%if objects.has_previous %} - <li><a href="?page={{objects.previous_page_number}}&count={{request.GET.count}}">«</a></li> + <li><a href="javascript:reload_params({'page':{{objects.previous_page_number}}})">«</a></li> {%else%} <li class="disabled"><a href="#">«</a></li> {%endif%} {% for i in objects.page_range %} - <li{%if i == objects.number %} class="active" {%endif%}><a href="?page={{i}}&count={{request.GET.count}}">{{i}}</a></li> + <li{%if i == objects.number %} class="active" {%endif%}><a href="javascript:reload_params({'page':{{i}}})">{{i}}</a></li> {% endfor %} {%if objects.has_next%} - <li><a href="?page={{objects.next_page_number}}&count={{request.GET.count}}">»</a></li> + <li><a href="javascript:reload_params({'page':{{objects.next_page_number}}})">»</a></li> {%else%} <li class="disabled"><a href="#">»</a></li> {%endif%} @@ -58,3 +59,9 @@ }); }); </script> + +<!-- modal filter boxes --> + {% for tc in tablecols %}{% if tc.filter %}{% with f=tc.filter %} + {% include "filtersnippet.html" %} + {% endwith %}{% endif %} {% endfor %} +<!-- end modals --> diff --git a/bitbake/lib/toaster/toastergui/templates/basetable_top.html b/bitbake/lib/toaster/toastergui/templates/basetable_top.html index b9277b4..34e0cd7 100644 --- a/bitbake/lib/toaster/toastergui/templates/basetable_top.html +++ b/bitbake/lib/toaster/toastergui/templates/basetable_top.html @@ -21,46 +21,53 @@ <!-- control header --> <div class="navbar"> - <div class="navbar-inner"> - <form class="navbar-search input-append pull-left"> - <input class="input-xxlarge" type="text" placeholder="Search {{objectname}}" /> - <button class="btn" type="button">Search</button> - </form> - <div class="pull-right"> - - {% if tablecols %} - <div class="btn-group"> - <button class="btn dropdown-toggle" data-toggle="dropdown"> - Edit columns - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - - {% for i in tablecols %} - <li> - <label class="checkbox"> -<input type="checkbox" class="chbxtoggle" id="{{i.clclass}}" value="ct{{i.name}}" {% if i.clclass %}{% if not i.hidden %}checked="checked"{%endif%} onchange="showhideTableColumn($(this).attr('id'), $(this).is(':checked'))" {%else%} disabled{% endif %}/> {{i.name}} - </label> - </li> - {% endfor %} - </ul> - </div> - {% endif %} - - <div style="display:inline"> - <span class="divider-vertical"></span> - <span class="help-inline" style="padding-top:5px;">Show rows:</span> - <select style="margin-top:5px;margin-bottom:0px;" class="pagesize"> + <div class="navbar-inner"> + <form class="navbar-search input-append pull-left" > + <input class="input-xxlarge" name="search" type="text" placeholder="Search {{objectname}}" value="{{request.GET.search}}"/> + <input class="btn" type="submit" value="Search"/> + </form> + <div class="pull-right"> +{% if tablecols %} + <div class="btn-group"> + <button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns + <span class="caret"></span> + </button> + <ul class="dropdown-menu">{% for i in tablecols %} + <li> + <label class="checkbox"> + <input type="checkbox" class="chbxtoggle" {% if i.clclass %}id="{{i.clclass}}" value="ct{{i.name}}" {% if not i.hidden %}checked="checked"{%endif%} onchange="showhideTableColumn($(this).attr('id'), $(this).is(':checked'))" {%else%} checked disabled{% endif %}/> {{i.name}} + </label> + </li>{% endfor %} + </ul> + </div> +{% endif %} + <div style="display:inline"> + <span class="divider-vertical"></span> + <span class="help-inline" style="padding-top:5px;">Show rows:</span> + <select style="margin-top:5px;margin-bottom:0px;" class="pagesize"> {% with "2 5 10 25 50 100" as list%} - {% for i in list.split %}<option{%if i == request.GET.count %} selected{%endif%}>{{i}}</option> +{% for i in list.split %} <option{%if i == request.GET.count %} selected{%endif%}>{{i}}</option> {% endfor %} {% endwith %} - </select> - </div> - </div> - </div> - </div> + </select> + </div> + </div> + </div> <!-- navbar-inner --> +</div> <!-- the actual rows of the table --> <table class="table table-bordered table-hover tablesorter" id="otable"> + <thead> + <!-- Table header row; generated from "tablecols" entry in the context dict --> + <tr> + {% for tc in tablecols %}<th class="{{tc.dclass}} {{tc.clclass}}"> + {%if tc.qhelp%}<i class="icon-question-sign get-help" data-toggle="tooltip" title="{{tc.qhelp}}"></i>{%endif%} + <a href="javascript:reload_params({'orderby' : '{{tc.orderfield}}' })" style="font-weight:normal;">{{tc.name}}</a> + {%if tc.filter%}<div class="btn-group pull-right"> + <a href="#filter_{{tc.filter.class}}" role="button" class="btn btn-mini{%if request.GET.filter in tc.filter.options.values%} btn-primary{%endif%}" data-toggle="modal"> <i class="icon-filter filtered"></i> </a> + </div>{%endif%} + </th>{% endfor %} + </tr> + </thead> + <tbody> diff --git a/bitbake/lib/toaster/toastergui/templates/build.html b/bitbake/lib/toaster/toastergui/templates/build.html index 43b491d..eb7e03c 100644 --- a/bitbake/lib/toaster/toastergui/templates/build.html +++ b/bitbake/lib/toaster/toastergui/templates/build.html @@ -7,70 +7,77 @@ {% block pagecontent %} <div class="row-fluid"> -<div class="page-header" style="margin-top:40px;"> - <h1> - Recent Builds - </h1> -</div> -{% for build in mru %} -<div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}"> - <div class="row-fluid"> - <div class="lead span5"> - {%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%} - <a href="{%url 'builddashboard' build.pk%}"> - <span data-toggle="tooltip" {%if build.target_set.all.count > 1%}title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|naturaltime}})</span> - </a> - </div> -{%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} - <div class="span2 lead"> -{% if build.errors_no %} - <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> -{% endif %} - </div> - <div class="span2 lead"> -{% if build.warnings_no %} - <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a> -{% endif %} - </div> - <div class="lead pull-right"> - Build time: <a href="build-time.html">{{ build|timespent }}</a> - </div> -{%endif%}{%if build.outcome == build.IN_PROGRESS %} - <div class="span4"> - <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete"> - <div style="width: {{build.completeper}}%;" class="bar"></div> + {%if mru.count > 0%} + <div class="page-header" style="margin-top:40px;"> + <h1> + Recent Builds + </h1> + </div> + {% for build in mru %} + <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}"> + <div class="row-fluid"> + <div class="lead span5"> + {%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%} + <a href="{%url 'builddashboard' build.pk%}"> + <span data-toggle="tooltip" {%if build.target_set.all.count > 1%}title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|naturaltime}})</span> + </a> + </div> + {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} + <div class="span2 lead"> + {% if build.errors_no %} + <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> + {% endif %} + </div> + <div class="span2 lead"> + {% if build.warnings_no %} + <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a> + {% endif %} </div> + <div class="lead pull-right"> + Build time: <a href="build-time.html">{{ build|timespent }}</a> + </div> + {%endif%}{%if build.outcome == build.IN_PROGRESS %} + <div class="span4"> + <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete"> + <div style="width: {{build.completeper}}%;" class="bar"></div> + </div> + </div> + <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div> + {%endif%} </div> - <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div> -{%endif%} </div> -</div> -{% endfor %} + {% endfor %}{%endif%} - -<div class="page-header" style="margin-top:40px;"> - <h1> - All builds + <div class="page-header" style="margin-top:40px;"> + <h1> + {% if request.GET.filter or request.GET.search and objects.ocount > 0 %} + {{objects.ocount}} build{{objects.ocount|pluralize}} found + {%elif objects.ocount == 0%} + No builds + {%else%} + All builds + {%endif%} </h1> -</div> + </div> -{% include "basetable_top.html" %} + {% if objects.ocount == 0 %} + <div class="row-fluid"> + <div class="alert"> + <form class="no-results"> + <div class="input-append"> + <input class="input-xxlarge" type="text" placeholder="{{request.GET.search}}" /> + <input class="btn" type="submit" value="Search"/> + <button class="btn btn-link" onclick="javascript:reload_params({'search':'', 'filter':''})">Show all builds</button> + </div> + </form> + </div> + </div> - <tr> - <th class="outcome span2"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The outcome tells you if a build completed successfully or failed"></i> <a href="#" style="font-weight:normal;">Outcome</a> <div class="btn-group pull-right"> <a href="#outcome" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> </th> - <th class="target"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="This is the build target(s): one or more recipes or image recipes"></i> <a href="#" style="font-weight:normal;">Target</a> </th> - <th class="machine span3"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The machine is the hardware for which you are building"></i> <a href="#" style="font-weight:normal;">Machine</a> </th> - <th class="started_on"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The date and time you started the build"></i> <a href="#" style="font-weight:normal;">Started on</a> <div class="btn-group pull-right"> <a href="#started-on" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> </th> - <th class="completed_on"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The date and time the build finished"></i> <a href="#" class="sorted"> Completed on </a> <div class="btn-group pull-right"> <a href="#completed-on" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> <i class="icon-caret-down"></i> </th> - <th class="failed_tasks"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How many tasks failed during the build"></i> <a href="#" style="font-weight:normal;">Failed tasks</a> <div class="btn-group pull-right"> <a href="#failed-tasks" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> <!--div id="filtered" class="btn-group pull-right" title="<p>Showing only builds with failed tasks</p><p><a class='btn btn-mini btn-primary' href='#'>Show all builds</a></p>"> <a class="btn btn-mini btn-primary"> <i class="icon-filter"></i> </a> </div--> </th> - <th class="errors"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How many errors were encountered during the build (if any)"></i> <a href="#" style="font-weight:normal;">Errors</a> <div class="btn-group pull-right"> <a href="#errors" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> </th> - <th class="warnings"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How many warnigns were encountered during the build (if any)"></i> <a href="#" style="font-weight:normal;">Warnings</a> <div class="btn-group pull-right"> <a href="#warnings" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> <!--div id="filtered" class="btn-group pull-right" title="<p>Showing only builds without warnings</p><p><a class='btn btn-mini btn-primary' href='#'>Show all builds</a></p>"> <a class="btn btn-mini btn-primary"> <i class="icon-filter"></i> </a> </div--> </th> - <th class="time"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How long it took the build to finish"></i> <a href="#" style="font-weight:normal;">Time</a> </th> - <th class="log span4"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The location in disk of the build main log file"></i> <a href="#" style="font-weight:normal;">Log</a> </th> - <th class="output"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory"></i> <a href="#" style="font-weight:normal;">Output</a> </th> - </tr> +{% else %} +{% include "basetable_top.html" %} + <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> {% for build in objects %} <tr class="data"> <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td> @@ -78,11 +85,11 @@ <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on}}</a></td> <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on}}</a></td> - <td class="failed_tasks"></td> - <td class="errors">{% if build.errors_no %}<a class="error" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>{%endif%}</td> - <td class="warnings">{% if build.warnings_no %}<a class="warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td> + <td class="failed_tasks">{% query build.task_build outcome=4 order__gt=0 as exectask%}{% if exectask.count == 1 %}{{exectask.0.recipe.name}}.{{exectask.0.task_name}}{% elif exectask.count > 1%}{{exectask.count}}{%endif%}</td> + <td class="errors_no">{% if build.errors_no %}<a class="errors_no" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>{%endif%}</td> + <td class="warnings_no">{% if build.warnings_no %}<a class="warnings_no" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td> <td class="time"><a href="{% url "buildtime" build.id %}">{{build|timespent}}</a></td> - <td class="log">{{build.log}}</td> + <td class="log">{{build.cooker_log_path}}</td> <td class="output">{% if build.outcome == 0 %}{% for t in build.target_set.all %}{% if t.is_image %}<a href="{%url "builddashboard" build.id%}#images">{{build.image_fstypes}}</a>{% endif %}{% endfor %}{% endif %}</td> </tr> @@ -91,5 +98,7 @@ {% include "basetable_bottom.html" %} -</div> +{% endif %} +</div><!-- end row-fluid--> + {% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/configuration.html b/bitbake/lib/toaster/toastergui/templates/configuration.html index e390a95..467fbd0 100644 --- a/bitbake/lib/toaster/toastergui/templates/configuration.html +++ b/bitbake/lib/toaster/toastergui/templates/configuration.html @@ -4,25 +4,54 @@ {% endblock %} {% block buildinfomain %} +<!-- page title --> +<div class="row-fluid span10"> + <div class="page-header"> + <h1>Configuration</h1> + </div> +</div> -{% include "basetable_top.html" %} +<!-- configuration table --> +<div class="row-fluid pull-right span10" id="navTab"> +<ul class="nav nav-pills"> + <li class="active"><a href="#">Summary</a></li> + <li class=""><a href="{% url 'configvars' build.id %}">BitBake variables</a></li> +</ul> - <tr> - <th>Name</th> - <th>Description</th> - <th>Definition history</th> - <th>Value</th> - </tr> + <!-- summary --> + <div id="summary" class="tab-pane active"> + <h3>Build configuration</h3> + <dl class="dl-horizontal"> + <dt>BitBake version</dt><dd>1.19.1</dd> + <dt>Build system</dt><dd>x86_64-linux</dd> + <dt>Host distribution</dt><dd>Ubuntu-12.04</dd> + <dt>Target system</dt><dd>i586-poky-linux</dd> + <dt><i class="icon-question-sign get-help" data-toggle="tooltip" title="Specifies the target device for which the image is built"></i> Machine</dt><dd>atom-pc</dd> + <dt><i class="icon-question-sign get-help" data-toggle="tooltip" title="The short name of the distribution"></i> Distro</dt><dd>poky</dd> + <dt>Distro version</dt><dd>1.4+snapshot-20130718</dd> + <dt>Tune features</dt><dd>m32 i586</dd> + <dt>Target(s)</dt><dd>core-image-sato</dd> + </dl> + <h3>Layers</h3> + <div class="span9" style="margin-left:0px;"> + <table class="table table-bordered table-hover"> + <thead> + <tr> + <th>Layer</th> + <th>Layer branch</th> + <th>Layer commit</th> + <th>Layer directory</th> + </tr> + </thead> + <tbody>{% for lv in build.layer_version_build.all %} + <tr> + <td>{{lv.layer.name}}<a href="{{lv.layer.layer_index_url}}" target="_blank"> <i class="icon-share get-info"></i></a></td><td>{{lv.branch}}</td><td class="layer_commit"><a data-content="{{lv.commit}}" title="" href="#" class="btn" data-original-title="">{{lv.commit|slice:":8"}}...</a></td><td>{{lv.layer.local_path}}</td> + </tr>{% endfor %} + </tbody> + </table> + </div> + </div> - {% for variable in objects %} - - <tr class="data"> - <td>{{variable.variable_name}}</td> - <td>{% if variable.description %}{{variable.description}}{% endif %}</td> - <td>{% for vh in variable.variablehistory_set.all %}{{vh.operation}} in {{vh.file_name}}:{{vh.line_number}}<br/>{%endfor%}</td> - <td>{{variable.variable_value}}</td> - {% endfor %} - -{% include "basetable_bottom.html" %} +</div> {% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/configvars.html b/bitbake/lib/toaster/toastergui/templates/configvars.html new file mode 100644 index 0000000..8ce04b8 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/configvars.html @@ -0,0 +1,40 @@ +{% extends "basebuildpage.html" %} +{% block localbreadcrumb %} +<li>Configuration</li> +{% endblock %} + +{% block buildinfomain %} +<!-- page title --> +<div class="row-fluid span10"> + <div class="page-header"> + <h1>Configuration</h1> + </div> +</div> + +<!-- configuration table --> +<div class="row-fluid pull-right span10" id="navTab"> +<ul class="nav nav-pills"> + <li class=""><a href="{% url 'configuration' build.id %}">Summary</a></li> + <li class="active"><a href="#" >BitBake variables</a></li> +</ul> + + + <!-- variables --> + <div id="variables" class="tab-pane"> +{% include "basetable_top.html" %} + +{% for variable in objects %} + <tr class="data"> + <td class="variable">{{variable.variable_name}}</td> + <td class="variable_value">{{variable.variable_value}}</td> + <td class="file">{% for vh in variable.variablehistory_set.all %}{{vh.operation}} in {{vh.file_name}}:{{vh.line_number}}<br/>{%endfor%}</td> + <td class="description">{% if variable.description %}{{variable.description}}{% endif %}</td> + </tr> +{% endfor %} + +{% include "basetable_bottom.html" %} + + </div> <!-- endvariables --> + +</div> +{% endblock %} diff --git a/bitbake/lib/toaster/toastergui/templates/filtersnippet.html b/bitbake/lib/toaster/toastergui/templates/filtersnippet.html new file mode 100644 index 0000000..26ff675 --- /dev/null +++ b/bitbake/lib/toaster/toastergui/templates/filtersnippet.html @@ -0,0 +1,19 @@ + + <!-- '{{f.class}}' filter --> + <form id="filter_{{f.class}}" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true"> + <input type="hidden" name="search" value="{{request.GET.search}}"/> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button> + <h3>Filter builds by {{tc.name}}</h3> + </div> + <div class="modal-body"> + <label>{{f.label}}</label> + <select name="filter"> + <option value="">No Filter</option>{% for key, value in f.options.items %} + <option {%if request.GET.filter == value %}selected="" {%endif%}value="{{value}}">{{key}}</option>{% endfor %} + </select> + </div> + <div class="modal-footer"> + <button type="submit" class="btn btn-primary disabled">Apply</button> + </div> + </form> diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py index 1455026..15a1757 100644 --- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py +++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py @@ -16,8 +16,9 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from datetime import datetime +from datetime import datetime, timedelta from django import template +from django.utils import timezone register = template.Library() @@ -42,8 +43,14 @@ def query(qs, **kwargs): @register.filter def divide(value, arg): + if int(arg) == 0: + return -1 return int(value) / int(arg) @register.filter def multiply(value, arg): return int(value) * int(arg) + +@register.assignment_tag +def datecompute(delta, start = timezone.now()): + return start + timedelta(delta) diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py index f531eb0..5855783 100644 --- a/bitbake/lib/toaster/toastergui/urls.py +++ b/bitbake/lib/toaster/toastergui/urls.py @@ -39,6 +39,7 @@ urlpatterns = patterns('toastergui.views', url(r'^build/(?P<build_id>\d+)/target/(?P<target_id>\d+)/packages$', 'tpackage', name='targetpackages'), url(r'^build/(?P<build_id>\d+)/configuration$', 'configuration', name='configuration'), + url(r'^build/(?P<build_id>\d+)/configvars$', 'configvars', name='configvars'), url(r'^build/(?P<build_id>\d+)/buildtime$', 'buildtime', name='buildtime'), url(r'^build/(?P<build_id>\d+)/cpuusage$', 'cpuusage', name='cpuusage'), url(r'^build/(?P<build_id>\d+)/diskio$', 'diskio', name='diskio'), diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py index 7d4d710..09da9c2 100644 --- a/bitbake/lib/toaster/toastergui/views.py +++ b/bitbake/lib/toaster/toastergui/views.py @@ -25,7 +25,10 @@ from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File from orm.models import Target_Installed_Package from django.views.decorators.cache import cache_control from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger - +from django.http import HttpResponseBadRequest +from django.utils import timezone +from datetime import timedelta +from django.utils import formats def _build_page_range(paginator, index = 1): try: @@ -72,6 +75,109 @@ def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs): return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs) +FIELD_SEPARATOR = ":" +VALUE_SEPARATOR = ";" +DESCENDING = "-" + +def __get_q_for_val(name, value): + if "OR" in value: + return reduce(operator.or_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("OR") ])) + if "AND" in value: + return reduce(operator.and_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("AND") ])) + if value.startswith("NOT"): + kwargs = { name : value.strip("NOT") } + return ~Q(**kwargs) + else: + kwargs = { name : value } + return Q(**kwargs) + +def _get_filtering_query(filter_string): + + search_terms = filter_string.split(FIELD_SEPARATOR) + keys = search_terms[0].split(VALUE_SEPARATOR) + values = search_terms[1].split(VALUE_SEPARATOR) + + querydict = dict(zip(keys, values)) + return reduce(lambda x, y: x & y, map(lambda x: __get_q_for_val(k, querydict[k]),[k for k in querydict])) + +def _get_toggle_order(request, orderkey): + return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey + +# we check that the input comes in a valid form that we can recognize +def _validate_input(input, model): + + invalid = None + + if input: + input_list = input.split(FIELD_SEPARATOR) + + # Check we have only one colon + if len(input_list) != 2: + invalid = "We have an invalid number of separators" + return None, invalid + + # Check we have an equal number of terms both sides of the colon + if len(input_list[0].split(VALUE_SEPARATOR)) != len(input_list[1].split(VALUE_SEPARATOR)): + invalid = "Not all arg names got values" + return None, invalid + str(input_list) + + # Check we are looking for a valid field + valid_fields = model._meta.get_all_field_names() + for field in input_list[0].split(VALUE_SEPARATOR): + if not reduce(lambda x, y: x or y, map(lambda x: field.startswith(x), [ x for x in valid_fields ])): + return None, (field, [ x for x in valid_fields ]) + + return input, invalid + +# uses search_allowed_fields in orm/models.py to create a search query +# for these fields with the supplied input text +def _get_search_results(search_term, queryset, model): + search_objects = [] + for st in search_term.split(" "): + q_map = map(lambda x: Q(**{x+'__icontains': st}), + model.search_allowed_fields) + + search_objects.append(reduce(operator.or_, q_map)) + search_object = reduce(operator.and_, search_objects) + queryset = queryset.filter(search_object) + + return queryset + + +# function to extract the search/filter/ordering parameters from the request +# it uses the request and the model to validate input for the filter and orderby values +def _search_tuple(request, model): + ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model) + if invalid: + raise BaseException("Invalid ordering " + str(invalid)) + + filter_string, invalid = _validate_input(request.GET.get('filter', ''), model) + if invalid: + raise BaseException("Invalid filter " + str(invalid)) + + search_term = request.GET.get('search', '') + return (filter_string, search_term, ordering_string) + + +# returns a lazy-evaluated queryset for a filter/search/order combination +def _get_queryset(model, filter_string, search_term, ordering_string): + if filter_string: + filter_query = _get_filtering_query(filter_string) + queryset = model.objects.filter(filter_query) + else: + queryset = model.objects.all() + + if search_term: + queryset = _get_search_results(search_term, queryset, model) + + if ordering_string and queryset: + column, order = ordering_string.split(':') + if order.lower() == DESCENDING: + queryset = queryset.order_by('-' + column) + else: + queryset = queryset.order_by(column) + + return queryset # shows the "all builds" page def builds(request): @@ -84,16 +190,24 @@ def builds(request): if retval: return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) - # retrieve the objects that will be displayed in the table - build_info = _build_page_range(Paginator(Build.objects.exclude(outcome = Build.IN_PROGRESS).order_by("-id"), request.GET.get('count', 10)),request.GET.get('page', 1)) + # boilerplate code that takes a request for an object type and returns a queryset + # for that object type. copypasta for all needed table searches + (filter_string, search_term, ordering_string) = _search_tuple(request, Build) + queryset = _get_queryset(Build, filter_string, search_term, ordering_string) + + # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display + build_info = _build_page_range(Paginator(queryset.exclude(outcome = Build.IN_PROGRESS), request.GET.get('count', 10)),request.GET.get('page', 1)) - # build view-specific information; this is rendered specifically in the builds page - build_mru = Build.objects.order_by("-started_on")[:3] + # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds) + build_mru = Build.objects.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).order_by("-started_on")[:3] for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]: tf = Task.objects.filter(build = b) b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count() - from django.utils import timezone - b.eta = timezone.now() + ((timezone.now() - b.started_on)*100/b.completeper) + b.eta = timezone.now() + if b.completeper > 0: + b.eta += ((timezone.now() - b.started_on)*100/b.completeper) + else: + b.eta = 0 # send the data to the template context = { @@ -101,19 +215,78 @@ def builds(request): 'mru' : build_mru, # TODO: common objects for all table views, adapt as needed 'objects' : build_info, + # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns 'tablecols' : [ - {'name': 'Target ', 'clclass': 'target',}, - {'name': 'Machine ', 'clclass': 'machine'}, - {'name': 'Completed on ', 'clclass': 'completed_on'}, - {'name': 'Failed tasks ', 'clclass': 'failed_tasks'}, - {'name': 'Errors ', 'clclass': 'errors_no'}, - {'name': 'Warnings', 'clclass': 'warnings_no'}, - {'name': 'Output ', 'clclass': 'output'}, - {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1}, - {'name': 'Time ', 'clclass': 'time', 'hidden' : 1}, - {'name': 'Output', 'clclass': 'output'}, - {'name': 'Log', 'clclass': 'log', 'hidden': 1}, - ]} + {'name': 'Outcome ', # column with a single filter + 'qhelp' : "The outcome tells you if a build completed successfully or failed", # the help button content + 'dclass' : "span2", # indication about column width; comes from the design + 'orderfield': _get_toggle_order(request, "outcome"), # adds ordering by the field value; default ascending unless clicked from ascending into descending + # filter field will set a filter on that column with the specs in the filter description + # the class field in the filter has no relation with clclass; the control different aspects of the UI + # still, it is recommended for the values to be identical for easy tracking in the generated HTML + 'filter' : {'class' : 'outcome', 'label': 'Show only', 'options' : { + 'Successful builds': 'outcome:' + str(Build.SUCCEEDED), # this is the field search expression + 'Failed builds': 'outcome:'+ str(Build.FAILED), + } + } + }, + {'name': 'Target ', # default column, disabled box, with just the name in the list + 'qhelp': "This is the build target(s): one or more recipes or image recipes", + 'orderfield': _get_toggle_order(request, "target__target"), + }, + {'name': 'Machine ', + 'qhelp': "The machine is the hardware for which you are building", + 'dclass': 'span3'}, # a slightly wider column + {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1, # this is an unchecked box, which hides the column + 'qhelp': "The date and time you started the build", + 'filter' : {'class' : 'started_on', 'label': 'Show only builds started', 'options' : { + 'Today' : 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"), + 'Yesterday' : 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), + 'Within one week' : 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), + }} + }, + {'name': 'Completed on ', + 'qhelp': "The date and time the build finished", + 'orderfield': _get_toggle_order(request, "completed_on"), + 'filter' : {'class' : 'completed_on', 'label': 'Show only builds completed', 'options' : { + 'Today' : 'completed_on__gte:'+timezone.now().strftime("%Y-%m-%d"), + 'Yesterday' : 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"), + 'Within one week' : 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"), + }} + }, + {'name': 'Failed tasks ', 'clclass': 'failed_tasks', # specifing a clclass will enable the checkbox + 'qhelp': "How many tasks failed during the build", + 'filter' : {'class' : 'failed_tasks', 'label': 'Show only ', 'options' : { + 'Builds with failed tasks' : 'task_build__outcome:4', + 'Builds without failed tasks' : 'task_build__outcome:NOT4', + }} + }, + {'name': 'Errors ', 'clclass': 'errors_no', + 'qhelp': "How many errors were encountered during the build (if any)", + 'orderfield': _get_toggle_order(request, "errors_no"), + 'filter' : {'class' : 'errors_no', 'label': 'Show only ', 'options' : { + 'Builds with errors' : 'errors_no__gte:1', + 'Builds without errors' : 'errors_no:0', + }} + }, + {'name': 'Warnings', 'clclass': 'warnings_no', + 'qhelp': "How many warnigns were encountered during the build (if any)", + 'orderfield': _get_toggle_order(request, "warnings_no"), + 'filter' : {'class' : 'warnings_no', 'label': 'Show only ', 'options' : { + 'Builds with warnings' : 'warnings_no__gte:1', + 'Builds without warnings' : 'warnings_no:0', + }} + }, + {'name': 'Time ', 'clclass': 'time', 'hidden' : 1, + 'qhelp': "How long it took the build to finish",}, + {'name': 'Log', + 'dclass': "span4", + 'qhelp': "The location in disk of the build main log file", + 'clclass': 'log', 'hidden': 1}, + {'name': 'Output', 'clclass': 'output', + 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory"}, + ] + } return render(request, template, context) @@ -191,8 +364,10 @@ def tasks(request, build_id): retval = _verify_parameters( request.GET, mandatory_parameters ) if retval: return _redirect_parameters( 'tasks', request.GET, mandatory_parameters, build_id = build_id) + (filter_string, search_term, ordering_string) = _search_tuple(request, Task) + queryset = _get_queryset(Task, filter_string, search_term, ordering_string) - tasks = _build_page_range(Paginator(Task.objects.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1)) + tasks = _build_page_range(Paginator(queryset.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1)) for t in tasks: if t.outcome == Task.OUTCOME_COVERED: @@ -208,8 +383,10 @@ def recipes(request, build_id): retval = _verify_parameters( request.GET, mandatory_parameters ) if retval: return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) + (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe) + queryset = _get_queryset(Recipe, filter_string, search_term, ordering_string) - recipes = _build_page_range(Paginator(Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)), request.GET.get('count', 100)),request.GET.get('page', 1)) + recipes = _build_page_range(Paginator(queryset.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)), request.GET.get('count', 100)),request.GET.get('page', 1)) context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes, } @@ -218,15 +395,63 @@ def recipes(request, build_id): def configuration(request, build_id): template = 'configuration.html' + context = {'build': Build.objects.filter(pk=build_id)[0]} + return render(request, template, context) + + +def configvars(request, build_id): + template = 'configvars.html' mandatory_parameters = { 'count': 100, 'page' : 1}; retval = _verify_parameters( request.GET, mandatory_parameters ) if retval: - return _redirect_parameters( 'configuration', request.GET, mandatory_parameters, build_id = build_id) + return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id) + + (filter_string, search_term, ordering_string) = _search_tuple(request, Variable) + queryset = _get_queryset(Variable, filter_string, search_term, ordering_string) + + variables = _build_page_range(Paginator(queryset.filter(build=build_id), request.GET.get('count', 50)), request.GET.get('page', 1)) + + context = { + 'build': Build.objects.filter(pk=build_id)[0], + 'objects' : variables, + # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns + 'tablecols' : [ + {'name': 'Variable ', + 'qhelp': "Base variable expanded name", + 'clclass' : 'variable', + 'dclass' : "span3", + 'orderfield': _get_toggle_order(request, "variable_name"), + }, + {'name': 'Value ', + 'qhelp': "The value assigned to the variable", + 'clclass': 'variable_value', + 'dclass': "span4", + 'orderfield': _get_toggle_order(request, "variable_value"), + }, + {'name': 'Configuration file(s) ', + 'qhelp': "The configuration file(s) that touched the variable value", + 'clclass': 'file', + 'dclass': "span6", + 'orderfield': _get_toggle_order(request, "variable_vhistory__file_name"), + 'filter' : { 'class': 'file', 'label' : 'Show only', 'options' : { + } + } + }, + {'name': 'Description ', + 'qhelp': "A brief explanation of a variable", + 'clclass': 'description', + 'dclass': "span5", + 'orderfield': _get_toggle_order(request, "description"), + 'filter' : { 'class' : 'description', 'label' : 'No', 'options' : { + } + }, + } + ] + } - variables = _build_page_range(Paginator(Variable.objects.filter(build=build_id), 50), request.GET.get('page', 1)) - context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : variables} return render(request, template, context) + def buildtime(request, build_id): template = "buildtime.html" if Build.objects.filter(pk=build_id).count() == 0 : @@ -263,8 +488,10 @@ def bpackage(request, build_id): retval = _verify_parameters( request.GET, mandatory_parameters ) if retval: return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id) + (filter_string, search_term, ordering_string) = _search_tuple(request, Package) + queryset = _get_queryset(Package, filter_string, search_term, ordering_string) - packages = _build_page_range(Paginator(Package.objects.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1)) + packages = _build_page_range(Paginator(queryset.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1)) context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages} return render(request, template, context) @@ -305,139 +532,4 @@ def layer_versions_recipes(request, layerversion_id): return render(request, template, context) -#### API - -import json -from django.core import serializers -from django.http import HttpResponse, HttpResponseBadRequest - - -def model_explorer(request, model_name): - - DESCENDING = 'desc' - response_data = {} - model_mapping = { - 'build': Build, - 'target': Target, - 'task': Task, - 'task_dependency': Task_Dependency, - 'package': Package, - 'layer': Layer, - 'layerversion': Layer_Version, - 'recipe': Recipe, - 'recipe_dependency': Recipe_Dependency, - 'package': Package, - 'package_dependency': Package_Dependency, - 'build_file': Package_File, - 'variable': Variable, - 'logmessage': LogMessage, - } - - if model_name not in model_mapping.keys(): - return HttpResponseBadRequest() - - model = model_mapping[model_name] - - try: - limit = int(request.GET.get('limit', 0)) - except ValueError: - limit = 0 - - try: - offset = int(request.GET.get('offset', 0)) - except ValueError: - offset = 0 - - ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), - model) - if invalid: - return HttpResponseBadRequest() - - filter_string, invalid = _validate_input(request.GET.get('filter', ''), - model) - if invalid: - return HttpResponseBadRequest() - - search_term = request.GET.get('search', '') - - if filter_string: - filter_terms = _get_filtering_terms(filter_string) - try: - queryset = model.objects.filter(**filter_terms) - except ValueError: - queryset = [] - else: - queryset = model.objects.all() - if search_term: - queryset = _get_search_results(search_term, queryset, model) - - if ordering_string and queryset: - column, order = ordering_string.split(':') - if order.lower() == DESCENDING: - queryset = queryset.order_by('-' + column) - else: - queryset = queryset.order_by(column) - - if offset and limit: - queryset = queryset[offset:(offset+limit)] - elif offset: - queryset = queryset[offset:] - elif limit: - queryset = queryset[:limit] - - if queryset: - response_data['count'] = queryset.count() - else: - response_data['count'] = 0 - response_data['list'] = serializers.serialize('json', queryset) -# response_data = serializers.serialize('json', queryset) - - return HttpResponse(json.dumps(response_data), - content_type='application/json') - -def _get_filtering_terms(filter_string): - - search_terms = filter_string.split(":") - keys = search_terms[0].split(',') - values = search_terms[1].split(',') - - return dict(zip(keys, values)) - -def _validate_input(input, model): - - invalid = 0 - - if input: - input_list = input.split(":") - - # Check we have only one colon - if len(input_list) != 2: - invalid = 1 - return None, invalid - - # Check we have an equal number of terms both sides of the colon - if len(input_list[0].split(',')) != len(input_list[1].split(',')): - invalid = 1 - return None, invalid - - # Check we are looking for a valid field - valid_fields = model._meta.get_all_field_names() - for field in input_list[0].split(','): - if field not in valid_fields: - invalid = 1 - return None, invalid - - return input, invalid - -def _get_search_results(search_term, queryset, model): - search_objects = [] - for st in search_term.split(" "): - q_map = map(lambda x: Q(**{x+'__icontains': st}), - model.search_allowed_fields) - - search_objects.append(reduce(operator.or_, q_map)) - search_object = reduce(operator.and_, search_objects) - queryset = queryset.filter(search_object) - - return queryset |