From 2a4e9bc94bb8401b791582fd4a12d3375177e466 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 11 Apr 2018 13:43:10 +0200 Subject: [PATCH] Add live graphs for active users and submission volume --- app/assets/javascripts/statistics_graphs.js | 104 ++++++++++++++++++++ app/assets/stylesheets/statistics.css.scss | 31 ++++++ app/controllers/statistics_controller.rb | 8 ++ app/helpers/statistics_helper.rb | 18 ++++ app/policies/statistics_policy.rb | 5 + app/views/statistics/graphs.html.slim | 13 +++ config/locales/de.yml | 2 + config/locales/en.yml | 2 + config/routes.rb | 1 + 9 files changed, 184 insertions(+) create mode 100644 app/assets/javascripts/statistics_graphs.js create mode 100644 app/views/statistics/graphs.html.slim diff --git a/app/assets/javascripts/statistics_graphs.js b/app/assets/javascripts/statistics_graphs.js new file mode 100644 index 00000000..3dbf79b8 --- /dev/null +++ b/app/assets/javascripts/statistics_graphs.js @@ -0,0 +1,104 @@ +$(document).ready(function () { + if ($.isController('statistics') && $('.graph#user-activity').isPresent()) { + var CHART_START = window.vis ? vis.moment().add(-1, 'minute') : undefined; + var DEFAULT_REFRESH_INTERVAL = 10000; + + var refreshInterval; + + var initialData; + var dataset; + var graph; + var groups; + + var buildChartGroups = function() { + initialData = initialData.sort(function (a, b) {return a.data - b.data}); + return _.map(initialData, function(element, index) { + return { + content: element.name + (element.unit ? ' [' + element.unit + ']' : ''), + id: element.key, + visible: false, + options: { + yAxisOrientation: index >= initialData.length / 2 ? 'right' : 'left' + } + }; + }); + }; + + var initializeChart = function() { + dataset = new vis.DataSet(); + groups = new vis.DataSet(buildChartGroups()); + graph = new vis.Graph2d(document.getElementById('user-activity'), dataset, groups, { + dataAxis: { + customRange: { + left: { + min: 0 + }, + right: { + min: 0 + } + }, + showMinorLabels: true + }, + drawPoints: { + style: 'circle' + }, + end: vis.moment(), + legend: true, + shaded: true, + start: CHART_START + }); + }; + + var refreshChart = function() { + var now = vis.moment(); + var window = graph.getWindow(); + var interval = window.end - window.start; + graph.setWindow(now - interval, now); + }; + + var refreshData = function(callback) { + if (! ($.isController('statistics') && $('#user-activity').isPresent())) { + clearInterval(refreshInterval); + } else { + var jqxhr = $.ajax({ + dataType: 'json', + method: 'GET' + }); + jqxhr.done(function(response) { + (callback || _.noop)(response); + setGroupVisibility(response); + updateChartData(response); + requestAnimationFrame(refreshChart); + }); + } + }; + + var setGroupVisibility = function(response) { + _.each(response, function(data) { + groups.update({ + id: data.key, + visible: true + }); + }); + }; + + var updateChartData = function(response) { + _.each(response, function(data) { + dataset.add({ + group: data.key, + x: vis.moment(), + y: data.data + }); + }); + }; + + refreshData(function (data) { + initialData = data; + $('#user-activity').parent().find('.spinner').hide(); + initializeChart(); + + var refresh_interval = location.search.match(/interval=(\d+)/) ? parseInt(RegExp.$1) : DEFAULT_REFRESH_INTERVAL; + refreshInterval = setInterval(refreshData, refresh_interval); + }); + } +}); diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index 9ce3d61f..9fcc81cc 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -101,3 +101,34 @@ div.negative-result { } } } + +.spinner { + width: 40px; + height: 40px; + background-color: #333; + + margin: 100px auto; + -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out; + animation: sk-rotateplane 1.2s infinite ease-in-out; +} + +@-webkit-keyframes sk-rotateplane { + 0% { -webkit-transform: perspective(120px) } + 50% { -webkit-transform: perspective(120px) rotateY(180deg) } + 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) } +} + +@keyframes sk-rotateplane { + 0% { + transform: perspective(120px) rotateX(0deg) rotateY(0deg); + -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg) + } + 50% { + transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); + -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) + } + 100% { + transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); + -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); + } +} diff --git a/app/controllers/statistics_controller.rb b/app/controllers/statistics_controller.rb index a26d5670..448fa1d8 100644 --- a/app/controllers/statistics_controller.rb +++ b/app/controllers/statistics_controller.rb @@ -13,4 +13,12 @@ class StatisticsController < ApplicationController end end + def graphs + authorize self + respond_to do |format| + format.html + format.json { render(json: graph_live_data) } + end + end + end diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index bd597cc1..a740f3a0 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -122,4 +122,22 @@ module StatisticsHelper ] end + def graph_live_data + [ + { + key: 'active_in_last_hour', + name: t('statistics.entries.users.currently_active'), + data: ExternalUser.joins(:submissions) + .where(['submissions.created_at >= ?', DateTime.now - 5.minutes]) + .distinct('external_users.id').count, + }, + { + key: 'submissions_per_minute', + name: t('statistics.entries.exercises.submissions_per_minute'), + data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2), + unit: '/min' + } + ] + end + end diff --git a/app/policies/statistics_policy.rb b/app/policies/statistics_policy.rb index 36ca2b51..2cfdc301 100644 --- a/app/policies/statistics_policy.rb +++ b/app/policies/statistics_policy.rb @@ -1,2 +1,7 @@ class StatisticsPolicy < AdminOnlyPolicy + + def graphs? + admin? + end + end diff --git a/app/views/statistics/graphs.html.slim b/app/views/statistics/graphs.html.slim new file mode 100644 index 00000000..c2627c1f --- /dev/null +++ b/app/views/statistics/graphs.html.slim @@ -0,0 +1,13 @@ +- content_for :head do + = javascript_include_tag(asset_path('vis.min.js', type: :javascript)) + = stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet)) + +.group + h1 User Activity + .spinner + .graph#user-activity + +.group + h1 RFC Activity + .spinner + .graph#rfc-activity diff --git a/config/locales/de.yml b/config/locales/de.yml index 05c7108e..4c5066fb 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -221,6 +221,7 @@ de: destroy_through_lti: Code-Abgabe statistics: show: "Statistiken" + graphs: "Visualisierungen" consumers: show: link: Konsument @@ -755,3 +756,4 @@ de: with_comments: "Anfragen mit Kommentaren" users: currently_active: "Aktiv (5 Minuten)" + currently_active60: "Aktiv (60 Minuten)" diff --git a/config/locales/en.yml b/config/locales/en.yml index 5d6ada43..6ccc1477 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -221,6 +221,7 @@ en: destroy_through_lti: Code Submission statistics: show: "Statistics" + graphs: "Graphs" consumers: show: link: Consumer @@ -755,3 +756,4 @@ en: with_comments: "RfCs with Comments" users: currently_active: "Active (5 minutes)" + currently_active60: "Active (60 minutes)" diff --git a/config/routes.rb b/config/routes.rb index 399bee40..4da4a0ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,6 +43,7 @@ Rails.application.routes.draw do get '/help', to: 'application#help' get 'statistics/', to: 'statistics#show' + get 'statistics/graphs', to: 'statistics#graphs' concern :statistics do member do