#219 [frontend] add graphs of utilization
Merged 6 years ago by clime. Opened 6 years ago by dturecek.
copr/ dturecek/copr graphs  into  master

file modified
+5 -1
@@ -88,6 +88,7 @@ 

  BuildRequires: python3-flask-whooshee

  BuildRequires: python3-modulemd

  BuildRequires: redis

+ BuildRequires: xstatic-patternfly-common

  %endif

  

  Requires: httpd
@@ -123,6 +124,7 @@ 

  Requires: python3-CommonMark

  Requires: python3-psycopg2

  Requires: python3-zmq

+ Requires: xstatic-patternfly-common

  

  Provides: bundled(bootstrap) = 3.3.4

  Provides: bundled(bootstrap-combobox) = 1.1.6
@@ -244,6 +246,8 @@ 

  %%copr_frontend_chroot_logodir    %%copr_frontend_staticdir/chroot_logodir

  EOF

  

+ cp -a %{_datadir}/javascript/patternfly %{buildroot}%{_datadir}/copr/coprs_frontend/coprs/static/components/

+ 

  %check

  %if %{with check} && "%{_arch}" == "x86_64"

      pushd coprs_frontend
@@ -346,7 +350,7 @@ 

  - update the info how to install a module

  - fix code block spacing

  - fix scm unification migrations for mock-scm

- - show most recent post from our blog 

+ - show most recent post from our blog

  

  * Thu Nov 09 2017 clime <clime@redhat.com> 1.124-1

  - fix build_on_pagure_commit.py

@@ -85,6 +85,33 @@ 

          return list(query.all()[:4])

  

      @classmethod

+     def get_tasks_by_time(cls, start, end):

+         result = models.BuildChroot.query\

+             .filter(models.BuildChroot.ended_on > start)\

+             .filter(models.BuildChroot.started_on < end)\

+             .order_by(models.BuildChroot.started_on.asc())

+ 

+         return result

+ 

+     @classmethod

+     def get_tasks_from_last_day(cls):

+         end = int(time.time())

+         start = end - 86399

+         step = 3600

+         tasks = cls.get_tasks_by_time(start, end)

+         steps = int(round((end - start) / step + 0.5))

+         current_step = 0

+ 

+         data = [[0] * (steps + 1)]

+         data[0][0] = ''

+         for t in tasks:

+             task = t.to_dict()

+             while task['started_on'] > start + step * (current_step + 1):

+                 current_step += 1

+             data[0][current_step + 1] += 1

+         return data

+ 

+     @classmethod

      def get_build_importing_queue(cls):

          """

          Returns Builds which are waiting to be uploaded to dist git

@@ -0,0 +1,93 @@ 

+ $(document).ready(function(){

+     $("#stats-link").removeClass("hidden");

+     $("#graphs").removeClass("hidden");

+     $(window).trigger('resize');

+ });

+ 

+ function graphConfig() {

+     var colorPattern = ['#0088ce', '#cc0000', '#3f9c35', '#003d44', '#ec7a08', '#703fec',

+     '#470000', '#35caed', '#92d400', '#f5c12e'];

+     var chart = {

+         axis: {

+             x: {

+                 type: 'timeseries'

+             },

+             y: {

+                 min: 0,

+                 padding: {bottom: 0}

+             }

+         },

+         color: {pattern: colorPattern},

+         data: {

+             hide: ['average'],

+             types: {

+                 'average': 'line',

+                 'tasks': 'area'

+             },

+             x: 'time',

+             xFormat: '%Y-%m-%d %H:%M:%S'

+         },

+         grid: {

+             y: {show: true}

+         },

+         point: {r: 2.5},

+         tooltip: {

+             format: {

+                 title: function(d) {

+                     var a = d.toString().substring(0, 25) + 'UTC';

+                     return a;

+                 },

+                 value: function(value, ratio, id) {

+                     if (id === 'average') return value.toFixed(2);

+                     else return value;

+                 }

+             }

+         },

+         zoom: {enabled: true}

+     };

+     return chart;

+ };

+ 

+ function lineGraph(data, ticks, bind, format) {

+     chart = graphConfig();

+     chart.axis.x.tick = {

+         culling: {max: ticks},

+         format: format

+     };

+     chart.bindto = bind;

+     chart.data.columns = data;

+     var chartDay = c3.generate(chart);

+ };

+ 

+ function chrootGraph(data, bind) {

+     chart = graphConfig();

+     chart.axis.x = {show: false};

+     chart.bindto =  bind;

+     chart.data = {

+         columns: data,

+         type: 'bar'

+     };

+     chart.size = {height: 550,

+                   width: 400};

+     chart.tooltip = {

+         format: {

+             title: function (x) {return ''}

+         },

+         position: function (data, width, height, element) {

+             return {top: 0, left: -150};

+         }

+     }

+     chart.zoom = {enabled: false};

+     var chrootsChart = c3.generate(chart);

+ };

+ 

+ function smallGraph(data, bind) {

+     var c3ChartDefaults = $().c3ChartDefaults();

+     var sparklineChartConfig = c3ChartDefaults.getDefaultSparklineConfig();

+     sparklineChartConfig.bindto = bind;

+     sparklineChartConfig.data = {

+         columns: data,

+         type: 'area'

+     };

+     var sparklineChart = c3.generate(sparklineChartConfig);

+ };

@@ -1,212 +0,0 @@ 

- // PatternFly Namespace

- var PatternFly = PatternFly || {};

- 

- // Util: PatternFly Sidebar 

- // Set height of sidebar-pf to height of document minus height of navbar-pf if not mobile

- (function($) {

-   sidebar = function() {

-     var documentHeight = 0;

-     var navbarpfHeight = 0;

-     var colHeight = 0;

-     if ( $('.navbar-pf .navbar-toggle').is(':hidden') ) {

-       documentHeight = $(document).height();

-       navbarpfHeight = $('.navbar-pf').outerHeight();

-       colHeight = documentHeight - navbarpfHeight;

-     }

-     $('.sidebar-pf').parent('.row').children('[class*="col-"]').css({ "min-height":colHeight});

-   }

-   $(document).ready(function() {

-     // Call sidebar() on ready if .sidebar-pf exists and .datatable does not exist

-     if ($('.sidebar-pf').length > 0 && $('.datatable').length == 0) {

-       sidebar();

-     }

-   });

-   $(window).resize(function() {

-     // Call sidebar() on resize if .sidebar-pf exists

-     if ($('.sidebar-pf').length > 0) {

-       sidebar();

-     }

-   });

- })(jQuery);

- 

- // Util: PatternFly Popovers

- // Add data-close="true" to insert close X icon

- (function($) {

-   PatternFly.popovers = function( selector ) {

-     var allpopovers = $(selector);

-     

-     // Initialize

-     allpopovers.popover();

-     

-     // Add close icons

-     allpopovers.filter('[data-close=true]').each(function(index, element) {

-       var $this = $(element),

-         title = $this.attr('data-original-title') + '<button type="button" class="close" aria-hidden="true"><span class="pficon pficon-close"></span></button>';

- 

-       $this.attr('data-original-title', title);

-     });

-     

-     // Bind Close Icon to Toggle Display

-     allpopovers.on('click', function(e) {

-       var $this = $(this);

-         $title = $this.next('.popover').find('.popover-title');

-       

-       // Only if data-close is true add class "x" to title for right padding

-       $title.find('.close').parent('.popover-title').addClass('closable');

-       

-       // Bind x icon to close popover

-       $title.find('.close').on('click', function() {

-         $this.popover('toggle');

-       });

-       

-       // Prevent href="#" page scroll to top

-       e.preventDefault();

-     });

-   };

- })(jQuery);

- 

- 

- // Util: DataTables Settings

- (function($) {

-   if ($.fn.dataTableExt) {

-     /* Set the defaults for DataTables initialisation */

-     $.extend( true, $.fn.dataTable.defaults, {

-       "bDestroy": true,

-       "bAutoWidth": false,

-       "iDisplayLength": 20,

-       "sDom": 

-         "<'dataTables_header' f i r >" + 

-         "<'table-responsive'  t >" + 

-         "<'dataTables_footer' p >",

-       "oLanguage": {

-         "sInfo": "Showing <b>_START_</b> to <b>_END_</b> of <b>_TOTAL_</b> Items",

-         "sInfoFiltered" : "(of <b>_MAX_</b>)",

-         "sInfoEmpty" : "Showing <b>0</b> Results",

-         "sZeroRecords": 

-           "<p>Suggestions</p>" + 

-           "<ul>" + 

-             "<li>Check the syntax of the search term.</li>" +

-             "<li>Check that the correct menu option is chosen (token ID vs. user ID).</li>" +

-             "<li>Use wildcards (* to match zero or more characters or ? to match a single character).</li>" +

-             "<li>Clear the search field, then click Search to return to the 20 most recent records.</li>" +

-           "</ul>",

-         "sSearch": ""

-       },

-       "sPaginationType": "bootstrap_input"

-     });

- 

-     /* Default class modification */

-     $.extend( $.fn.dataTableExt.oStdClasses, {

-       "sWrapper": "dataTables_wrapper"

-     });

- 

-     /* 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 )

-       };

-     };

- 

-     /* Combination of Bootstrap + Input Text style pagination control */

-     $.extend( $.fn.dataTableExt.oPagination, {

-       "bootstrap_input": {

-         "fnInit": function( oSettings, nPaging, fnDraw ) {

-           var oLang = oSettings.oLanguage.oPaginate;

-           var fnClickHandler = function ( e ) {

-             e.preventDefault();

-             if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) {

-               fnDraw( oSettings );

-             }

-           };

- 

-           $(nPaging).append(

-             '<ul class="pagination">'+

-               '<li class="first disabled"><span class="i fa fa-angle-double-left"></span></li>' +

-               '<li class="prev disabled"><span class="i fa fa-angle-left"></span></li>' +

-             '</ul>' + 

-             '<div class="pagination-input">' + 

-               '<input type="text" class="paginate_input">' + 

-               '<span class="paginate_of">of <b>3</b></span>' + 

-             '</div>' + 

-             '<ul class="pagination">'+

-               '<li class="next disabled"><span class="i fa fa-angle-right"></span></li>' +

-               '<li class="last disabled"><span class="i fa fa-angle-double-right"></span></li>' +

-             '</ul>'

-           );

-           

-           var els = $('li', nPaging);

-           $(els[0]).bind( 'click.DT', { action: "first" }, fnClickHandler );

-           $(els[1]).bind( 'click.DT', { action: "previous" }, fnClickHandler );

-           $(els[2]).bind( 'click.DT', { action: "next" }, fnClickHandler );

-           $(els[3]).bind( 'click.DT', { action: "last" }, fnClickHandler );

-           

-           var nInput = $('input', nPaging);

-           $(nInput).keyup( function (e) {

-               if ( e.which == 38 || e.which == 39 ) {

-                 this.value++;

-               }

-               else if ( (e.which == 37 || e.which == 40) && this.value > 1 ) {

-                 this.value--;

-               }

-                 

-               if ( this.value == "" || this.value.match(/[^0-9]/) ) {

-                 /* Nothing entered or non-numeric character */

-                 return;

-               }

-                 

-               var iNewStart = oSettings._iDisplayLength * (this.value - 1);

-               if ( iNewStart > oSettings.fnRecordsDisplay() ) {

-                 /* Display overrun */

-                 oSettings._iDisplayStart = (Math.ceil((oSettings.fnRecordsDisplay()-1) /

-                   oSettings._iDisplayLength)-1) * oSettings._iDisplayLength;

-                 fnDraw( oSettings );

-                 return;

-               }

-                 

-               oSettings._iDisplayStart = iNewStart;

-               fnDraw( oSettings );

-           });

-         },

- 

-         "fnUpdate": function ( oSettings, fnDraw ) {

-           var oPaging = oSettings.oInstance.fnPagingInfo(),

-             an = oSettings.aanFeatures.p,

-             i,

-             ien,

-             iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength),

-             iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;

- 

-           for ( i=0, ien=an.length ; i<ien ; i++ ) {

-             $('.paginate_input').val(iCurrentPage);

-             $('.paginate_of b').html(iPages);

-             

-             // Add / remove disabled classes from the static elements

-             if ( oPaging.iPage === 0 ) {

-               $('li.first', an[i]).addClass('disabled');

-               $('li.prev', an[i]).addClass('disabled');

-             } else {

-               $('li.first', an[i]).removeClass('disabled');

-               $('li.prev', an[i]).removeClass('disabled');

-             }

- 

-             if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) {

-               $('li.next', an[i]).addClass('disabled');

-               $('li.last', an[i]).addClass('disabled');

-             } else {

-               $('li.next', an[i]).removeClass('disabled');

-               $('li.last', an[i]).removeClass('disabled');

-             }

-           }

-         }

-       }

-     });

-   }

- })(jQuery);

@@ -1,1 +0,0 @@ 

- var PatternFly=PatternFly||{};!function($){sidebar=function(){var documentHeight=0,navbarpfHeight=0,colHeight=0;$(".navbar-pf .navbar-toggle").is(":hidden")&&(documentHeight=$(document).height(),navbarpfHeight=$(".navbar-pf").outerHeight(),colHeight=documentHeight-navbarpfHeight),$(".sidebar-pf").parent(".row").children('[class*="col-"]').css({"min-height":colHeight})},$(document).ready(function(){$(".sidebar-pf").length>0&&0==$(".datatable").length&&sidebar()}),$(window).resize(function(){$(".sidebar-pf").length>0&&sidebar()})}(jQuery),function($){PatternFly.popovers=function(selector){var allpopovers=$(selector);allpopovers.popover(),allpopovers.filter("[data-close=true]").each(function(index,element){var $this=$(element),title=$this.attr("data-original-title")+'<button type="button" class="close" aria-hidden="true"><span class="pficon pficon-close"></span></button>';$this.attr("data-original-title",title)}),allpopovers.on("click",function(e){var $this=$(this);$title=$this.next(".popover").find(".popover-title"),$title.find(".close").parent(".popover-title").addClass("closable"),$title.find(".close").on("click",function(){$this.popover("toggle")}),e.preventDefault()})}}(jQuery),function($){$.fn.dataTableExt&&($.extend(!0,$.fn.dataTable.defaults,{bDestroy:!0,bAutoWidth:!1,iDisplayLength:20,sDom:"<'dataTables_header' f i r ><'table-responsive'  t ><'dataTables_footer' p >",oLanguage:{sInfo:"Showing <b>_START_</b> to <b>_END_</b> of <b>_TOTAL_</b> Items",sInfoFiltered:"(of <b>_MAX_</b>)",sInfoEmpty:"Showing <b>0</b> Results",sZeroRecords:"<p>Suggestions</p><ul><li>Check the syntax of the search term.</li><li>Check that the correct menu option is chosen (token ID vs. user ID).</li><li>Use wildcards (* to match zero or more characters or ? to match a single character).</li><li>Clear the search field, then click Search to return to the 20 most recent records.</li></ul>",sSearch:""},sPaginationType:"bootstrap_input"}),$.extend($.fn.dataTableExt.oStdClasses,{sWrapper:"dataTables_wrapper"}),$.fn.dataTableExt.oApi.fnPagingInfo=function(oSettings){return{iStart:oSettings._iDisplayStart,iEnd:oSettings.fnDisplayEnd(),iLength:oSettings._iDisplayLength,iTotal:oSettings.fnRecordsTotal(),iFilteredTotal:oSettings.fnRecordsDisplay(),iPage:-1===oSettings._iDisplayLength?0:Math.ceil(oSettings._iDisplayStart/oSettings._iDisplayLength),iTotalPages:-1===oSettings._iDisplayLength?0:Math.ceil(oSettings.fnRecordsDisplay()/oSettings._iDisplayLength)}},$.extend($.fn.dataTableExt.oPagination,{bootstrap_input:{fnInit:function(oSettings,nPaging,fnDraw){var fnClickHandler=(oSettings.oLanguage.oPaginate,function(e){e.preventDefault(),oSettings.oApi._fnPageChange(oSettings,e.data.action)&&fnDraw(oSettings)});$(nPaging).append('<ul class="pagination"><li class="first disabled"><span class="i fa fa-angle-double-left"></span></li><li class="prev disabled"><span class="i fa fa-angle-left"></span></li></ul><div class="pagination-input"><input type="text" class="paginate_input"><span class="paginate_of">of <b>3</b></span></div><ul class="pagination"><li class="next disabled"><span class="i fa fa-angle-right"></span></li><li class="last disabled"><span class="i fa fa-angle-double-right"></span></li></ul>');var els=$("li",nPaging);$(els[0]).bind("click.DT",{action:"first"},fnClickHandler),$(els[1]).bind("click.DT",{action:"previous"},fnClickHandler),$(els[2]).bind("click.DT",{action:"next"},fnClickHandler),$(els[3]).bind("click.DT",{action:"last"},fnClickHandler);var nInput=$("input",nPaging);$(nInput).keyup(function(e){if(38==e.which||39==e.which?this.value++:(37==e.which||40==e.which)&&this.value>1&&this.value--,""!=this.value&&!this.value.match(/[^0-9]/)){var iNewStart=oSettings._iDisplayLength*(this.value-1);if(iNewStart>oSettings.fnRecordsDisplay())return oSettings._iDisplayStart=(Math.ceil((oSettings.fnRecordsDisplay()-1)/oSettings._iDisplayLength)-1)*oSettings._iDisplayLength,void fnDraw(oSettings);oSettings._iDisplayStart=iNewStart,fnDraw(oSettings)}})},fnUpdate:function(oSettings){var i,ien,oPaging=oSettings.oInstance.fnPagingInfo(),an=oSettings.aanFeatures.p,iPages=Math.ceil(oSettings.fnRecordsDisplay()/oSettings._iDisplayLength),iCurrentPage=Math.ceil(oSettings._iDisplayStart/oSettings._iDisplayLength)+1;for(i=0,ien=an.length;ien>i;i++)$(".paginate_input").val(iCurrentPage),$(".paginate_of b").html(iPages),0===oPaging.iPage?($("li.first",an[i]).addClass("disabled"),$("li.prev",an[i]).addClass("disabled")):($("li.first",an[i]).removeClass("disabled"),$("li.prev",an[i]).removeClass("disabled")),oPaging.iPage===oPaging.iTotalPages-1||0===oPaging.iTotalPages?($("li.next",an[i]).addClass("disabled"),$("li.last",an[i]).addClass("disabled")):($("li.next",an[i]).removeClass("disabled"),$("li.last",an[i]).removeClass("disabled"))}}}))}(jQuery); 

\ No newline at end of file

@@ -256,7 +256,7 @@ 

  {% endmacro %}

  

  

- {% macro task_queue_panel(tasks_info) %}

+ {% macro task_queue_panel(tasks_info, graph) %}

    <div class="panel panel-default">

      <div class="panel-heading">

        <h3 class="panel-title"> Task Queue </h3>
@@ -274,6 +274,10 @@ 

          <span class="badge">{{ tasks_info.running }}</span>

          Running

        </a>

+       <a id="stats-link" href="{{url_for('status_ns.stats')}}" class="list-group-item hidden">

+         Tasks during last 24 hours:

+         <div id="small-graph" class="chart-pf-sparkline"></div>

+       </a>

      </div>

    </div>

  {% endmacro %}

@@ -40,12 +40,12 @@ 

    </div>

    <div class="col-md-3 col-sm-4">

      <br>

-     

+ 

      {% if g.user %}

        {{ user_projects_panel(g.user) }}

      {% endif %}

  

-     {{ task_queue_panel(tasks_info) }}

+     {{ task_queue_panel(tasks_info, graph) }}

      {{ recent_builds_panel(users_builds) }}

  

    </div>

@@ -4,6 +4,9 @@ 

  {% from "_helpers.html" import render_pagination, copr_details_href, copr_name, user_projects_panel %}

  {% from "_helpers.html" import recent_builds_panel, task_queue_panel, recent_blog_panel %}

  {%block main_menu_projects %}active{% endblock %}

+ {% block head %}

+ <link rel="stylesheet" href="{{ url_for('static', filename='components/patternfly/css/patternfly-additions.min.css') }}">

+ {% endblock %}

  {% block body %}

  <div class="row">

    <div class="col-md-9 col-sm-8">
@@ -47,9 +50,15 @@ 

      {% endif %}

  

      {{ recent_blog_panel() }}

-     {{ task_queue_panel(tasks_info) }}

+     {{ task_queue_panel(tasks_info, graph) }}

      {{ recent_builds_panel(users_builds) }}

  

    </div>

  </div>

  {% endblock %}

+ {% block footer %}

+ <script src="{{ url_for('static', filename='components/c3/c3.min.js') }}"></script>

+ <script src="{{ url_for('static', filename='components/d3/d3.min.js') }}"></script>

+ <script src="{{ url_for('static', filename='js/graphs.js') }}"></script>

+ <script>smallGraph({{ graph|tojson }}, "#small-graph");</script>

+ {% endblock %}

@@ -21,11 +21,13 @@ 

    <script src="{{ url_for('static', filename='components/jquery/dist/jquery.min.js') }}"></script>

    <script src="{{ url_for('static', filename='components/bootstrap/dist/js/bootstrap.min.js') }}"></script>

    <script src="{{ url_for('static', filename='components/datatables/media/js/jquery.dataTables.js') }}"></script>

-   <script src="{{ url_for('static', filename='js/patternfly.min.js') }}"></script>

+   <script src="{{ url_for('static', filename='components/patternfly/js/patternfly.min.js') }}"></script>

  

    <script src="{{ url_for('static', filename='copr.js') }}"></script>

    <link rel=stylesheet type=text/css href="{{ url_for('static', filename='copr.css') }}">

    <link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/style-overwrite.css') }}">

+   {% block head %}

+   {% endblock %}

  </head>

  

  <body>
@@ -170,5 +172,6 @@ 

    <!-- Last post from our blog -->

    <script src="{{ url_for('static', filename='js/blog.js') }}"></script>

  

+   {% block footer %}{% endblock %}

  </body>

  </html>

@@ -25,6 +25,9 @@ 

    <li class="{% block running_selected %}{% endblock %}">

        <a href="{{ url_for('status_ns.running') }}">Running</a>

    </li>

+   <li class="{% block stats_selected %}{% endblock %}">

+       <a href="{{ url_for('status_ns.stats') }}">Statistics</a>

+   </li>

  </ul>

  {% block status_body %}{% endblock %}

  {% endblock %}

@@ -0,0 +1,51 @@ 

+ {% extends "status.html" %}

+ {% block title %} Statistics - Copr {% endblock %}

+ {% from "_helpers.html" import build_href, copr_name, copr_details_href %}

+ {% block head %}

+ <link rel="stylesheet" href="{{ url_for('static', filename='components/patternfly/css/patternfly-additions.min.css') }}">

+ {% endblock %}

+ {% block stats_selected %}active{% endblock %}

+ {%block status_breadcrumb %}

+ <li class="active">

+   Statistics

+ </li>

+ {%endblock%}

+ {% block status_body %}

+ <noscript>

+ This page uses JavaScript to show graphs of Copr utilization.

+ </noscript>

+ <div id="graphs" class="hidden">

+   <div class="row">

+     <h3>Tasks during last 24 hours</h3>

+     <div id="chartDay" class="line-chart-pf"></div>

+   </div>

+   <div class="row">

+     <h3>Tasks during last 90 days</h3>

+       <div id="chartNinetyDays" class="line-chart-pf"></div>

+   </div>

+   <div class="row">

+     <h3>Tasks divided by chroots</h3>

+     <div class="col-md-1"></div>

+     <div class="col-md-5">

+       <h4>During last 24 hours</h4>

+       <div id="chartChrootsDay"></div>

+     </div>

+     <div class="col-md-5">

+       <h4>During last 90 days</h4>

+       <div id="chartChrootsNinetyDays"></div>

+     </div>

+   </div>

+ </div>

+ {% endblock %}

+ 

+ {% block footer %}

+ <script src="{{ url_for('static', filename='components/c3/c3.min.js') }}"></script>

+ <script src="{{ url_for('static', filename='components/d3/d3.min.js') }}"></script>

+ <script src="{{ url_for('static', filename='js/graphs.js') }}"></script>

+ <script>

+   lineGraph({{ data1|tojson }}, 48, "#chartDay", "%H:%M");

+   lineGraph({{ data2|tojson }}, 20, "#chartNinetyDays", "%Y-%m-%d");

+   chrootGraph({{ chroots1|tojson }}, "#chartChrootsDay");

+   chrootGraph({{ chroots2|tojson }}, "#chartChrootsNinetyDays");

+ </script>

+ {% endblock %}

@@ -77,11 +77,14 @@ 

      # users_builds = builds_logic.BuildsLogic.get_recent_tasks(flask.g.user, 5)

      users_builds = builds_logic.BuildsLogic.get_recent_tasks(None, 4)

  

+     data = builds_logic.BuildsLogic.get_tasks_from_last_day()

+ 

      return flask.render_template("coprs/show/all.html",

                                   coprs=coprs,

                                   paginator=paginator,

                                   tasks_info=ComplexLogic.get_queues_size(),

-                                  users_builds=users_builds)

+                                  users_builds=users_builds,

+                                  graph=data)

  

  

  @coprs_ns.route("/<username>/", defaults={"page": 1})
@@ -103,12 +106,15 @@ 

      # flask.g.user is none when no user is logged - showing builds from everyone

      users_builds = builds_logic.BuildsLogic.get_recent_tasks(flask.g.user, 4)

  

+     data = builds_logic.BuildsLogic.get_tasks_from_last_day()

+ 

      return flask.render_template("coprs/show/user.html",

                                   user=user,

                                   coprs=coprs,

                                   paginator=paginator,

                                   tasks_info=ComplexLogic.get_queues_size(),

-                                  users_builds=users_builds)

+                                  users_builds=users_builds,

+                                  graph=data)

  

  

  @coprs_ns.route("/fulltext/", defaults={"page": 1})

@@ -5,6 +5,7 @@ 

  from coprs.exceptions import InsufficientRightsException

  from coprs.forms import ActivateFasGroupForm

  from coprs.helpers import Paginator

+ from coprs.logic import builds_logic

  from coprs.logic.complex_logic import ComplexLogic

  from coprs.logic.coprs_logic import CoprsLogic

  from coprs.logic.users_logic import UsersLogic
@@ -65,13 +66,16 @@ 

  

      coprs = paginator.sliced_query

  

+     data = builds_logic.BuildsLogic.get_tasks_from_last_day()

+ 

      return render_template(

          "coprs/show/group.html",

          user=flask.g.user,

          coprs=coprs,

          paginator=paginator,

          tasks_info=ComplexLogic.get_queues_size(),

-         group=group

+         group=group,

+         graph=data

      )

  

  

@@ -1,10 +1,50 @@ 

  import flask

+ import time

  

  from coprs.views.status_ns import status_ns

- from coprs.logic import builds_logic

+ from coprs.logic import builds_logic, coprs_logic

  from coprs import helpers

  

  

+ def get_graph_data(start, end, step):

+     chroots_dict = {}

+     chroots = []

+     chroot_names = {}

+     tasks = builds_logic.BuildsLogic.get_tasks_by_time(start, end)

+     steps = int(round((end - start) / step + 0.5))

+     current_step = 0

+ 

+     data = [[0] * (steps + 1), [1.0 * tasks.count() / steps] * (steps + 1), [0] * (steps + 1)]

+     data[0][0] = 'tasks'

+     data[1][0] = 'average'

+     data[2][0] = 'time'

+ 

+     for t in tasks:

+         task = t.to_dict()

+         while task['started_on'] > start + step * (current_step + 1):

+             current_step += 1

+         data[0][current_step + 1] += 1

+ 

+         if task['mock_chroot_id'] not in chroots_dict:

+             chroots_dict[task['mock_chroot_id']] = 1

+         else:

+             chroots_dict[task['mock_chroot_id']] += 1

+ 

+     for i in range(0, steps):

+         data[2][i + 1] = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start + (i * step)))

+ 

+     for key in chroots_dict:

+         chroots.append([key, chroots_dict[key]])

+ 

+     mock_chroots = coprs_logic.MockChrootsLogic.get_multiple()

+     for mock_chroot in mock_chroots:

+         for l in chroots:

+             if l[0] == mock_chroot.id:

+                 l[0] = mock_chroot.name

+ 

+     return data, chroots

+ 

+ 

  @status_ns.route("/")

  @status_ns.route("/waiting/")

  def waiting():
@@ -37,3 +77,15 @@ 

                                   number=len(list(tasks)),

                                   bg_tasks_cnt=bg_tasks_cnt,

                                   tasks=tasks)

+ 

+ 

+ @status_ns.route("/stats/")

+ def stats():

+     current_time = int(time.time())

+     data1, chroots1 = get_graph_data(current_time - 86400 + 1, current_time, 600) # last 24 hours

+     data2, chroots2 = get_graph_data(current_time - 86400 * 90 + 1, current_time, 86400) # last 90 days

+     return flask.render_template("status/stats.html",

+                                  data1=data1,

+                                  data2=data2,

+                                  chroots1=chroots1,

+                                  chroots2=chroots2)

This adds the graphs of utilization to the front page and the status page.
It's already up on dev server so you can see how it looks.

Instead of bundling patternfly static files, can you rather require xstatic-patternfly-common package and use it?

rebased onto 8b52c554d13c9a5497cbdae28121b205cc3e0af6

6 years ago

Can you remove this comment, please? It's probably not needed.

rebased onto 1ec335aa64b2486f1908c2771c79fc152ebfcb91

6 years ago

I have removed the comment in spec file.
I moved the chroot chart to the bottom and made the average line unchecked by default.

Better than place those script and link definition directly here is to make empty {% block footer_additional_js %}``{% endblock %} and{% block footer_additional_css %}`{% endblock %} blocks in footer at the bottom of layout.html and then "extend" (redefine) those blocks in the child templates that extends layout.html and which display the graphs (in our case coprs/templates/coprs/show.html) . That way the script and link directive will be included in footer in the final rendered page and won't block the page rendering in the middle (which is the case now).

Please make sure that we actually need to include those scripts and css files. There is alread /static/components/bootstrap/dist/js/bootstrap.min.js included on every page and also /static/js/patternfly.min.js is there (and also css/patternfly.min.css).

I have removed the comment in spec file.
I moved the chroot chart to the bottom and made the average line unchecked by default.

Cool, thank you!

Actually, the css files should be placed in header so better add {% block header_additional_css %}{% endblock %} to the header of layout.html and use that to include the additional files in child templates (that extend the layout).

There are two <head> tags now after this change. Is this change only about indentation? I can see you have also added {% block head %} and put the header script into that. I would personally prefer to have the block {% block head %} empty ( {% block head %}{% endblock %}) so that the scripts specified here in layout.html are always included on every page but at the same time that the child templates have an option to add additional scripts and css files by overriding the block. By the way, you may either call that block head or add two blocks instead head_additional_js and head_additional_css. That depends mostly on your taste and how you will do it in footer. (you can just have block footer instead of footer_additional_css and footer_additional_js).

Also here, you should just be 'appending' css files to header and js scripts to footer. For that the parent block head (the one defined in layout.html) needs to be empty. Make sure all the files you additionally include are minified.

Maybe also display the chroot1 graph when we already have data for it. I would like to see on status page as well.

Ideally, we would have just two graphs with filter that would enable us to see only tasks matching filter criteria but right now this is sufficient.

rebased onto 5e7c2d8ddc8529f195f79ee3dc96721f6787354f

6 years ago

I made the changes suggested in your comments. Also, I moved the js used for graphs into a separate file and removed the patternfly.js and patternfly.min.js as we now use a newer version provided by the xstatic-patternfly-common package.

rebased onto e4b14ed

6 years ago

In the last commit I fixed two typos (show.html used patternfly-additions.css instead of patternfly-additions.min.css and there was an unnecessary line $("#statistics-link").removeClass("hidden"); in graphs.js).

In the last commit I fixed two typos (show.html used patternfly-additions.css instead of patternfly-additions.min.css and there was an unnecessary line $("#statistics-link").removeClass("hidden"); in graphs.js).

Thanks!

Pull-Request has been merged by clime

6 years ago