449 lines
13 KiB
JavaScript
449 lines
13 KiB
JavaScript
"use strict";
|
|
/*global define, ajaxify, app, socket, utils, bootbox, RELATIVE_PATH*/
|
|
|
|
define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (semver, Chart, translator) {
|
|
var Admin = {};
|
|
var intervals = {
|
|
rooms: false,
|
|
graphs: false
|
|
};
|
|
var isMobile = false;
|
|
var isPrerelease = /^v?\d+\.\d+\.\d+-.+$/;
|
|
var graphData = {
|
|
rooms: {},
|
|
traffic: {}
|
|
};
|
|
var currentGraph = {
|
|
units: 'hours',
|
|
until: undefined
|
|
};
|
|
|
|
var DEFAULTS = {
|
|
roomInterval: 10000,
|
|
graphInterval: 15000,
|
|
realtimeInterval: 1500
|
|
};
|
|
|
|
$(window).on('action:ajaxify.start', function (ev, data) {
|
|
clearInterval(intervals.rooms);
|
|
clearInterval(intervals.graphs);
|
|
|
|
intervals.rooms = null;
|
|
intervals.graphs = null;
|
|
graphData.rooms = null;
|
|
graphData.traffic = null;
|
|
usedTopicColors.length = 0;
|
|
});
|
|
|
|
Admin.init = function () {
|
|
app.enterRoom('admin');
|
|
|
|
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
|
|
$.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;
|
|
}).filter(function (version) {
|
|
return !isPrerelease.test(version.name); // filter out automated prerelease versions
|
|
});
|
|
|
|
var version = $('#version').html(),
|
|
latestVersion = releases[0].name.slice(1),
|
|
checkEl = $('.version-check'),
|
|
text;
|
|
|
|
// Alter box colour accordingly
|
|
if (semver.eq(latestVersion, version)) {
|
|
checkEl.removeClass('alert-info').addClass('alert-success');
|
|
text = '[[admin/general/dashboard:up-to-date]]';
|
|
} else if (semver.gt(latestVersion, version)) {
|
|
checkEl.removeClass('alert-info').addClass('alert-warning');
|
|
if (!isPrerelease.test(version)) {
|
|
text = '[[admin/general/dashboard:upgrade-available, ' + latestVersion + ']]';
|
|
} else {
|
|
text = '[[admin/general/dashboard:prerelease-upgrade-available, ' + latestVersion + ']]';
|
|
}
|
|
} else if (isPrerelease.test(version)) {
|
|
checkEl.removeClass('alert-info').addClass('alert-info');
|
|
text = '[[admin/general/dashboard:prerelease-warning]]';
|
|
}
|
|
|
|
translator.translate(text, function (text) {
|
|
checkEl.append(text);
|
|
});
|
|
});
|
|
|
|
$('[data-toggle="tooltip"]').tooltip();
|
|
|
|
setupRealtimeButton();
|
|
setupGraphs();
|
|
};
|
|
|
|
Admin.updateRoomUsage = function (err, data) {
|
|
if (err) {
|
|
return app.alertError(err.message);
|
|
}
|
|
|
|
if (JSON.stringify(graphData.rooms) === JSON.stringify(data)) {
|
|
return;
|
|
}
|
|
|
|
graphData.rooms = data;
|
|
|
|
var html = '<div class="text-center pull-left">' +
|
|
'<span class="formatted-number">' + data.onlineRegisteredCount + '</span>' +
|
|
'<div class="stat">[[admin/general/dashboard:active-users.users]]</div>' +
|
|
'</div>' +
|
|
'<div class="text-center pull-left">' +
|
|
'<span class="formatted-number">' + data.onlineGuestCount + '</span>' +
|
|
'<div class="stat">[[admin/general/dashboard:active-users.guests]]</div>' +
|
|
'</div>' +
|
|
'<div class="text-center pull-left">' +
|
|
'<span class="formatted-number">' + (data.onlineRegisteredCount + data.onlineGuestCount) + '</span>' +
|
|
'<div class="stat">[[admin/general/dashboard:active-users.total]]</div>' +
|
|
'</div>' +
|
|
'<div class="text-center pull-left">' +
|
|
'<span class="formatted-number">' + data.socketCount + '</span>' +
|
|
'<div class="stat">[[admin/general/dashboard:active-users.connections]]</div>' +
|
|
'</div>';
|
|
|
|
updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount);
|
|
updatePresenceGraph(data.users);
|
|
updateTopicsGraph(data.topics);
|
|
|
|
$('#active-users').translateHtml(html);
|
|
};
|
|
|
|
var graphs = {
|
|
traffic: null,
|
|
registered: null,
|
|
presence: null,
|
|
topics: null
|
|
};
|
|
|
|
var topicColors = ["#bf616a","#5B90BF","#d08770","#ebcb8b","#a3be8c","#96b5b4","#8fa1b3","#b48ead","#ab7967","#46BFBD"];
|
|
var 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 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 = utils.getHoursArray();
|
|
|
|
if (isMobile) {
|
|
Chart.defaults.global.tooltips.enabled = false;
|
|
}
|
|
|
|
var t = translator.Translator.create();
|
|
Promise.all([
|
|
t.translateKey('admin/general/dashboard:graphs.page-views', []),
|
|
t.translateKey('admin/general/dashboard:graphs.unique-visitors', []),
|
|
t.translateKey('admin/general/dashboard:graphs.registered-users', []),
|
|
t.translateKey('admin/general/dashboard:graphs.anonymous-users', []),
|
|
t.translateKey('admin/general/dashboard:on-categories', []),
|
|
t.translateKey('admin/general/dashboard:reading-posts', []),
|
|
t.translateKey('admin/general/dashboard:browsing-topics', []),
|
|
t.translateKey('admin/general/dashboard:recent', []),
|
|
t.translateKey('admin/general/dashboard:unread', []),
|
|
]).then(function (translations) {
|
|
var data = {
|
|
labels: trafficLabels,
|
|
datasets: [
|
|
{
|
|
label: translations[0],
|
|
backgroundColor: "rgba(220,220,220,0.2)",
|
|
borderColor: "rgba(220,220,220,1)",
|
|
pointBackgroundColor: "rgba(220,220,220,1)",
|
|
pointHoverBackgroundColor: "#fff",
|
|
pointBorderColor: "#fff",
|
|
pointHoverBorderColor: "rgba(220,220,220,1)",
|
|
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]
|
|
},
|
|
{
|
|
label: translations[1],
|
|
backgroundColor: "rgba(151,187,205,0.2)",
|
|
borderColor: "rgba(151,187,205,1)",
|
|
pointBackgroundColor: "rgba(151,187,205,1)",
|
|
pointHoverBackgroundColor: "#fff",
|
|
pointBorderColor: "#fff",
|
|
pointHoverBorderColor: "rgba(151,187,205,1)",
|
|
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]
|
|
}
|
|
]
|
|
};
|
|
|
|
trafficCanvas.width = $(trafficCanvas).parent().width();
|
|
graphs.traffic = new Chart(trafficCtx, {
|
|
type: 'line',
|
|
data: data,
|
|
options: {
|
|
responsive: true,
|
|
legend: {
|
|
display: false
|
|
},
|
|
scales: {
|
|
yAxes: [{
|
|
ticks: {
|
|
beginAtZero: true
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
});
|
|
|
|
graphs.registered = new Chart(registeredCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: translations.slice(2, 4),
|
|
datasets: [{
|
|
data: [1, 1],
|
|
backgroundColor: ["#F7464A", "#46BFBD"],
|
|
hoverBackgroundColor: ["#FF5A5E", "#5AD3D1"]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
});
|
|
|
|
graphs.presence = new Chart(presenceCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: translations.slice(4, 9),
|
|
datasets: [{
|
|
data: [1, 1, 1, 1, 1],
|
|
backgroundColor: ["#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#9FB194"],
|
|
hoverBackgroundColor: ["#FF5A5E", "#5AD3D1", "#FFC870", "#A8B3C5", "#A8B3C5"]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
});
|
|
|
|
graphs.topics = new Chart(topicsCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: [],
|
|
hoverBackgroundColor: []
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
});
|
|
|
|
updateTrafficGraph();
|
|
|
|
$(window).on('resize', adjustPieCharts);
|
|
adjustPieCharts();
|
|
|
|
$('[data-action="updateGraph"]').on('click', function () {
|
|
var until;
|
|
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);
|
|
});
|
|
|
|
socket.emit('admin.rooms.getAll', Admin.updateRoomUsage);
|
|
initiateDashboard();
|
|
});
|
|
}
|
|
|
|
function adjustPieCharts() {
|
|
$('.pie-chart.legend-up').each(function () {
|
|
var $this = $(this);
|
|
|
|
if ($this.width() < 320) {
|
|
$this.addClass('compact');
|
|
} else {
|
|
$this.removeClass('compact');
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateTrafficGraph(units, until) {
|
|
if (!app.isFocused) {
|
|
return;
|
|
}
|
|
|
|
socket.emit('admin.analytics.get', {
|
|
graph: 'traffic',
|
|
units: units || 'hours',
|
|
until: until
|
|
}, function (err, data) {
|
|
if (err) {
|
|
return app.alertError(err.message);
|
|
}
|
|
if (JSON.stringify(graphData.traffic) === JSON.stringify(data)) {
|
|
return;
|
|
}
|
|
|
|
graphData.traffic = data;
|
|
|
|
if (units === 'days') {
|
|
graphs.traffic.data.xLabels = utils.getDaysArray(until);
|
|
} else {
|
|
graphs.traffic.data.xLabels = utils.getHoursArray();
|
|
|
|
$('#pageViewsThisMonth').html(data.monthlyPageViews.thisMonth);
|
|
$('#pageViewsLastMonth').html(data.monthlyPageViews.lastMonth);
|
|
$('#pageViewsPastDay').html(data.pastDay);
|
|
utils.addCommasToNumbers($('#pageViewsThisMonth'));
|
|
utils.addCommasToNumbers($('#pageViewsLastMonth'));
|
|
utils.addCommasToNumbers($('#pageViewsPastDay'));
|
|
}
|
|
|
|
graphs.traffic.data.datasets[0].data = data.pageviews;
|
|
graphs.traffic.data.datasets[1].data = data.uniqueVisitors;
|
|
graphs.traffic.data.labels = graphs.traffic.data.xLabels;
|
|
|
|
graphs.traffic.update();
|
|
currentGraph.units = units;
|
|
currentGraph.until = until;
|
|
});
|
|
}
|
|
|
|
function updateRegisteredGraph(registered, anonymous) {
|
|
graphs.registered.data.datasets[0].data[0] = registered;
|
|
graphs.registered.data.datasets[0].data[1] = anonymous;
|
|
graphs.registered.update();
|
|
}
|
|
|
|
function updatePresenceGraph(users) {
|
|
graphs.presence.data.datasets[0].data[0] = users.categories;
|
|
graphs.presence.data.datasets[0].data[1] = users.topics;
|
|
graphs.presence.data.datasets[0].data[2] = users.category;
|
|
graphs.presence.data.datasets[0].data[3] = users.recent;
|
|
graphs.presence.data.datasets[0].data[4] = users.unread;
|
|
|
|
graphs.presence.update();
|
|
}
|
|
|
|
function updateTopicsGraph(topics) {
|
|
if (!Object.keys(topics).length) {
|
|
topics = {"0": {
|
|
title: "No users browsing",
|
|
value: 1
|
|
}};
|
|
}
|
|
|
|
var tids = Object.keys(topics);
|
|
|
|
graphs.topics.data.labels = [];
|
|
graphs.topics.data.datasets[0].data = [];
|
|
graphs.topics.data.datasets[0].backgroundColor = [];
|
|
graphs.topics.data.datasets[0].hoverBackgroundColor = [];
|
|
|
|
for (var i = 0, ii = tids.length; i < ii; i++) {
|
|
graphs.topics.data.labels.push(topics[tids[i]].title);
|
|
graphs.topics.data.datasets[0].data.push(topics[tids[i]].value);
|
|
graphs.topics.data.datasets[0].backgroundColor.push(topicColors[i]);
|
|
graphs.topics.data.datasets[0].hoverBackgroundColor.push(lighten(topicColors[i], 10));
|
|
}
|
|
|
|
function buildTopicsLegend() {
|
|
var legend = $('#topics-legend').html('');
|
|
|
|
for (var i = 0, ii = tids.length; i < ii; i++) {
|
|
var topic = topics[tids[i]];
|
|
var label = topic.value === '0' ? topic.title : '<a title="' + topic.title + '"href="' + RELATIVE_PATH + '/topic/' + tids[i] + '" target="_blank"> ' + topic.title + '</a>';
|
|
|
|
legend.append(
|
|
'<li>' +
|
|
'<div style="background-color: ' + topicColors[i] + ';"></div>' +
|
|
'<span>' + label + '</span>' +
|
|
'</li>');
|
|
}
|
|
}
|
|
|
|
buildTopicsLegend();
|
|
graphs.topics.update();
|
|
}
|
|
|
|
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) {
|
|
socket.emit('admin.rooms.getAll', Admin.updateRoomUsage);
|
|
}
|
|
}, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.roomInterval);
|
|
|
|
intervals.graphs = setInterval(function () {
|
|
updateTrafficGraph(currentGraph.units, currentGraph.until);
|
|
}, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval);
|
|
}
|
|
|
|
return Admin;
|
|
});
|