2014-10-08 15:36:47 -04:00
"use strict" ;
2015-02-25 19:03:32 -05:00
/*global define, ajaxify, app, socket, utils, bootbox, Chart, RELATIVE_PATH*/
2014-10-08 15:36:47 -04:00
2014-10-08 16:08:35 -04:00
define ( 'admin/general/dashboard' , [ 'semver' ] , function ( semver ) {
2014-10-08 15:36:47 -04:00
var Admin = { } ,
intervals = {
rooms : false ,
graphs : false
} ,
2015-07-08 16:54:39 -04:00
isMobile = false ,
graphData = {
rooms : { } ,
traffic : { }
2015-11-25 14:26:38 -05:00
} ,
2015-11-25 14:48:32 -05:00
currentGraph = {
units : 'hours' ,
until : undefined
} ;
2014-10-08 15:36:47 -04:00
2015-07-08 16:35:59 -04:00
var DEFAULTS = {
roomInterval : 10000 ,
graphInterval : 15000 ,
realtimeInterval : 1500
} ;
2014-10-08 15:36:47 -04:00
Admin . init = function ( ) {
app . enterRoom ( 'admin' ) ;
2015-11-04 17:43:43 -05:00
socket . emit ( 'admin.rooms.getAll' , Admin . updateRoomUsage ) ;
2014-10-08 15:36:47 -04:00
2014-10-17 23:14:17 -04:00
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i . test ( navigator . userAgent ) ;
2014-10-08 15:36:47 -04:00
$ ( window ) . on ( 'action:ajaxify.start' , function ( ev , data ) {
clearInterval ( intervals . rooms ) ;
clearInterval ( intervals . graphs ) ;
intervals . rooms = null ;
intervals . graphs = null ;
2015-09-27 13:42:36 -04:00
graphData . rooms = null ;
graphData . traffic = null ;
2014-10-31 20:30:07 -04:00
usedTopicColors . length = 0 ;
2014-10-08 15:36:47 -04:00
} ) ;
$ . get ( 'https://api.github.com/repos/NodeBB/NodeBB/tags' , function ( releases ) {
// Re-sort the releases, as they do not follow Semver (wrt pre-releases)
releases = releases . sort ( function ( a , b ) {
a = a . name . replace ( /^v/ , '' ) ;
b = b . name . replace ( /^v/ , '' ) ;
return semver . lt ( a , b ) ? 1 : - 1 ;
} ) ;
var version = $ ( '#version' ) . html ( ) ,
latestVersion = releases [ 0 ] . name . slice ( 1 ) ,
checkEl = $ ( '.version-check' ) ;
checkEl . html ( $ ( '.version-check' ) . html ( ) . replace ( '<i class="fa fa-spinner fa-spin"></i>' , 'v' + latestVersion ) ) ;
// Alter box colour accordingly
if ( semver . eq ( latestVersion , version ) ) {
checkEl . removeClass ( 'alert-info' ) . addClass ( 'alert-success' ) ;
checkEl . append ( '<p>You are <strong>up-to-date</strong> <i class="fa fa-check"></i></p>' ) ;
} else if ( semver . gt ( latestVersion , version ) ) {
checkEl . removeClass ( 'alert-info' ) . addClass ( 'alert-danger' ) ;
2015-11-28 23:26:51 -05:00
checkEl . append ( '<p>A new version (v' + latestVersion + ') has been released. Consider <a href="https://docs.nodebb.org/en/latest/upgrading/index.html">upgrading your NodeBB</a>.</p>' ) ;
2014-10-08 15:36:47 -04:00
}
} ) ;
2015-05-29 16:47:12 -04:00
$ ( '[data-toggle="tooltip"]' ) . tooltip ( ) ;
2015-07-14 17:03:28 -04:00
2015-07-08 16:35:59 -04:00
setupRealtimeButton ( ) ;
setupGraphs ( ) ;
initiateDashboard ( ) ;
2014-10-08 15:36:47 -04:00
} ;
Admin . updateRoomUsage = function ( err , data ) {
if ( err ) {
return app . alertError ( err . message ) ;
}
2015-07-08 16:54:39 -04:00
if ( JSON . stringify ( graphData . rooms ) === JSON . stringify ( data ) ) {
return ;
}
graphData . rooms = data ;
2014-10-17 23:04:50 -04:00
var html = '<div class="text-center pull-left">' +
'<div>' + data . onlineRegisteredCount + '</div>' +
2014-10-08 15:36:47 -04:00
'<div>Users</div>' +
'</div>' +
2014-10-17 23:04:50 -04:00
'<div class="text-center pull-left">' +
'<div>' + data . onlineGuestCount + '</div>' +
2014-10-08 15:36:47 -04:00
'<div>Guests</div>' +
'</div>' +
2014-10-17 23:04:50 -04:00
'<div class="text-center pull-left">' +
'<div>' + ( data . onlineRegisteredCount + data . onlineGuestCount ) + '</div>' +
2014-10-08 15:36:47 -04:00
'<div>Total</div>' +
'</div>' +
2014-10-17 23:04:50 -04:00
'<div class="text-center pull-left">' +
'<div>' + data . socketCount + '</div>' +
2014-10-08 15:36:47 -04:00
'<div>Connections</div>' +
'</div>' ;
updateRegisteredGraph ( data . onlineRegisteredCount , data . onlineGuestCount ) ;
2015-03-26 16:32:17 -04:00
updatePresenceGraph ( data . users ) ;
2014-10-08 15:36:47 -04:00
updateTopicsGraph ( data . topics ) ;
$ ( '#active-users' ) . html ( html ) ;
} ;
var graphs = {
traffic : null ,
registered : null ,
presence : null ,
topics : null
} ;
var topicColors = [ "#bf616a" , "#5B90BF" , "#d08770" , "#ebcb8b" , "#a3be8c" , "#96b5b4" , "#8fa1b3" , "#b48ead" , "#ab7967" , "#46BFBD" ] ,
usedTopicColors = [ ] ;
// from chartjs.org
function lighten ( col , amt ) {
var usePound = false ;
if ( col [ 0 ] == "#" ) {
col = col . slice ( 1 ) ;
usePound = true ;
}
var num = parseInt ( col , 16 ) ;
var r = ( num >> 16 ) + amt ;
if ( r > 255 ) r = 255 ;
else if ( r < 0 ) r = 0 ;
var b = ( ( num >> 8 ) & 0x00FF ) + amt ;
if ( b > 255 ) b = 255 ;
else if ( b < 0 ) b = 0 ;
var g = ( num & 0x0000FF ) + amt ;
if ( g > 255 ) g = 255 ;
else if ( g < 0 ) g = 0 ;
return ( usePound ? "#" : "" ) + ( g | ( b << 8 ) | ( r << 16 ) ) . toString ( 16 ) ;
}
function getHoursArray ( ) {
var currentHour = new Date ( ) . getHours ( ) ,
labels = [ ] ;
2014-12-20 20:34:46 -05:00
for ( var i = currentHour , ii = currentHour - 24 ; i > ii ; i -- ) {
2014-10-08 15:36:47 -04:00
var hour = i < 0 ? 24 + i : i ;
2015-11-25 14:22:32 -05:00
labels . push ( hour + ':00' ) ;
2014-10-08 15:36:47 -04:00
}
return labels . reverse ( ) ;
}
2015-11-25 14:48:32 -05:00
function getDaysArray ( from ) {
var currentDay = new Date ( from || Date . now ( ) ) . getTime ( ) ,
2015-11-25 14:22:32 -05:00
months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ,
labels = [ ] ,
tmpDate ;
for ( var x = 29 ; x >= 0 ; x -- ) {
tmpDate = new Date ( currentDay - ( 1000 * 60 * 60 * 24 * x ) ) ;
labels . push ( months [ tmpDate . getMonth ( ) ] + ' ' + tmpDate . getDate ( ) ) ;
}
return labels ;
}
2014-10-08 15:36:47 -04:00
function setupGraphs ( ) {
var trafficCanvas = document . getElementById ( 'analytics-traffic' ) ,
registeredCanvas = document . getElementById ( 'analytics-registered' ) ,
presenceCanvas = document . getElementById ( 'analytics-presence' ) ,
topicsCanvas = document . getElementById ( 'analytics-topics' ) ,
trafficCtx = trafficCanvas . getContext ( '2d' ) ,
registeredCtx = registeredCanvas . getContext ( '2d' ) ,
presenceCtx = presenceCanvas . getContext ( '2d' ) ,
topicsCtx = topicsCanvas . getContext ( '2d' ) ,
trafficLabels = getHoursArray ( ) ;
if ( isMobile ) {
Chart . defaults . global . showTooltips = false ;
}
var data = {
labels : trafficLabels ,
datasets : [
{
label : "Page Views" ,
fillColor : "rgba(220,220,220,0.2)" ,
strokeColor : "rgba(220,220,220,1)" ,
pointColor : "rgba(220,220,220,1)" ,
pointStrokeColor : "#fff" ,
pointHighlightFill : "#fff" ,
pointHighlightStroke : "rgba(220,220,220,1)" ,
2014-12-20 20:34:46 -05:00
data : [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]
2014-10-08 15:36:47 -04:00
} ,
{
label : "Unique Visitors" ,
fillColor : "rgba(151,187,205,0.2)" ,
strokeColor : "rgba(151,187,205,1)" ,
pointColor : "rgba(151,187,205,1)" ,
pointStrokeColor : "#fff" ,
pointHighlightFill : "#fff" ,
pointHighlightStroke : "rgba(151,187,205,1)" ,
2014-12-20 20:34:46 -05:00
data : [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]
2014-10-08 15:36:47 -04:00
}
]
} ;
trafficCanvas . width = $ ( trafficCanvas ) . parent ( ) . width ( ) ; // is this necessary
graphs . traffic = new Chart ( trafficCtx ) . Line ( data , {
responsive : true
} ) ;
graphs . registered = new Chart ( registeredCtx ) . Doughnut ( [ {
2015-03-26 16:32:17 -04:00
value : 1 ,
color : "#F7464A" ,
highlight : "#FF5A5E" ,
label : "Registered Users"
} ,
{
value : 1 ,
color : "#46BFBD" ,
highlight : "#5AD3D1" ,
label : "Anonymous Users"
} ] , {
responsive : true
} ) ;
2014-10-08 15:36:47 -04:00
graphs . presence = new Chart ( presenceCtx ) . Doughnut ( [ {
2015-03-26 16:32:17 -04:00
value : 1 ,
color : "#F7464A" ,
highlight : "#FF5A5E" ,
label : "On categories list"
} ,
{
value : 1 ,
color : "#46BFBD" ,
highlight : "#5AD3D1" ,
label : "Reading posts"
} ,
{
value : 1 ,
color : "#FDB45C" ,
highlight : "#FFC870" ,
label : "Browsing topics"
} ,
{
value : 1 ,
color : "#949FB1" ,
highlight : "#A8B3C5" ,
2015-07-14 17:03:28 -04:00
label : "Recent"
2015-07-08 17:02:33 -04:00
} ,
{
value : 1 ,
2015-07-14 17:03:28 -04:00
color : "#9FB194" ,
2015-07-08 17:02:33 -04:00
highlight : "#A8B3C5" ,
2015-07-14 17:03:28 -04:00
label : "Unread"
2015-04-10 16:45:37 -04:00
}
] , {
2015-03-26 16:32:17 -04:00
responsive : true
} ) ;
2014-10-08 15:36:47 -04:00
graphs . topics = new Chart ( topicsCtx ) . Doughnut ( [ ] , { responsive : true } ) ;
topicsCanvas . onclick = function ( evt ) {
var obj = graphs . topics . getSegmentsAtEvent ( evt ) ;
2014-10-19 18:37:11 -04:00
if ( obj && obj [ 0 ] ) {
window . open ( RELATIVE _PATH + '/topic/' + obj [ 0 ] . tid ) ;
}
2014-10-08 15:36:47 -04:00
} ;
updateTrafficGraph ( ) ;
$ ( window ) . on ( 'resize' , adjustPieCharts ) ;
adjustPieCharts ( ) ;
2015-11-25 14:22:32 -05:00
$ ( '[data-action="updateGraph"]' ) . on ( 'click' , function ( ) {
2015-11-25 14:48:32 -05:00
var until = undefined ;
switch ( $ ( this ) . attr ( 'data-until' ) ) {
case 'last-month' :
var lastMonth = new Date ( ) ;
lastMonth . setDate ( lastMonth . getDate ( ) - 30 ) ;
until = lastMonth . getTime ( ) ;
}
updateTrafficGraph ( $ ( this ) . attr ( 'data-units' ) , until ) ;
} ) ;
2014-10-08 15:36:47 -04:00
}
function adjustPieCharts ( ) {
$ ( '.pie-chart.legend-up' ) . each ( function ( ) {
var $this = $ ( this ) ;
if ( $this . width ( ) < 320 ) {
$this . addClass ( 'compact' ) ;
} else {
$this . removeClass ( 'compact' ) ;
}
} ) ;
}
2015-11-25 14:48:32 -05:00
function updateTrafficGraph ( units , until ) {
2014-10-19 16:13:10 -04:00
if ( ! app . isFocused ) {
2014-10-08 15:36:47 -04:00
return ;
}
2015-11-25 14:22:32 -05:00
socket . emit ( 'admin.analytics.get' , {
graph : 'traffic' ,
2015-11-25 14:48:32 -05:00
units : units || 'hours' ,
until : until
2015-11-25 14:22:32 -05:00
} , function ( err , data ) {
2015-07-08 16:54:39 -04:00
if ( JSON . stringify ( graphData . traffic ) === JSON . stringify ( data ) ) {
return ;
}
graphData . traffic = data ;
2015-11-25 14:22:32 -05:00
// If new data set contains fewer points than currently shown, truncate
while ( graphs . traffic . datasets [ 0 ] . points . length > data . pageviews . length ) {
graphs . traffic . removeData ( ) ;
}
if ( units === 'days' ) {
2015-11-25 14:48:32 -05:00
graphs . traffic . scale . xLabels = getDaysArray ( until ) ;
2015-11-25 14:22:32 -05:00
} else {
graphs . traffic . scale . xLabels = getHoursArray ( ) ;
2015-11-25 16:44:52 -05:00
$ ( '#pageViewsThisMonth' ) . html ( data . monthlyPageViews . thisMonth ) ;
$ ( '#pageViewsLastMonth' ) . html ( data . monthlyPageViews . lastMonth ) ;
$ ( '#pageViewsPastDay' ) . html ( data . pastDay ) ;
utils . addCommasToNumbers ( $ ( '#pageViewsThisMonth' ) ) ;
utils . addCommasToNumbers ( $ ( '#pageViewsLastMonth' ) ) ;
utils . addCommasToNumbers ( $ ( '#pageViewsPastDay' ) ) ;
2014-10-08 15:36:47 -04:00
}
2015-11-25 14:22:32 -05:00
for ( var i = 0 , ii = data . pageviews . length ; i < ii ; i ++ ) {
if ( graphs . traffic . datasets [ 0 ] . points [ i ] ) {
graphs . traffic . datasets [ 0 ] . points [ i ] . value = data . pageviews [ i ] ;
graphs . traffic . datasets [ 0 ] . points [ i ] . label = graphs . traffic . scale . xLabels [ i ] ;
graphs . traffic . datasets [ 1 ] . points [ i ] . value = data . uniqueVisitors [ i ] ;
graphs . traffic . datasets [ 1 ] . points [ i ] . label = graphs . traffic . scale . xLabels [ i ] ;
} else {
// No points to replace? Add data.
graphs . traffic . addData ( [ data . pageviews [ i ] , data . uniqueVisitors [ i ] ] , graphs . traffic . scale . xLabels [ i ] ) ;
}
}
2014-10-08 15:36:47 -04:00
graphs . traffic . update ( ) ;
2015-11-25 14:48:32 -05:00
currentGraph . units = units ;
currentGraph . until = until ;
2014-10-08 15:36:47 -04:00
} ) ;
}
function updateRegisteredGraph ( registered , anonymous ) {
graphs . registered . segments [ 0 ] . value = registered ;
graphs . registered . segments [ 1 ] . value = anonymous ;
graphs . registered . update ( ) ;
}
2015-03-26 16:32:17 -04:00
function updatePresenceGraph ( users ) {
graphs . presence . segments [ 0 ] . value = users . categories ;
graphs . presence . segments [ 1 ] . value = users . topics ;
graphs . presence . segments [ 2 ] . value = users . category ;
graphs . presence . segments [ 3 ] . value = users . recent ;
2015-07-14 17:03:28 -04:00
graphs . presence . segments [ 4 ] . value = users . unread ;
2015-03-26 16:32:17 -04:00
2014-10-08 15:36:47 -04:00
graphs . presence . update ( ) ;
}
function updateTopicsGraph ( topics ) {
if ( ! Object . keys ( topics ) . length ) {
topics = { "0" : {
title : "No users browsing" ,
value : 1
} } ;
}
var tids = Object . keys ( topics ) ,
segments = graphs . topics . segments ;
function reassignExistingTopics ( ) {
2014-10-17 23:04:50 -04:00
for ( var i = segments . length - 1 ; i >= 0 ; i -- ) {
2014-10-08 15:36:47 -04:00
if ( ! segments [ i ] ) {
continue ;
}
var tid = segments [ i ] . tid ;
if ( $ . inArray ( tid , tids ) === - 1 ) {
2014-10-19 15:59:33 -04:00
usedTopicColors . splice ( $ . inArray ( segments [ i ] . fillColor , usedTopicColors ) , 1 ) ;
2014-10-08 15:36:47 -04:00
graphs . topics . removeData ( i ) ;
} else {
graphs . topics . segments [ i ] . value = topics [ tid ] . value ;
delete topics [ tid ] ;
}
}
}
function assignNewTopics ( ) {
while ( segments . length < 10 && tids . length > 0 ) {
var tid = tids . pop ( ) ,
data = topics [ tid ] ,
color = null ;
if ( ! data ) {
continue ;
}
if ( tid === '0' ) {
color = '#4D5360' ;
} else {
do {
for ( var i = 0 , ii = topicColors . length ; i < ii ; i ++ ) {
var chosenColor = topicColors [ i ] ;
if ( $ . inArray ( chosenColor , usedTopicColors ) === - 1 ) {
color = chosenColor ;
usedTopicColors . push ( color ) ;
break ;
}
}
} while ( color === null && usedTopicColors . length < topicColors . length ) ;
}
if ( color ) {
graphs . topics . addData ( {
value : data . value ,
color : color ,
highlight : lighten ( color , 10 ) ,
label : data . title
} ) ;
segments [ segments . length - 1 ] . tid = tid ;
}
}
}
function buildTopicsLegend ( ) {
var legend = $ ( '#topics-legend' ) . html ( '' ) ;
2015-03-25 22:33:14 -04:00
segments . sort ( function ( a , b ) {
return b . value - a . value ;
} ) ;
2014-10-08 15:36:47 -04:00
for ( var i = 0 , ii = segments . length ; i < ii ; i ++ ) {
var topic = segments [ i ] ,
label = topic . tid === '0' ? topic . label : '<a title="' + topic . label + '"href="' + RELATIVE _PATH + '/topic/' + topic . tid + '" target="_blank"> ' + topic . label + '</a>' ;
legend . append (
'<li>' +
'<div style="background-color: ' + topic . highlightColor + '; border-color: ' + topic . strokeColor + '"></div>' +
'<span>' + label + '</span>' +
'</li>' ) ;
}
}
reassignExistingTopics ( ) ;
assignNewTopics ( ) ;
buildTopicsLegend ( ) ;
graphs . topics . update ( ) ;
}
2015-07-08 16:35:59 -04:00
function setupRealtimeButton ( ) {
$ ( '#toggle-realtime .fa' ) . on ( 'click' , function ( ) {
var $this = $ ( this ) ;
if ( $this . hasClass ( 'fa-toggle-on' ) ) {
$this . removeClass ( 'fa-toggle-on' ) . addClass ( 'fa-toggle-off' ) ;
$this . parent ( ) . find ( 'strong' ) . html ( 'OFF' ) ;
initiateDashboard ( false ) ;
} else {
$this . removeClass ( 'fa-toggle-off' ) . addClass ( 'fa-toggle-on' ) ;
$this . parent ( ) . find ( 'strong' ) . html ( 'ON' ) ;
initiateDashboard ( true ) ;
}
} ) ;
}
function initiateDashboard ( realtime ) {
clearInterval ( intervals . rooms ) ;
clearInterval ( intervals . graphs ) ;
intervals . rooms = setInterval ( function ( ) {
if ( app . isFocused && app . isConnected ) {
2015-11-04 17:43:43 -05:00
socket . emit ( 'admin.rooms.getAll' , Admin . updateRoomUsage ) ;
2015-07-08 16:35:59 -04:00
}
} , realtime ? DEFAULTS . realtimeInterval : DEFAULTS . roomInterval ) ;
2015-11-25 14:48:32 -05:00
intervals . graphs = setInterval ( function ( ) {
updateTrafficGraph ( currentGraph . units , currentGraph . until ) ;
} , realtime ? DEFAULTS . realtimeInterval : DEFAULTS . graphInterval ) ;
2015-07-08 16:35:59 -04:00
}
2014-10-08 15:36:47 -04:00
return Admin ;
} ) ;