aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMilo Casagrande <milo@ubuntu.com>2014-04-25 15:28:52 +0200
committerMilo Casagrande <milo@ubuntu.com>2014-04-25 15:28:52 +0200
commitbf35309042f183604e992e6337a33381bef584f5 (patch)
tree96d00561f813aa11c71588c775edf59552a4e3d3
Initial commit.
-rw-r--r--README4
-rw-r--r--app/dashboard/__init__.py77
-rw-r--r--app/dashboard/default_settings.py27
-rw-r--r--app/dashboard/static/css/datatables.css173
-rw-r--r--app/dashboard/static/css/datatables.min.css1
-rw-r--r--app/dashboard/static/images/sort_asc.pngbin0 -> 1118 bytes
-rw-r--r--app/dashboard/static/images/sort_asc_disabled.pngbin0 -> 1050 bytes
-rw-r--r--app/dashboard/static/images/sort_both.pngbin0 -> 1136 bytes
-rw-r--r--app/dashboard/static/images/sort_desc.pngbin0 -> 1127 bytes
-rw-r--r--app/dashboard/static/images/sort_desc_disabled.pngbin0 -> 1045 bytes
-rw-r--r--app/dashboard/static/js/datatables.js251
-rw-r--r--app/dashboard/static/js/datatables.min.js1
-rw-r--r--app/dashboard/static/js/kernelci.js45
-rw-r--r--app/dashboard/templates/base.html42
-rw-r--r--app/dashboard/templates/build.html0
-rw-r--r--app/dashboard/templates/builds.html110
-rw-r--r--app/dashboard/templates/index.html142
-rw-r--r--app/dashboard/templates/info.html15
-rw-r--r--app/dashboard/templates/job-kernel.html104
-rw-r--r--app/dashboard/templates/job.html82
-rw-r--r--app/dashboard/templates/jobs.html131
-rw-r--r--app/dashboard/utils/__init__.py0
-rw-r--r--app/dashboard/utils/backend.py107
-rw-r--r--app/dashboard/views/__init__.py0
-rw-r--r--app/dashboard/views/about.py23
-rw-r--r--app/dashboard/views/build.py24
-rw-r--r--app/dashboard/views/index.py28
-rw-r--r--app/dashboard/views/job.py85
-rwxr-xr-xapp/server.py22
-rw-r--r--requirements.txt3
30 files changed, 1497 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..2fc87c6
--- /dev/null
+++ b/README
@@ -0,0 +1,4 @@
+Kernel CI Frontend
+==================
+
+Data visualization tool for the kernel-ci-backend.
diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py
new file mode 100644
index 0000000..852394d
--- /dev/null
+++ b/app/dashboard/__init__.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from flask import (
+ Flask,
+ request,
+)
+
+from dashboard.views.about import AboutView
+from dashboard.views.build import BuildsView
+from dashboard.views.index import IndexView
+from dashboard.views.job import (
+ JobsView,
+ JobView,
+ JobIdView,
+)
+from utils.backend import (
+ ajax_get_defconfigs,
+ ajax_get_jobs,
+)
+
+APP_ENVVAR = 'FLASK_SETTINGS'
+
+app = Flask('kernel-ci-frontend')
+
+app.root_path = os.path.abspath(os.path.dirname(__file__))
+
+app.config.from_object('dashboard.default_settings')
+if os.environ.get(APP_ENVVAR):
+ app.config.from_envvar(APP_ENVVAR)
+
+app.add_url_rule(
+ '/build/', view_func=BuildsView.as_view('builds'), methods=['GET'],
+)
+app.add_url_rule(
+ '/info/', view_func=AboutView.as_view('about'), methods=['GET'],
+)
+app.add_url_rule(
+ '/job/<string:job>/', view_func=JobView.as_view('job'), methods=['GET'],
+)
+app.add_url_rule('/job/', view_func=JobsView.as_view('jobs'), methods=['GET'])
+app.add_url_rule('/', view_func=IndexView.as_view('index'), methods=['GET'])
+
+app.add_url_rule(
+ '/job/<string:job>/kernel/<string:kernel>/',
+ view_func=JobIdView.as_view('job-id'),
+ methods=['GET'],
+)
+
+
+@app.route('/static/js/<path:path>')
+def static_proxy(path):
+ return app.send_static_file(os.path.join('js', path))
+
+
+@app.route('/_ajax/job')
+def ajax_jobs():
+ return ajax_get_jobs(request)
+
+
+@app.route('/_ajax/defconf')
+def ajax_defconfs():
+ return ajax_get_defconfigs(request)
diff --git a/app/dashboard/default_settings.py b/app/dashboard/default_settings.py
new file mode 100644
index 0000000..f9fb210
--- /dev/null
+++ b/app/dashboard/default_settings.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Following keys should be defined in an external file and passed as an
+# environment variable called FLASK_SETTINGS.
+LOGGER_NAME = 'kernel-ci-frontend'
+PREFERRED_URL_SCHEME = 'http'
+SESSION_COOKIE_NAME = 'linarokernelci'
+# Add the trailing slash!
+BACKEND_URL = 'http://192.168.0.127:8888/'
+BASE_URL = 'http://127.0.0.1:5000'
+BACKEND_TOKEN = 'foo'
+SECRET_KEY = 'bar'
+DEBUG = True
+TESTING = DEBUG
diff --git a/app/dashboard/static/css/datatables.css b/app/dashboard/static/css/datatables.css
new file mode 100644
index 0000000..6dfbaea
--- /dev/null
+++ b/app/dashboard/static/css/datatables.css
@@ -0,0 +1,173 @@
+
+div.dataTables_length label {
+ float: left;
+ text-align: left;
+}
+
+div.dataTables_length select {
+ width: 75px;
+}
+
+div.dataTables_filter label {
+ float: right;
+}
+
+div.dataTables_info {
+ padding-top: 8px;
+}
+
+div.dataTables_paginate {
+ float: right;
+ margin: 0;
+}
+
+table.table {
+ clear: both;
+ margin-bottom: 6px !important;
+ max-width: none !important;
+}
+
+table.table thead .sorting,
+table.table thead .sorting_asc,
+table.table thead .sorting_desc,
+table.table thead .sorting_asc_disabled,
+table.table thead .sorting_desc_disabled {
+ cursor: pointer;
+ *cursor: hand;
+}
+
+table.table thead .sorting { background: url('/static/images/sort_both.png') no-repeat center right; }
+table.table thead .sorting_asc { background: url('/static/images/sort_asc.png') no-repeat center right; }
+table.table thead .sorting_desc { background: url('/static/images/sort_desc.png') no-repeat center right; }
+
+table.table thead .sorting_asc_disabled { background: url('/static/images/sort_asc_disabled.png') no-repeat center right; }
+table.table thead .sorting_desc_disabled { background: url('/static/images/sort_desc_disabled.png') no-repeat center right; }
+
+table.dataTable th:active {
+ outline: none;
+}
+
+/* Scrolling */
+div.dataTables_scrollHead table {
+ margin-bottom: 0 !important;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+div.dataTables_scrollHead table thead tr:last-child th:first-child,
+div.dataTables_scrollHead table thead tr:last-child td:first-child {
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+div.dataTables_scrollBody table {
+ border-top: none;
+ margin-bottom: 0 !important;
+}
+
+div.dataTables_scrollBody tbody tr:first-child th,
+div.dataTables_scrollBody tbody tr:first-child td {
+ border-top: none;
+}
+
+div.dataTables_scrollFoot table {
+ border-top: none;
+}
+
+/*
+ * TableTools styles
+ */
+.table tbody tr.active td,
+.table tbody tr.active th {
+ background-color: #08C;
+ color: white;
+}
+
+.table tbody tr.active:hover td,
+.table tbody tr.active:hover th {
+ background-color: #0075b0 !important;
+}
+
+.table-striped tbody tr.active:nth-child(odd) td,
+.table-striped tbody tr.active:nth-child(odd) th {
+ background-color: #017ebc;
+}
+
+table.DTTT_selectable tbody tr {
+ cursor: pointer;
+ *cursor: hand;
+}
+
+div.DTTT .btn {
+ color: #333 !important;
+ font-size: 12px;
+}
+
+div.DTTT .btn:hover {
+ text-decoration: none !important;
+}
+
+
+ul.DTTT_dropdown.dropdown-menu a {
+ color: #333 !important; /* needed only when demo_page.css is included */
+}
+
+ul.DTTT_dropdown.dropdown-menu li:hover a {
+ background-color: #0088cc;
+ color: white !important;
+}
+
+/* TableTools information display */
+div.DTTT_print_info.modal {
+ height: 150px;
+ margin-top: -75px;
+ text-align: center;
+}
+
+div.DTTT_print_info h6 {
+ font-weight: normal;
+ font-size: 28px;
+ line-height: 28px;
+ margin: 1em;
+}
+
+div.DTTT_print_info p {
+ font-size: 14px;
+ line-height: 20px;
+}
+
+/*
+ * FixedColumns styles
+ */
+div.DTFC_LeftHeadWrapper table,
+div.DTFC_LeftFootWrapper table,
+table.DTFC_Cloned tr.even {
+ background-color: white;
+}
+
+div.DTFC_LeftHeadWrapper table {
+ margin-bottom: 0 !important;
+ border-top-right-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+div.DTFC_LeftHeadWrapper table thead tr:last-child th:first-child,
+div.DTFC_LeftHeadWrapper table thead tr:last-child td:first-child {
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+div.DTFC_LeftBodyWrapper table {
+ border-top: none;
+ margin-bottom: 0 !important;
+}
+
+div.DTFC_LeftBodyWrapper tbody tr:first-child th,
+div.DTFC_LeftBodyWrapper tbody tr:first-child td {
+ border-top: none;
+}
+
+div.DTFC_LeftFootWrapper table {
+ border-top: none;
+}
diff --git a/app/dashboard/static/css/datatables.min.css b/app/dashboard/static/css/datatables.min.css
new file mode 100644
index 0000000..a12d552
--- /dev/null
+++ b/app/dashboard/static/css/datatables.min.css
@@ -0,0 +1 @@
+div.dataTables_length label{float:left;text-align:left}div.dataTables_length select{width:75px}div.dataTables_filter label{float:right}div.dataTables_info{padding-top:8px}div.dataTables_paginate{float:right;margin:0}table.table{clear:both;margin-bottom:6px !important;max-width:none !important}table.table thead .sorting,table.table thead .sorting_asc,table.table thead .sorting_desc,table.table thead .sorting_asc_disabled,table.table thead .sorting_desc_disabled{cursor:pointer;*cursor:hand}table.table thead .sorting{background:url('/static/images/sort_both.png') no-repeat center right}table.table thead .sorting_asc{background:url('/static/images/sort_asc.png') no-repeat center right}table.table thead .sorting_desc{background:url('/static/images/sort_desc.png') no-repeat center right}table.table thead .sorting_asc_disabled{background:url('/static/images/sort_asc_disabled.png') no-repeat center right}table.table thead .sorting_desc_disabled{background:url('/static/images/sort_desc_disabled.png') no-repeat center right}table.dataTable th:active{outline:0}div.dataTables_scrollHead table{margin-bottom:0 !important;border-bottom-left-radius:0;border-bottom-right-radius:0}div.dataTables_scrollHead table thead tr:last-child th:first-child,div.dataTables_scrollHead table thead tr:last-child td:first-child{border-bottom-left-radius:0 !important;border-bottom-right-radius:0 !important}div.dataTables_scrollBody table{border-top:0;margin-bottom:0 !important}div.dataTables_scrollBody tbody tr:first-child th,div.dataTables_scrollBody tbody tr:first-child td{border-top:0}div.dataTables_scrollFoot table{border-top:0}.table tbody tr.active td,.table tbody tr.active th{background-color:#08C;color:white}.table tbody tr.active:hover td,.table tbody tr.active:hover th{background-color:#0075b0 !important}.table-striped tbody tr.active:nth-child(odd) td,.table-striped tbody tr.active:nth-child(odd) th{background-color:#017ebc}table.DTTT_selectable tbody tr{cursor:pointer;*cursor:hand}div.DTTT .btn{color:#333 !important;font-size:12px}div.DTTT .btn:hover{text-decoration:none !important}ul.DTTT_dropdown.dropdown-menu a{color:#333 !important}ul.DTTT_dropdown.dropdown-menu li:hover a{background-color:#08c;color:white !important}div.DTTT_print_info.modal{height:150px;margin-top:-75px;text-align:center}div.DTTT_print_info h6{font-weight:normal;font-size:28px;line-height:28px;margin:1em}div.DTTT_print_info p{font-size:14px;line-height:20px}div.DTFC_LeftHeadWrapper table,div.DTFC_LeftFootWrapper table,table.DTFC_Cloned tr.even{background-color:white}div.DTFC_LeftHeadWrapper table{margin-bottom:0 !important;border-top-right-radius:0 !important;border-bottom-left-radius:0 !important;border-bottom-right-radius:0 !important}div.DTFC_LeftHeadWrapper table thead tr:last-child th:first-child,div.DTFC_LeftHeadWrapper table thead tr:last-child td:first-child{border-bottom-left-radius:0 !important;border-bottom-right-radius:0 !important}div.DTFC_LeftBodyWrapper table{border-top:0;margin-bottom:0 !important}div.DTFC_LeftBodyWrapper tbody tr:first-child th,div.DTFC_LeftBodyWrapper tbody tr:first-child td{border-top:0}div.DTFC_LeftFootWrapper table{border-top:0} \ No newline at end of file
diff --git a/app/dashboard/static/images/sort_asc.png b/app/dashboard/static/images/sort_asc.png
new file mode 100644
index 0000000..a88d797
--- /dev/null
+++ b/app/dashboard/static/images/sort_asc.png
Binary files differ
diff --git a/app/dashboard/static/images/sort_asc_disabled.png b/app/dashboard/static/images/sort_asc_disabled.png
new file mode 100644
index 0000000..4e144cf
--- /dev/null
+++ b/app/dashboard/static/images/sort_asc_disabled.png
Binary files differ
diff --git a/app/dashboard/static/images/sort_both.png b/app/dashboard/static/images/sort_both.png
new file mode 100644
index 0000000..1867040
--- /dev/null
+++ b/app/dashboard/static/images/sort_both.png
Binary files differ
diff --git a/app/dashboard/static/images/sort_desc.png b/app/dashboard/static/images/sort_desc.png
new file mode 100644
index 0000000..def071e
--- /dev/null
+++ b/app/dashboard/static/images/sort_desc.png
Binary files differ
diff --git a/app/dashboard/static/images/sort_desc_disabled.png b/app/dashboard/static/images/sort_desc_disabled.png
new file mode 100644
index 0000000..7824973
--- /dev/null
+++ b/app/dashboard/static/images/sort_desc_disabled.png
Binary files differ
diff --git a/app/dashboard/static/js/datatables.js b/app/dashboard/static/js/datatables.js
new file mode 100644
index 0000000..5e9c529
--- /dev/null
+++ b/app/dashboard/static/js/datatables.js
@@ -0,0 +1,251 @@
+/* Set the defaults for DataTables initialisation */
+$.extend(true, $.fn.dataTable.defaults, {
+ 'sDom':
+ "<'row'<'col-xs-6'l>r>" +
+ 't' +
+ "<'row'<'col-xs-6'i><'col-xs-6'p>>",
+});
+
+/* Default class modification */
+$.extend($.fn.dataTableExt.oStdClasses, {
+ 'sWrapper': 'dataTables_wrapper form-inline',
+ 'sFilterInput': 'form-control input-sm',
+ 'sLengthSelect': 'form-control input-sm'
+});
+
+// In 1.10 we use the pagination renderers to draw the Bootstrap paging,
+// rather than custom plug-in
+if ($.fn.dataTable.Api) {
+ $.fn.dataTable.defaults.renderer = 'bootstrap';
+ $.fn.dataTable.ext.renderer.pageButton.bootstrap =
+ function(settings, host, idx, buttons, page, pages) {
+ var api = new $.fn.dataTable.Api(settings),
+ classes = settings.oClasses,
+ lang = settings.oLanguage.oPaginate,
+ btnDisplay,
+ btnClass;
+
+ var attach = function(container, buttons) {
+ var i, ien, node, button;
+ var clickHandler = function(e) {
+ e.preventDefault();
+ if (e.data.action !== 'ellipsis') {
+ api.page(e.data.action).draw(false);
+ }
+ };
+
+ for (i = 0, ien = buttons.length; i < ien; i++) {
+ button = buttons[i];
+
+ if ($.isArray(button)) {
+ attach(container, button);
+ } else {
+ btnDisplay = '';
+ btnClass = '';
+
+ switch (button) {
+ case 'ellipsis':
+ btnDisplay = '&hellip;';
+ btnClass = 'disabled';
+ break;
+
+ case 'first':
+ btnDisplay = lang.sFirst;
+ btnClass = button + (page > 0 ?
+ '' : ' disabled');
+ break;
+
+ case 'previous':
+ btnDisplay = lang.sPrevious;
+ btnClass = button + (page > 0 ?
+ '' : ' disabled');
+ break;
+
+ case 'next':
+ btnDisplay = lang.sNext;
+ btnClass = button + (page < pages - 1 ?
+ '' : ' disabled');
+ break;
+
+ case 'last':
+ btnDisplay = lang.sLast;
+ btnClass = button + (page < pages - 1 ?
+ '' : ' disabled');
+ break;
+
+ default:
+ btnDisplay = button + 1;
+ btnClass = page === button ?
+ 'active' : '';
+ break;
+ }
+
+ if (btnDisplay) {
+ node = $('<li>', {
+ 'class': classes.sPageButton + ' ' + btnClass,
+ 'aria-controls': settings.sTableId,
+ 'tabindex': settings.iTabIndex,
+ 'id': idx === 0 && typeof button === 'string' ?
+ settings.sTableId + '_' + button :
+ null
+ })
+ .append($('<a>', {
+ 'href': '#'
+ })
+ .html(btnDisplay)
+ )
+ .appendTo(container);
+
+ settings.oApi._fnBindAction(
+ node, {action: button}, clickHandler
+ );
+ }
+ }
+ }
+ };
+
+ attach(
+ $(host).empty().html('<ul class="pagination"/>').children('ul'),
+ buttons
+ );
+ };
+}
+else {
+ // Integration for 1.9-
+ $.fn.dataTable.defaults.sPaginationType = 'bootstrap';
+
+ /* API method to get paging information */
+ $.fn.dataTableExt.oApi.fnPagingInfo = function(oSettings) {
+ return {
+ 'iStart': oSettings._iDisplayStart,
+ 'iEnd': oSettings.fnDisplayEnd(),
+ 'iLength': oSettings._iDisplayLength,
+ 'iTotal': oSettings.fnRecordsTotal(),
+ 'iFilteredTotal': oSettings.fnRecordsDisplay(),
+ 'iPage': oSettings._iDisplayLength === -1 ?
+ 0 : Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength),
+ 'iTotalPages': oSettings._iDisplayLength === -1 ?
+ 0 : Math.ceil(oSettings.fnRecordsDisplay() / oSettings._iDisplayLength)
+ };
+ };
+
+ /* Bootstrap style pagination control */
+ $.extend($.fn.dataTableExt.oPagination, {
+ 'bootstrap': {
+ 'fnInit': function(oSettings, nPaging, fnDraw) {
+ var oLang = oSettings.oLanguage.oPaginate,
+ fnClickHandler = function(e) {
+ e.preventDefault();
+ if (oSettings.oApi._fnPageChange(oSettings, e.data.action)) {
+ fnDraw(oSettings);
+ }
+ };
+
+ $(nPaging).append(
+ '<ul class="pagination">' +
+ '<li class="prev disabled"><a href="#">' + oLang.sPrevious + '</a></li>' +
+ '<li class="next disabled"><a href="#">' + oLang.sNext + '</a></li>' +
+ '</ul>'
+ );
+
+ var els = $('a', nPaging);
+ $(els[0]).bind('click.DT', { action: 'previous' }, fnClickHandler);
+ $(els[1]).bind('click.DT', { action: 'next' }, fnClickHandler);
+ },
+
+ 'fnUpdate': function(oSettings, fnDraw) {
+ var iListLength = 5,
+ oPaging = oSettings.oInstance.fnPagingInfo(),
+ an = oSettings.aanFeatures.p,
+ i,
+ ien,
+ j,
+ sClass,
+ iStart,
+ iEnd,
+ iHalf = Math.floor(iListLength / 2);
+
+ if (oPaging.iTotalPages < iListLength) {
+ iStart = 1;
+ iEnd = oPaging.iTotalPages;
+ } else if (oPaging.iPage <= iHalf) {
+ iStart = 1;
+ iEnd = iListLength;
+ } else if (oPaging.iPage >= (oPaging.iTotalPages - iHalf)) {
+ iStart = oPaging.iTotalPages - iListLength + 1;
+ iEnd = oPaging.iTotalPages;
+ } else {
+ iStart = oPaging.iPage - iHalf + 1;
+ iEnd = iStart + iListLength - 1;
+ }
+
+ for (i = 0, ien = an.length; i < ien; i++) {
+ // Remove the middle elements
+ $('li:gt(0)', an[i]).filter(':not(:last)').remove();
+
+ // Add the new list items and their event handlers
+ for (j = iStart; j <= iEnd; j++) {
+ sClass = (j == oPaging.iPage + 1) ? 'class="active"' : '';
+ $('<li ' + sClass + '><a href="#">' + j + '</a></li>')
+ .insertBefore($('li:last', an[i])[0])
+ .bind('click', function(e) {
+ e.preventDefault();
+ oSettings._iDisplayStart = (parseInt($('a', this).text(), 10) - 1) * oPaging.iLength;
+ fnDraw(oSettings);
+ });
+ }
+
+ // Add / remove disabled classes from the static elements
+ if (oPaging.iPage === 0) {
+ $('li:first', an[i]).addClass('disabled');
+ } else {
+ $('li:first', an[i]).removeClass('disabled');
+ }
+
+ if (oPaging.iPage === oPaging.iTotalPages - 1 || oPaging.iTotalPages === 0) {
+ $('li:last', an[i]).addClass('disabled');
+ } else {
+ $('li:last', an[i]).removeClass('disabled');
+ }
+ }
+ }
+ }
+ });
+}
+
+/*
+ * TableTools Bootstrap compatibility
+ * Required TableTools 2.1+
+ */
+if ($.fn.DataTable.TableTools) {
+ // Set the classes that TableTools uses to something suitable for Bootstrap
+ $.extend(true, $.fn.DataTable.TableTools.classes, {
+ 'container': 'DTTT btn-group',
+ 'buttons': {
+ 'normal': 'btn btn-default',
+ 'disabled': 'disabled'
+ },
+ 'collection': {
+ 'container': 'DTTT_dropdown dropdown-menu',
+ 'buttons': {
+ 'normal': '',
+ 'disabled': 'disabled'
+ }
+ },
+ 'print': {
+ 'info': 'DTTT_print_info modal'
+ },
+ 'select': {
+ 'row': 'active'
+ }
+ });
+
+ // Have the collection use a bootstrap compatible dropdown
+ $.extend(true, $.fn.DataTable.TableTools.DEFAULTS.oTags, {
+ 'collection': {
+ 'container': 'ul',
+ 'button': 'li',
+ 'liner': 'a'
+ }
+ });
+}
diff --git a/app/dashboard/static/js/datatables.min.js b/app/dashboard/static/js/datatables.min.js
new file mode 100644
index 0000000..8351d2a
--- /dev/null
+++ b/app/dashboard/static/js/datatables.min.js
@@ -0,0 +1 @@
+$.extend(true,$.fn.dataTable.defaults,{sDom:"<'row'<'col-xs-6'l>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>",oLanguage:{sLengthMenu:"_MENU_ builds per page"},bSort:false});$.extend($.fn.dataTableExt.oStdClasses,{sWrapper:"dataTables_wrapper form-inline",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm"});if($.fn.dataTable.Api){$.fn.dataTable.defaults.renderer="bootstrap";$.fn.dataTable.ext.renderer.pageButton.bootstrap=function(settings,host,idx,buttons,page,pages){var api=new $.fn.dataTable.Api(settings),classes=settings.oClasses,lang=settings.oLanguage.oPaginate,btnDisplay,btnClass;var attach=function(container,buttons){var i,ien,node,button;var clickHandler=function(e){e.preventDefault();if(e.data.action!=="ellipsis"){api.page(e.data.action).draw(false)}};for(i=0,ien=buttons.length;i<ien;i++){button=buttons[i];if($.isArray(button)){attach(container,button)}else{btnDisplay="";btnClass="";switch(button){case"ellipsis":btnDisplay="&hellip;";btnClass="disabled";break;case"first":btnDisplay=lang.sFirst;btnClass=button+(page>0?"":" disabled");break;case"previous":btnDisplay=lang.sPrevious;btnClass=button+(page>0?"":" disabled");break;case"next":btnDisplay=lang.sNext;btnClass=button+(page<pages-1?"":" disabled");break;case"last":btnDisplay=lang.sLast;btnClass=button+(page<pages-1?"":" disabled");break;default:btnDisplay=button+1;btnClass=page===button?"active":"";break}if(btnDisplay){node=$("<li>",{"class":classes.sPageButton+" "+btnClass,"aria-controls":settings.sTableId,tabindex:settings.iTabIndex,id:idx===0&&typeof button==="string"?settings.sTableId+"_"+button:null}).append($("<a>",{href:"#"}).html(btnDisplay)).appendTo(container);settings.oApi._fnBindAction(node,{action:button},clickHandler)}}}};attach($(host).empty().html('<ul class="pagination"/>').children("ul"),buttons)}}else{$.fn.dataTable.defaults.sPaginationType="bootstrap";$.fn.dataTableExt.oApi.fnPagingInfo=function(oSettings){return{iStart:oSettings._iDisplayStart,iEnd:oSettings.fnDisplayEnd(),iLength:oSettings._iDisplayLength,iTotal:oSettings.fnRecordsTotal(),iFilteredTotal:oSettings.fnRecordsDisplay(),iPage:oSettings._iDisplayLength===-1?0:Math.ceil(oSettings._iDisplayStart/oSettings._iDisplayLength),iTotalPages:oSettings._iDisplayLength===-1?0:Math.ceil(oSettings.fnRecordsDisplay()/oSettings._iDisplayLength)}};$.extend($.fn.dataTableExt.oPagination,{bootstrap:{fnInit:function(oSettings,nPaging,fnDraw){var oLang=oSettings.oLanguage.oPaginate,fnClickHandler=function(e){e.preventDefault();if(oSettings.oApi._fnPageChange(oSettings,e.data.action)){fnDraw(oSettings)}};$(nPaging).append('<ul class="pagination"><li class="prev disabled"><a href="#">'+oLang.sPrevious+'</a></li><li class="next disabled"><a href="#">'+oLang.sNext+"</a></li></ul>");var els=$("a",nPaging);$(els[0]).bind("click.DT",{action:"previous"},fnClickHandler);$(els[1]).bind("click.DT",{action:"next"},fnClickHandler)},fnUpdate:function(oSettings,fnDraw){var iListLength=5,oPaging=oSettings.oInstance.fnPagingInfo(),an=oSettings.aanFeatures.p,i,ien,j,sClass,iStart,iEnd,iHalf=Math.floor(iListLength/2);if(oPaging.iTotalPages<iListLength){iStart=1;iEnd=oPaging.iTotalPages}else{if(oPaging.iPage<=iHalf){iStart=1;iEnd=iListLength}else{if(oPaging.iPage>=(oPaging.iTotalPages-iHalf)){iStart=oPaging.iTotalPages-iListLength+1;iEnd=oPaging.iTotalPages}else{iStart=oPaging.iPage-iHalf+1;iEnd=iStart+iListLength-1}}}for(i=0,ien=an.length;i<ien;i++){$("li:gt(0)",an[i]).filter(":not(:last)").remove();for(j=iStart;j<=iEnd;j++){sClass=(j==oPaging.iPage+1)?'class="active"':"";$("<li "+sClass+'><a href="#">'+j+"</a></li>").insertBefore($("li:last",an[i])[0]).bind("click",function(e){e.preventDefault();oSettings._iDisplayStart=(parseInt($("a",this).text(),10)-1)*oPaging.iLength;fnDraw(oSettings)})}if(oPaging.iPage===0){$("li:first",an[i]).addClass("disabled")}else{$("li:first",an[i]).removeClass("disabled")}if(oPaging.iPage===oPaging.iTotalPages-1||oPaging.iTotalPages===0){$("li:last",an[i]).addClass("disabled")}else{$("li:last",an[i]).removeClass("disabled")}}}}})}if($.fn.DataTable.TableTools){$.extend(true,$.fn.DataTable.TableTools.classes,{container:"DTTT btn-group",buttons:{normal:"btn btn-default",disabled:"disabled"},collection:{container:"DTTT_dropdown dropdown-menu",buttons:{normal:"",disabled:"disabled"}},print:{info:"DTTT_print_info modal"},select:{row:"active"}});$.extend(true,$.fn.DataTable.TableTools.DEFAULTS.oTags,{collection:{container:"ul",button:"li",liner:"a"}})}; \ No newline at end of file
diff --git a/app/dashboard/static/js/kernelci.js b/app/dashboard/static/js/kernelci.js
new file mode 100644
index 0000000..6fbed8c
--- /dev/null
+++ b/app/dashboard/static/js/kernelci.js
@@ -0,0 +1,45 @@
+// Copyright (C) 2014 Linaro Ltd.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Date.prototype.getCustomISOFormat = function() {
+ var year = this.getUTCFullYear().toString();
+ var month = (this.getUTCMonth() + 1).toString();
+ var day = this.getUTCDate().toString();
+
+ month = month[1] ? month : '0' + month[0];
+ day = day[1] ? day : '0' + day[0];
+
+ var hour = this.getUTCHours().toString();
+ var minute = this.getUTCMinutes().toString();
+ var seconds = this.getUTCSeconds().toString();
+
+ hour = hour[1] ? hour : '0' + hour[0];
+ minute = minute[1] ? minute : '0' + minute[0];
+ seconds = seconds[1] ? seconds : '0' + seconds[0];
+
+ return year + '-' + month + '-' + day + ' ' + hour + ':' + minute +
+ ':' + seconds + ' UTC';
+};
+
+Date.prototype.getCustomISODate = function() {
+ var year = this.getUTCFullYear().toString();
+ var month = (this.getUTCMonth() + 1).toString();
+ var day = this.getUTCDate().toString();
+
+ month = month[1] ? month : '0' + month[0];
+ day = day[1] ? day : '0' + day[0];
+
+ return year + '-' + month + '-' + day;
+};
diff --git a/app/dashboard/templates/base.html b/app/dashboard/templates/base.html
new file mode 100644
index 0000000..3032d12
--- /dev/null
+++ b/app/dashboard/templates/base.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{%- block title %}{%- endblock %}</title>
+{%- block head %}
+ <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.min.css">
+ <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css">
+{%- endblock %}
+</head>
+<body>
+ <div class="navbar navbar-default navbar-static-top" role="navigation">
+ <div class="container">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ </div>
+ <div class="navbar-collapse collapse">
+ <ul class="nav navbar-nav">
+ <li id="li-home"><a href="/">Home</a></li>
+ <li id="li-job"><a href="/job">Jobs</a></li>
+ <li id="li-build"><a href="/build">Builds</a></li>
+ <li id="li-info"><a href="/info">Info</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class="container">
+ {%- block content %}{%- endblock %}
+ </div>
+{%- block scripts %}
+<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
+<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/js/bootstrap.min.js"></script>
+{%- endblock %}
+</body>
+</html> \ No newline at end of file
diff --git a/app/dashboard/templates/build.html b/app/dashboard/templates/build.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/dashboard/templates/build.html
diff --git a/app/dashboard/templates/builds.html b/app/dashboard/templates/builds.html
new file mode 100644
index 0000000..20b75c2
--- /dev/null
+++ b/app/dashboard/templates/builds.html
@@ -0,0 +1,110 @@
+{% extends 'base.html' %}
+{%- block title %}{{ page_title|safe }}{%- endblock %}
+{%- block head %}
+{{ super() }}
+ <link rel="stylesheet" type="text/css" href="/static/css/datatables.min.css">
+{%- endblock %}
+{%- block content %}
+<div class="row">
+<table cellpadding="0" cellspacing="0" border="0" class="table table-hover table-striped" id="defconfigs">
+</table>
+</div>
+{%- endblock %}
+{%- block scripts %}
+{{ super() }}
+<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10-dev/js/jquery.dataTables.min.js"></script>
+<script type="text/javascript" src="/static/js/datatables.min.js"></script>
+<script type="text/javascript">
+$(document).ready(function () {
+ $('#li-build').addClass("active");
+ $('#defconfigs').dataTable({
+ 'oLanguage': {
+ 'sLengthMenu': '_MENU_ builds per page'
+ },
+ 'bProcessing': true,
+ 'bSort': true,
+ 'sAjaxSource': '/_ajax/defconf',
+ 'aLengthMenu': [[25, 50, 75, 100], [25, 50, 75, 100]],
+ 'iDisplayLength': 25,
+ 'aaSorting': [[1, 'asc']],
+ 'sAjaxDataProp': 'result',
+ 'aoColumns': [
+ {
+ 'mData': '_id',
+ 'bVisible': false,
+ },
+ {
+ 'mData': 'job',
+ 'sTitle': 'Job',
+ 'mRender': function(data, type, object) {
+ var displ = '<span rel="tooltip" data-toggle="tooltip"' +
+ 'title="Details for&nbsp;' + data + '">' +
+ '<a href="/job/' + data + '">' + data + '&nbsp;&dash;&nbsp;<small>' +
+ object['metadata']['git_branch'] +
+ '</small></a>' +
+ '</span>';
+ return displ;
+ },
+ },
+ {
+ 'mData': 'kernel',
+ 'sTitle': 'Kernel',
+ 'mRender': function(data, type, object) {
+ var job = object['job'];
+ var displ = '<span rel="tooltip" data-toggle="tooltip" title="Details for&nbsp;' + job + '&nbsp;&dash;&nbsp;' + data + '"><a href="/job/' + job + '/kernel/' + data + '">' + data + '</a></span>'
+
+ return displ;
+ },
+ },
+ {
+ 'mData': 'metadata',
+ 'sTitle': 'Defconfig',
+ 'mRender': function(data, type, object) {
+ return data['defconfig'];
+ },
+ },
+ {
+ 'mData': 'metadata',
+ 'sTitle': 'Architecture',
+ 'mRender': function(data, type, object) {
+ return data['arch'];
+ },
+ },
+ {
+ 'mData': 'metadata',
+ 'sTitle': 'Status',
+ 'mRender': function(data, type, object) {
+ var displ,
+ status = data['build_result'];
+ switch (status) {
+ case 'PASS':
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip" title="Build OK">' +
+ '<li class="fa fa-check"></li></span>';
+ break;
+ case 'FAIL':
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip" ' +
+ 'title="Build failed">' +
+ '<li class="fa fa-exclamation-triangle">' +
+ '</li></span>';
+ break;
+ default:
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip"' +
+ 'title="Unknown status">' +
+ '<li class="fa fa-question"></li></span>';
+ break;
+ }
+ return displ;
+ },
+ },
+ ]
+ });
+ $('body').tooltip({
+ 'selector': '[rel=tooltip]',
+ 'placement': 'auto left',
+ });
+});
+</script>
+{%- endblock %} \ No newline at end of file
diff --git a/app/dashboard/templates/index.html b/app/dashboard/templates/index.html
new file mode 100644
index 0000000..f8dafdc
--- /dev/null
+++ b/app/dashboard/templates/index.html
@@ -0,0 +1,142 @@
+{% extends 'base.html' %}
+{%- block title %}{{ page_title|safe }}{%- endblock %}
+{%- block head %}
+{{ super() }}
+ <link rel="stylesheet" type="text/css" href="/static/css/datatables.min.css">
+{%- endblock %}
+{%- block content %}
+<div class="row">
+ <div class="col-lg-6">
+ <h3>Latest Failed Builds <small>(last 15 days)</small></h3>
+ <table id="failed-builds" class="table table-striped table-condensed">
+ <thead>
+ <tr>
+ <th>Tree &dash; Branch</th>
+ <th>Kernel</th>
+ <th>Date</th>
+ </tr>
+ <thead>
+ <tbody id="failed-builds-body"></tbody>
+ <tfoot>
+ <tr>
+ <td colspan="3">
+ <span rel="tooltip" data-toggle="tooltip" title="View all available builds">
+ <small><a href="/build">All builds</a></small>
+ </span>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ <div class="col-lg-6">
+ <h3>Latest Failed Jobs <small>(last 15 days)</small></h3>
+ <table id="failed-jobs" class="table table-striped table-condensed">
+ <thead>
+ <tr>
+ <th>Tree &dash; Branch</th>
+ <th>Kernel</th>
+ <th>Date</th>
+ </tr>
+ <thead>
+ <tbody id="failed-jobs-body"></tbody>
+ <tfoot>
+ <tr>
+ <td colspan="3">
+ <span rel="tooltip" data-toggle="tooltip" title="View all available jobs">
+ <small><a href="/job">All jobs</a></small>
+ </span>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+</div>
+<div class="row">
+ <div class="col-lg-12">
+ <h3>Latest Boot Reports</h3>
+ </div>
+</div>
+{%- endblock %}
+{%- block scripts %}
+{{ super() }}
+<script type="text/javascript" src="/static/js/kernelci.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#li-home').addClass("active");
+ $('body').tooltip({
+ 'selector': '[rel=tooltip]',
+ 'placement': 'auto',
+ });
+
+ $.ajax({
+ 'url': '/_ajax/defconf',
+ 'type': 'GET',
+ 'traditional': true,
+ 'context': $('#failed-builds-body'),
+ 'data': {
+ 'limit': 5,
+ 'status': 'FAILED',
+ 'sort': 'created',
+ 'field': ['job', 'kernel', 'metadata', 'created'],
+ },
+ }).done(function(data) {
+ var defconfs = data['result'];
+
+ $(this).empty();
+
+ if (defconfs.length == 0) {
+ var row = '<tr><td colspan="3" align="center" valign="middle"><h4>' +
+ 'No failed builds.</h4></td></tr>';
+ $(this).append(row);
+ } else {
+ for (var i = 0; i < defconfs.length; i++) {
+ var job = defconfs[i]['job'];
+
+ var created = new Date(defconfs[i]['created']),
+ col1 = '<td>' +
+ '<span rel="tooltip" data-toggle="tooltip" title="Details for&nbsp;' + job + '">' +
+ '<a href="/job/' + job + '">' + job + '&nbsp;&dash;&nbsp;<small>' + defconfs[i]['metadata']['git_branch'] + '</small>' +'</a></span></td>',
+ col2 = '<td>' + defconfs[i]['kernel'] + '</td>',
+ col3 = '<td>' + created.getCustomISODate() + '</td>',
+ row = '<tr>' + col1 + col2 + col3 + '</tr>';
+
+ $(this).append(row);
+ };
+ };
+ });
+
+ $.ajax({
+ 'url': '/_ajax/job',
+ 'type': 'GET',
+ 'traditional': true,
+ 'context': $('#failed-jobs-body'),
+ 'data': {
+ 'limit': 5,
+ 'status': 'FAILED',
+ 'sort': 'created',
+ 'field': ['job', 'kernel', 'created', 'metadata'],
+ },
+ }).done(function(data) {
+ var jobs = data['result'];
+
+ $(this).empty();
+
+ if (jobs.length == 0) {
+ var row = '<tr><td colspan="3" align="center" valign="middle"><h4>' +
+ 'No failed jobs.</h4></td></tr>';
+ $(this).append(row);
+ } else {
+ for (var i = 0; i < jobs.length; i++) {
+ var created = new Date(jobs[i]['created']),
+ col1 = '<td>' + jobs[i]['job'] + '&nbsp;&dash;&nbsp;<small>' + jobs[i]['metadata']['git_branch'] + '</small>' +'</td>',
+ col2 = '<td>' + jobs[i]['kernel'] + '</td>',
+ col3 = '<td>' + created.getCustomISODate() + '</td>',
+ row = '<tr>' + col1 + col2 + col3 + '</tr>';
+
+ $(this).append(row);
+ };
+ };
+ });
+});
+</script>
+{%- endblock %} \ No newline at end of file
diff --git a/app/dashboard/templates/info.html b/app/dashboard/templates/info.html
new file mode 100644
index 0000000..c9786f9
--- /dev/null
+++ b/app/dashboard/templates/info.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block title %}Information on Kernel CI Dashboard{% endblock %}
+{% block content %}
+<p>
+ What this is all about.
+</p>
+{% endblock %}
+{%- block scripts %}
+{{ super() }}
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#li-info').addClass("active");
+});
+</script>
+{%- endblock %}
diff --git a/app/dashboard/templates/job-kernel.html b/app/dashboard/templates/job-kernel.html
new file mode 100644
index 0000000..b471f7a
--- /dev/null
+++ b/app/dashboard/templates/job-kernel.html
@@ -0,0 +1,104 @@
+{% extends 'base.html' %}
+{%- block title %}{{ page_title|safe }}{%- endblock %}
+{%- block head %}
+{{ super() }}
+ <link rel="stylesheet" type="text/css" href="/static/css/datatables.min.css">
+{%- endblock %}
+{%- block content %}
+<div class="row">
+ <h4>{{ body_title|safe }}</h4>
+</div>
+<div class="row">
+ <div class="col-lg-7">
+ <dl class="dl-horizontal">
+ <dt>Git branch</dt>
+ <dd>{{ job_doc.result.metadata.git_branch }}</dd>
+ <dt>Git describe</dt>
+ <dd>{{ job_doc.result.metadata.git_describe }}</dd>
+ <dt>Git URL</dt>
+ <dd>{{ job_doc.result.metadata.git_url }}</dd>
+ <dt>Git commit</dt>
+ <dd>{{ job_doc.result.metadata.git_commit }}</dd>
+ </dl>
+ </div>
+ <div class="col-lg-5">
+ <div id="builds-chart" align="center">
+ <div id="builds-chart-heading"></div>
+ </div>
+ </div>
+</div>
+<div class="row">
+<h4>Defconfigs Built</h4>
+</div>
+{%- endblock %}
+{%- block scripts %}
+{{ super() }}
+<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#li-job').addClass("active");
+ $('body').tooltip({
+ 'selector': '[rel=tooltip]',
+ 'placement': 'auto top',
+ });
+
+ $.ajax({
+ 'url': '/_ajax/defconf',
+ 'type': 'GET',
+ 'traditional': true,
+ 'data': {
+ 'job_id': '{{ job_doc.result._id }}',
+ 'field': 'status',
+ 'nfield': '_id',
+ },
+ }).done(function(data) {
+ console.log(data);
+
+ data = data['result']
+
+ var success = 0,
+ fail = 0,
+ unknown = 0;
+
+ for (var i = 0; i < data.length; i++) {
+ switch (data[i]['status']) {
+ case 'FAILED':
+ fail++;
+ break;
+ case 'SUCCESS':
+ success++;
+ break;
+ default:
+ unknown++;
+ break
+ };
+ };
+
+ var dataset = [success, fail, unknown],
+ width = 200,
+ height = 200,
+ radius = Math.min(width, height) / 2;
+
+ var color = ['#148514', '#A61919', '#A66619']
+ var pie = d3.layout.pie().sort(null);
+ var arc = d3.svg.arc().innerRadius(radius - 30).outerRadius(radius - 50);
+
+ var svg = d3.select("#builds-chart").append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("g")
+ .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+
+ var path = svg.selectAll("path")
+ .data(pie(dataset))
+ .enter().append("path")
+ .attr("fill", function(d, i) {
+ return color[i];
+ })
+ .attr("d", arc);
+
+ $('#builds-chart-heading').append(success + '&nbsp;/&nbsp;' + fail + '&nbsp;/&nbsp;' + unknown)
+ });
+});
+</script>
+{%- endblock %} \ No newline at end of file
diff --git a/app/dashboard/templates/job.html b/app/dashboard/templates/job.html
new file mode 100644
index 0000000..2b5a9ac
--- /dev/null
+++ b/app/dashboard/templates/job.html
@@ -0,0 +1,82 @@
+{%- extends 'base.html' %}
+{%- block title %}{{ page_title|safe }}{%- endblock %}
+{%- block content %}
+<div class="row">
+ <h4>{{ page_title|safe }}</h4>
+ <div class="col-lg-3">
+ <ul class="list-group">
+ <li class="list-group-item">
+ Total builds
+ <span class="badge">{{ kernel.count }}</span>
+ </li>
+ <li class="list-group-item">
+ Total defconfig
+ <span class="badge">{{ defconf.count }}</span>
+ </li>
+ </ul>
+ </div>
+</div>
+<div class="row">
+ <table class="table table-hover table-striped">
+ <thead>
+ <tr>
+ <th>Kernel</th>
+ <th>Branch</th>
+ <th>Commit</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {%- for k in kernel.result %}
+ <tr>
+ <td>{{ k.kernel }}</td>
+ {%- if k.metadata.git_branch %}
+ <td>{{ k.metadata.git_branch }}</td>
+ {%- else %}
+ <td>N/A</td>
+ {%- endif %}
+ {%- if k.metadata.git_commit %}
+ <td>{{ k.metadata.git_commit }}</td>
+ {%- else %}
+ <td>N/A</td>
+ {%- endif %}
+ <td>
+ <span rel="tooltip" data-toggle="tooltip" title="Details for {{k.job}} &dash; {{ k.kernel }}">
+ <a href="/job/{{ k.job }}/kernel/{{ k.kernel }}">
+ <i class="fa fa-search-plus"></i>
+ </a>
+ </span>
+ </td>
+ </tr>
+ {%- endfor %}
+ </tbody>
+ </table>
+</div>
+<div class="row">
+ <div id="matrix">
+ <table>
+ <tbody>
+ <tr>
+ <td class="defconf"></td>
+ </tr>
+ <tr id="row">
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+{%- endblock %}
+{%- block scripts %}
+{{ super() }}
+<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#li-job').addClass("active");
+ $('body').tooltip({
+ 'selector': '[rel=tooltip]',
+ 'placement': 'auto top',
+ });
+});
+</script>
+{%- endblock %}
diff --git a/app/dashboard/templates/jobs.html b/app/dashboard/templates/jobs.html
new file mode 100644
index 0000000..b0ba8b1
--- /dev/null
+++ b/app/dashboard/templates/jobs.html
@@ -0,0 +1,131 @@
+{% extends 'base.html' %}
+{%- block title %}{{ page_title|safe }}{%- endblock %}
+{%- block head %}
+{{ super() }}
+ <link rel="stylesheet" type="text/css" href="/static/css/datatables.min.css">
+{%- endblock %}
+{%- block content %}
+<div class="row">
+<table cellpadding="0" cellspacing="0" border="0" class="table table-hover table-striped" id="jobstable">
+</table>
+</div>
+{%- endblock %}
+{%- block scripts %}
+{{ super() }}
+<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10-dev/js/jquery.dataTables.min.js"></script>
+<script type="text/javascript" src="/static/js/kernelci.js"></script>
+<script type="text/javascript" src="/static/js/datatables.js"></script>
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#li-job').addClass("active");
+ $('#jobstable').dataTable({
+ 'oLanguage': {
+ 'sLengthMenu': '_MENU_ jobs per page'
+ },
+ 'bProcessing': true,
+ 'bSort': true,
+ 'sAjaxSource': '/_ajax/job',
+ 'aLengthMenu': [[25, 50, 75, 100], [25, 50, 75, 100]],
+ 'iDisplayLength': 25,
+ 'aaSorting': [[3,'desc']],
+ 'sAjaxDataProp': 'result',
+ 'fnServerData': function(sSource, aoData, fnCallback, oSettings) {
+ oSettings.jqXHR = $.ajax({
+ 'type': 'GET',
+ 'url': sSource,
+ 'success': fnCallback,
+ 'data': {
+ 'aggregate': 'job',
+ 'sort': 'created',
+ 'sort_order': 1,
+ },
+ });
+ },
+ 'aoColumns': [
+ {
+ 'mData': '_id',
+ 'bVisible': false,
+ },
+ {
+ 'mData': 'job',
+ 'sTitle': 'Job',
+ 'mRender': function(data, type, object) {
+ var displ = '<span rel="tooltip" data-toggle="tooltip"' +
+ 'title="Details for&nbsp;' + data + '">' +
+ '<a href="/job/' + data + '">' + data + '</a>' +
+ '</span>';
+ return displ;
+ }
+ },
+ {
+ 'mData': 'kernel',
+ 'sTitle': 'Kernel',
+ 'mRender': function(data, type, object) {
+ var job = object['job'];
+ var displ = '<span rel="tooltip" data-toggle="tooltip" title="Details for&nbsp;' + job + '&nbsp;&dash;&nbsp;' + data + '"><a href="/job/' + job + '/kernel/' + data + '">' + data + '</a></span>'
+
+ return displ;
+ },
+ },
+ {
+ 'mData': 'created',
+ 'sTitle': 'Created',
+ 'sType': 'date',
+ 'mRender': function(data, type, object) {
+ var created = new Date(object['created']);
+ return created.getCustomISOFormat();
+ },
+ },
+ {
+ 'mData': 'updated',
+ 'sTitle': 'Updated',
+ 'sType': 'date',
+ 'mRender': function(data, type, object) {
+ var updated = new Date(object['updated']);
+ return updated.getCustomISOFormat();
+ },
+ },
+ {
+ 'mData': 'status',
+ 'sTitle': 'Status',
+ 'bSortable': false,
+ 'mRender': function(data, type, object) {
+ var displ;
+ switch (data) {
+ case 'BUILDING':
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip" ' +
+ 'title="Building">' +
+ '<li class="fa fa-cogs"></li></span>';
+ break;
+ case 'DONE':
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip" ' +
+ 'title="Build completed">' +
+ '<li class="fa fa-check"></li></span>';
+ break;
+ case 'FAILED':
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip" title="Fail">' +
+ '<li class="fa fa-exclamation-triangle">' +
+ '</li></span>';
+ break;
+ default:
+ displ = '<span rel="tooltip" ' +
+ 'data-toggle="tooltip"' +
+ 'title="Unknown status">' +
+ '<li class="fa fa-question"></li></span>';
+ break;
+ }
+ return displ;
+ },
+ },
+ ]
+ });
+ $('body').tooltip({
+ 'selector': '[rel=tooltip]',
+ 'placement': 'auto left',
+ });
+});
+</script>
+{%- endblock %} \ No newline at end of file
diff --git a/app/dashboard/utils/__init__.py b/app/dashboard/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/dashboard/utils/__init__.py
diff --git a/app/dashboard/utils/backend.py b/app/dashboard/utils/backend.py
new file mode 100644
index 0000000..0ace6de
--- /dev/null
+++ b/app/dashboard/utils/backend.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import requests
+
+from bson import json_util
+from flask import (
+ current_app,
+ jsonify,
+)
+from urlparse import urljoin
+
+JOB_API = '/api/job'
+DEFCONF_API = '/api/defconfig'
+
+
+def _create_url_headers(api_path):
+ backend_token = current_app.config.get('BACKEND_TOKEN')
+ backend_url = current_app.config.get('BACKEND_URL')
+
+ backend_url = urljoin(backend_url, api_path)
+
+ headers = {}
+ if backend_token:
+ headers = {'X-XSRF-Header': backend_token}
+
+ return (backend_url, headers)
+
+
+def _create_api_path(api_path, doc_id):
+ if api_path[-1] != '/':
+ api_path += '/'
+
+ if doc_id[-1] == '/':
+ doc_id = doc_id[:-1]
+
+ return api_path + doc_id
+
+
+def get_job(**kwargs):
+ api_path = JOB_API
+ if kwargs.get('id', None):
+ api_path = _create_api_path(api_path, kwargs['id'])
+ kwargs.pop('id')
+
+ url, headers = _create_url_headers(api_path)
+ r = requests.get(url, params=kwargs, headers=headers)
+
+ return r
+
+
+def get_all(api_path, **kwargs):
+
+ url, headers = _create_url_headers(api_path)
+ r = requests.get(url, headers=headers, params=kwargs)
+
+ return (r.status_code, r.content)
+
+
+def get_defconfigs(**kwargs):
+
+ url, headers = _create_url_headers(DEFCONF_API)
+ r = requests.get(url, headers=headers, params=kwargs)
+
+ return r
+
+
+def ajax_get_defconfigs(request):
+ url, headers = _create_url_headers(DEFCONF_API)
+ r = requests.get(url, headers=headers, params=request.args.lists())
+
+ response = {}
+
+ if r.status_code == 200:
+ data = json_util.loads(r.content)
+ defconfs = json_util.loads(data['result'])
+
+ response['result'] = defconfs
+
+ return jsonify(response)
+
+
+def ajax_get_jobs(request):
+ url, headers = _create_url_headers(JOB_API)
+ r = requests.get(url, headers=headers, params=request.args.lists())
+
+ response = {}
+
+ if r.status_code == 200:
+ data = json_util.loads(r.content)
+ jobs = json_util.loads(data['result'])
+
+ response['result'] = jobs
+
+ return jsonify(response)
diff --git a/app/dashboard/views/__init__.py b/app/dashboard/views/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/dashboard/views/__init__.py
diff --git a/app/dashboard/views/about.py b/app/dashboard/views/about.py
new file mode 100644
index 0000000..e4ab14b
--- /dev/null
+++ b/app/dashboard/views/about.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from flask.views import View
+from flask import render_template
+
+
+class AboutView(View):
+
+ def dispatch_request(self):
+ return render_template('info.html')
diff --git a/app/dashboard/views/build.py b/app/dashboard/views/build.py
new file mode 100644
index 0000000..f69d53e
--- /dev/null
+++ b/app/dashboard/views/build.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from flask import render_template
+from flask.views import View
+
+
+class BuildsView(View):
+
+ def dispatch_request(self, *args, **kwargs):
+ page_title = 'Kernel CI Dashboard &mdash; Builds'
+ return render_template('builds.html', page_title=page_title)
diff --git a/app/dashboard/views/index.py b/app/dashboard/views/index.py
new file mode 100644
index 0000000..7495f42
--- /dev/null
+++ b/app/dashboard/views/index.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from flask import render_template
+from flask import current_app as app
+from flask.views import View
+
+
+class IndexView(View):
+
+ def dispatch_request(self, *args, **kwargs):
+ base_url = app.config.get('BASE_URL') + "/job"
+ page_title = 'Kernel CI Dashboad &mdash; Home'
+
+ return render_template(
+ 'index.html', page_tile=page_title, base_url=base_url)
diff --git a/app/dashboard/views/job.py b/app/dashboard/views/job.py
new file mode 100644
index 0000000..28aac1a
--- /dev/null
+++ b/app/dashboard/views/job.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from bson import json_util
+from flask import render_template
+from flask.views import View
+
+from dashboard.utils.backend import get_job, get_defconfigs
+
+
+class JobsView(View):
+
+ def dispatch_request(self):
+
+ title = 'Kernel CI Dashboard &mdash; Jobs'
+
+ return render_template(
+ 'jobs.html', page_title=title
+ )
+
+
+class JobView(View):
+
+ def dispatch_request(self, **kwargs):
+
+ title = 'Details for&nbsp;' + kwargs['job']
+
+ kwargs['sort'] = 'created'
+ kwargs['sort_order'] = 1
+
+ response = get_job(**kwargs)
+
+ kernel = {}
+ if response.status_code == 200:
+ kernel = json_util.loads(response.content)
+ kernel['result'] = json_util.loads(kernel['result'])
+
+ response = get_defconfigs(**kwargs)
+
+ if response.status_code == 200:
+ defconf = json_util.loads(response.content)
+ defconf['result'] = json_util.loads(defconf['result'])
+ else:
+ defconf = {}
+
+ return render_template(
+ 'job.html', page_title=title, kernel=kernel, defconf=defconf
+ )
+
+
+class JobIdView(View):
+
+ def dispatch_request(self, **kwargs):
+
+ job = kwargs['job']
+ kernel = kwargs['kernel']
+ job_id = '%s-%s' % (job, kernel)
+
+ body_title = 'Details for&nbsp;%s&nbsp;&dash;&nbsp;%s' % (job, kernel)
+ title = 'Kernel CI Dashboard &mdash;&nbsp;' + body_title
+
+ params = {'id': job_id}
+ response = get_job(**params)
+
+ job_doc = {}
+ if response.status_code == 200:
+ job_doc = json_util.loads(response.content)
+ job_doc['result'] = json_util.loads(job_doc['result'])
+
+ return render_template(
+ 'job-kernel.html', page_title=title, body_title=body_title,
+ job_doc=job_doc,
+ )
diff --git a/app/server.py b/app/server.py
new file mode 100755
index 0000000..53bb097
--- /dev/null
+++ b/app/server.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2014 Linaro Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from dashboard import app
+
+
+if __name__ == '__main__':
+ app.run()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..809d40e
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+Flask==0.10.1
+requests==2.2.1
+pymongo