diff options
author | Milo Casagrande <milo@ubuntu.com> | 2014-04-25 15:28:52 +0200 |
---|---|---|
committer | Milo Casagrande <milo@ubuntu.com> | 2014-04-25 15:28:52 +0200 |
commit | bf35309042f183604e992e6337a33381bef584f5 (patch) | |
tree | 96d00561f813aa11c71588c775edf59552a4e3d3 |
Initial commit.
30 files changed, 1497 insertions, 0 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..a88d797 --- /dev/null +++ b/app/dashboard/static/images/sort_asc.png diff --git a/app/dashboard/static/images/sort_asc_disabled.png b/app/dashboard/static/images/sort_asc_disabled.png Binary files differnew file mode 100644 index 0000000..4e144cf --- /dev/null +++ b/app/dashboard/static/images/sort_asc_disabled.png diff --git a/app/dashboard/static/images/sort_both.png b/app/dashboard/static/images/sort_both.png Binary files differnew file mode 100644 index 0000000..1867040 --- /dev/null +++ b/app/dashboard/static/images/sort_both.png diff --git a/app/dashboard/static/images/sort_desc.png b/app/dashboard/static/images/sort_desc.png Binary files differnew file mode 100644 index 0000000..def071e --- /dev/null +++ b/app/dashboard/static/images/sort_desc.png diff --git a/app/dashboard/static/images/sort_desc_disabled.png b/app/dashboard/static/images/sort_desc_disabled.png Binary files differnew file mode 100644 index 0000000..7824973 --- /dev/null +++ b/app/dashboard/static/images/sort_desc_disabled.png 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 = '…'; + 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="…";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 ' + data + '">' + + '<a href="/job/' + data + '">' + data + ' ‐ <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 ' + job + ' ‐ ' + 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 ‐ 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 ‐ 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 ' + job + '">' + + '<a href="/job/' + job + '">' + job + ' ‐ <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'] + ' ‐ <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 + ' / ' + fail + ' / ' + 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}} ‐ {{ 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 ' + 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 ' + job + ' ‐ ' + 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 — 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 — 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 — Jobs' + + return render_template( + 'jobs.html', page_title=title + ) + + +class JobView(View): + + def dispatch_request(self, **kwargs): + + title = 'Details for ' + 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 %s ‐ %s' % (job, kernel) + title = 'Kernel CI Dashboard — ' + 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 |