refactor: abstract out some client side dashboard code into modules, analytics subpages for users, topics, and logins

v1.18.x
Julian Lam 4 years ago
parent 079a13d41a
commit f561799f74

@ -1,5 +1,10 @@
{
"dashboard": "Dashboard",
"section-dashboard": "Dashboard",
"dashboard/logins": "Logins",
"dashboard/users": "Users",
"dashboard/topics": "Topics",
"section-general": "General",
"section-manage": "Manage",

@ -0,0 +1,14 @@
'use strict';
define('admin/dashboard/logins', ['admin/modules/dashboard-line-graph'], (graph) => {
const ACP = {};
ACP.init = () => {
graph.init({
set: 'logins',
dataset: ajaxify.data.dataset,
});
};
return ACP;
});

@ -0,0 +1,14 @@
'use strict';
define('admin/dashboard/topics', ['admin/modules/dashboard-line-graph'], (graph) => {
const ACP = {};
ACP.init = () => {
graph.init({
set: 'topics',
dataset: ajaxify.data.dataset,
});
};
return ACP;
});

@ -0,0 +1,14 @@
'use strict';
define('admin/dashboard/users', ['admin/modules/dashboard-line-graph'], (graph) => {
const ACP = {};
ACP.init = () => {
graph.init({
set: 'registrations',
dataset: ajaxify.data.dataset,
});
};
return ACP;
});

@ -0,0 +1,177 @@
'use strict';
define('admin/modules/dashboard-line-graph', ['Chart', 'translator', 'benchpress', 'api'], function (Chart, translator, Benchpress, api) {
const Graph = {
_current: null,
};
let isMobile = false;
Graph.init = ({ set, dataset }) => {
const canvas = document.getElementById('analytics-traffic');
const canvasCtx = canvas.getContext('2d');
const trafficLabels = utils.getHoursArray();
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
Chart.defaults.global.tooltips.enabled = false;
}
var t = translator.Translator.create();
t.translateKey(`admin/menu:${ajaxify.data.template.name.replace('admin/', '')}`, []).then((key) => {
const data = {
labels: trafficLabels,
datasets: [
{
label: key,
backgroundColor: 'rgba(151,187,205,0.2)',
borderColor: 'rgba(151,187,205,1)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointHoverBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: '#fff',
pointHoverBorderColor: 'rgba(151,187,205,1)',
data: dataset,
},
],
};
canvas.width = $(canvas).parent().width();
data.datasets[0].yAxisID = 'left-y-axis';
Graph._current = new Chart(canvasCtx, {
type: 'line',
data: data,
options: {
responsive: true,
legend: {
display: true,
},
scales: {
yAxes: [{
id: 'left-y-axis',
ticks: {
beginAtZero: true,
precision: 0,
},
type: 'linear',
position: 'left',
scaleLabel: {
display: true,
labelString: key,
},
}],
},
tooltips: {
mode: 'x',
},
},
});
});
$('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () {
var until = new Date();
var amount = $(this).attr('data-amount');
if ($(this).attr('data-units') === 'days') {
until.setHours(0, 0, 0, 0);
}
until = until.getTime();
Graph.update(set, $(this).attr('data-units'), until, amount);
$('[data-action="updateGraph"]').removeClass('active');
$(this).addClass('active');
require(['translator'], function (translator) {
translator.translate('[[admin/dashboard:page-views-custom]]', function (translated) {
$('[data-action="updateGraph"][data-units="custom"]').text(translated);
});
});
});
$('[data-action="updateGraph"][data-units="custom"]').on('click', function () {
var targetEl = $(this);
Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) {
var modal = bootbox.dialog({
title: '[[admin/dashboard:page-views-custom]]',
message: html,
buttons: {
submit: {
label: '[[global:search]]',
className: 'btn-primary',
callback: submit,
},
},
}).on('shown.bs.modal', function () {
var date = new Date();
var today = date.toISOString().substr(0, 10);
date.setDate(date.getDate() - 1);
var yesterday = date.toISOString().substr(0, 10);
modal.find('#startRange').val(targetEl.attr('data-startRange') || yesterday);
modal.find('#endRange').val(targetEl.attr('data-endRange') || today);
});
function submit() {
// NEED TO ADD VALIDATION HERE FOR YYYY-MM-DD
var formData = modal.find('form').serializeObject();
var validRegexp = /\d{4}-\d{2}-\d{2}/;
// Input validation
if (!formData.startRange && !formData.endRange) {
// No range? Assume last 30 days
Graph.update(set, 'days');
$('[data-action="updateGraph"]').removeClass('active');
$('[data-action="updateGraph"][data-units="days"]').addClass('active');
return;
} else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) {
// Invalid Input
modal.find('.alert-danger').removeClass('hidden');
return false;
}
var until = new Date(formData.endRange);
until.setDate(until.getDate() + 1);
until = until.getTime();
var amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24);
Graph.update(set, 'days', until, amount);
$('[data-action="updateGraph"]').removeClass('active');
targetEl.addClass('active');
// Update "custom range" label
targetEl.attr('data-startRange', formData.startRange);
targetEl.attr('data-endRange', formData.endRange);
targetEl.html(formData.startRange + ' – ' + formData.endRange);
}
});
});
};
Graph.update = (set, units, until, amount) => {
if (!Graph._current) {
return;
}
api.get(`/admin/analytics/${set}`, { units, until, amount }).then((dataset) => {
if (units === 'days') {
Graph._current.data.xLabels = utils.getDaysArray(until, amount);
} else {
Graph._current.data.xLabels = utils.getHoursArray();
}
Graph._current.data.datasets[0].data = dataset;
Graph._current.data.labels = Graph._current.data.xLabels;
Graph._current.update();
// Update the View as JSON button url
var apiEl = $('#view-as-json');
var newHref = $.param({
units: units || 'hours',
until: until,
count: amount,
});
apiEl.attr('href', `${config.relative_path}/api/v3/admin/analytics/${ajaxify.data.set}?${newHref}`);
});
};
return Graph;
});

@ -123,10 +123,17 @@ async function getStats() {
getStatsForSet('topics:tid', 'topicCount'),
]);
results[0].name = '[[admin/dashboard:unique-visitors]]';
results[1].name = '[[admin/dashboard:logins]]';
results[1].href = `${nconf.get('relative_path')}/admin/dashboard/logins`;
results[2].name = '[[admin/dashboard:new-users]]';
results[2].href = `${nconf.get('relative_path')}/admin/dashboard/users`;
results[3].name = '[[admin/dashboard:posts]]';
results[4].name = '[[admin/dashboard:topics]]';
results[4].href = `${nconf.get('relative_path')}/admin/dashboard/topics`;
({ results } = await plugins.hooks.fire('filter:admin.getStats', {
results,
@ -221,3 +228,66 @@ async function getLastRestart() {
lastrestart.timestampISO = utils.toISOString(lastrestart.timestamp);
return lastrestart;
}
dashboardController.getLogins = async (req, res) => {
let stats = await getStats();
const dataset = await analytics.getHourlyStatsForSet('analytics:logins', Date.now(), 24);
stats = stats.filter(stat => stat.name === '[[admin/dashboard:logins]]').map(({ ...stat }) => {
delete stat.href;
return stat;
});
const summary = {
day: stats[0].today,
week: stats[0].thisweek,
month: stats[0].thismonth,
};
res.render('admin/dashboard/logins', {
set: 'logins',
stats,
dataset,
summary,
});
};
dashboardController.getUsers = async (req, res) => {
let stats = await getStats();
const dataset = await analytics.getHourlyStatsForSet('analytics:registrations', Date.now(), 24);
stats = stats.filter(stat => stat.name === '[[admin/dashboard:new-users]]').map(({ ...stat }) => {
delete stat.href;
return stat;
});
const summary = {
day: stats[0].today,
week: stats[0].thisweek,
month: stats[0].thismonth,
};
res.render('admin/dashboard/users', {
set: 'registrations',
stats,
dataset,
summary,
});
};
dashboardController.getTopics = async (req, res) => {
let stats = await getStats();
const dataset = await analytics.getHourlyStatsForSet('analytics:topics', Date.now(), 24);
stats = stats.filter(stat => stat.name === '[[admin/dashboard:topics]]').map(({ ...stat }) => {
delete stat.href;
return stat;
});
const summary = {
day: stats[0].today,
week: stats[0].thisweek,
month: stats[0].thismonth,
};
res.render('admin/dashboard/topics', {
set: 'topics',
stats,
dataset,
summary,
});
};

@ -1,7 +1,9 @@
'use strict';
const user = require('../../user');
const meta = require('../../meta');
const privileges = require('../../privileges');
const analytics = require('../../analytics');
const helpers = require('../helpers');
@ -17,3 +19,22 @@ Admin.updateSetting = async (req, res) => {
await meta.configs.set(req.params.setting, req.body.value);
helpers.formatApiResponse(200, res);
};
Admin.getAnalytics = async (req, res) => {
const ok = await user.isAdministrator(req.uid);
if (!ok) {
return helpers.formatApiResponse(403, res);
}
// Default returns views from past 24 hours, by hour
if (!req.query.amount) {
if (req.query.units === 'days') {
req.query.amount = 30;
} else {
req.query.amount = 24;
}
}
const getStats = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet;
helpers.formatApiResponse(200, res, await getStats(`analytics:${req.params.set}`, parseInt(req.query.until, 10) || Date.now(), req.query.amount));
};

@ -8,6 +8,9 @@ module.exports = function (app, name, middleware, controllers) {
helpers.setupAdminPageRoute(app, `/${name}`, middleware, middlewares, controllers.admin.routeIndex);
helpers.setupAdminPageRoute(app, `/${name}/dashboard`, middleware, middlewares, controllers.admin.dashboard.get);
helpers.setupAdminPageRoute(app, `/${name}/dashboard/logins`, middleware, middlewares, controllers.admin.dashboard.getLogins);
helpers.setupAdminPageRoute(app, `/${name}/dashboard/users`, middleware, middlewares, controllers.admin.dashboard.getUsers);
helpers.setupAdminPageRoute(app, `/${name}/dashboard/topics`, middleware, middlewares, controllers.admin.dashboard.getTopics);
helpers.setupAdminPageRoute(app, `/${name}/manage/categories`, middleware, middlewares, controllers.admin.categories.getAll);
helpers.setupAdminPageRoute(app, `/${name}/manage/categories/:category_id`, middleware, middlewares, controllers.admin.categories.get);

@ -12,5 +12,7 @@ module.exports = function () {
setupApiRoute(router, 'put', '/settings/:setting', [...middlewares, middleware.checkRequired.bind(null, ['value'])], controllers.write.admin.updateSetting);
setupApiRoute(router, 'get', '/analytics/:set', [...middlewares], controllers.write.admin.getAnalytics);
return router;
};

@ -9,6 +9,7 @@ const slugify = require('../slugify');
const plugins = require('../plugins');
const groups = require('../groups');
const meta = require('../meta');
const analytics = require('../analytics');
module.exports = function (User) {
User.create = async function (data) {
@ -108,6 +109,7 @@ module.exports = function (User) {
await Promise.all([
db.incrObjectField('global', 'userCount'),
analytics.increment('registrations'),
db.sortedSetAddBulk(bulkAdd),
groups.join(groupsToJoin, userData.uid),
User.notifications.sendWelcomeNotification(userData.uid),

@ -1,80 +1,7 @@
<div class="row dashboard">
<div class="col-lg-9">
<div class="panel panel-default" id="analytics-panel">
<div class="panel-heading">
[[admin/dashboard:forum-traffic]]
<div class="pull-right">
<a id="view-as-json" href="{config.relative_path}/api/admin/analytics&type=hourly"><i class="fa fa-terminal"></i></a>
<i class="fa fa-expand"></i>
</div>
</div>
<div class="panel-body">
<div class="graph-container" id="analytics-traffic-container">
<canvas id="analytics-traffic" width="100%" height="400"></canvas>
</div>
<hr/>
<div class="row">
<div class="col-sm-3 hidden-xs text-center pageview-stats">
<div><strong id="pageViewsThirty">0</strong></div>
<div><a href="#" class="updatePageviewsGraph" data-action="updateGraph" data-units="days" data-amount="30">[[admin/dashboard:page-views-thirty]]</a></div>
</div>
<div class="col-sm-3 text-center pageview-stats">
<div><strong id="pageViewsSeven">0</strong></div>
<div><a href="#" class="updatePageviewsGraph" data-action="updateGraph" data-units="days" data-amount="7">[[admin/dashboard:page-views-seven]]</a></div>
</div>
<div class="col-sm-3 hidden-xs text-center pageview-stats">
<div><strong id="pageViewsPastDay">0</strong></div>
<div><a href="#" class="updatePageviewsGraph active" data-action="updateGraph" data-units="hours">[[admin/dashboard:page-views-last-day]]</a></div>
</div>
<div class="col-sm-3 text-center pageview-stats">
<div><strong><i class="fa fa-clock-o"></i></strong></div>
<div><a href="#" class="updatePageviewsGraph" data-action="updateGraph" data-units="custom">[[admin/dashboard:page-views-custom]]</a></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th class="text-right">[[admin/dashboard:stats.yesterday]]</th>
<th class="text-right">[[admin/dashboard:stats.today]]</th>
<th></th>
<th class="text-right">[[admin/dashboard:stats.last-week]]</th>
<th class="text-right">[[admin/dashboard:stats.this-week]]</th>
<th></th>
<th class="text-right">[[admin/dashboard:stats.last-month]]</th>
<th class="text-right">[[admin/dashboard:stats.this-month]]</th>
<th></th>
<th class="text-right">[[admin/dashboard:stats.all]]</th>
</tr>
</thead>
<tbody>
<!-- BEGIN stats -->
<tr>
<td><strong>{stats.name}</strong></td>
<td class="text-right formatted-number">{stats.yesterday}</td>
<td class="text-right formatted-number">{stats.today}</td>
<td class="{stats.dayTextClass}"><small>{stats.dayIncrease}%</small></td>
<td class="text-right formatted-number">{stats.lastweek}</td>
<td class="text-right formatted-number">{stats.thisweek}</td>
<td class="{stats.weekTextClass}"><small>{stats.weekIncrease}%</small></td>
<td class="text-right formatted-number">{stats.lastmonth}</td>
<td class="text-right formatted-number">{stats.thismonth}</td>
<td class="{stats.monthTextClass}"><small>{stats.monthIncrease}%</small></td>
<td class="text-right formatted-number">{stats.alltime}</td>
</tr>
<!-- END stats -->
</tbody>
</table>
</div>
</div>
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
<div class="row">
<div class="col-lg-4">

@ -0,0 +1,6 @@
<div class="row dashboard">
<div class="col-xs-12">
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
</div>
</div>

@ -0,0 +1,6 @@
<div class="row dashboard">
<div class="col-xs-12">
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
</div>
</div>

@ -0,0 +1,6 @@
<div class="row dashboard">
<div class="col-xs-12">
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
</div>
</div>

@ -0,0 +1,33 @@
<div class="panel panel-default" id="analytics-panel">
<div class="panel-heading">
[[admin/dashboard:forum-traffic]]
<div class="pull-right">
<a id="view-as-json" href="{config.relative_path}/api/v3/admin/analytics/{set}?type=hourly"><i class="fa fa-terminal"></i></a>
<i class="fa fa-expand"></i>
</div>
</div>
<div class="panel-body">
<div class="graph-container" id="analytics-traffic-container">
<canvas id="analytics-traffic" width="100%" height="400"></canvas>
</div>
<hr/>
<div class="row">
<div class="col-sm-3 hidden-xs text-center pageview-stats">
<div><strong id="pageViewsThirty">{{{ if summary.month }}}{./summary.month}{{{ else }}}0{{{ end }}}</strong></div>
<div><a href="#" class="updatePageviewsGraph" data-action="updateGraph" data-units="days" data-amount="30">[[admin/dashboard:page-views-thirty]]</a></div>
</div>
<div class="col-sm-3 text-center pageview-stats">
<div><strong id="pageViewsSeven">{{{ if summary.week }}}{./summary.week}{{{ else }}}0{{{ end }}}</strong></div>
<div><a href="#" class="updatePageviewsGraph" data-action="updateGraph" data-units="days" data-amount="7">[[admin/dashboard:page-views-seven]]</a></div>
</div>
<div class="col-sm-3 hidden-xs text-center pageview-stats">
<div><strong id="pageViewsPastDay">{{{ if summary.day }}}{./summary.day}{{{ else }}}0{{{ end }}}</strong></div>
<div><a href="#" class="updatePageviewsGraph active" data-action="updateGraph" data-units="hours">[[admin/dashboard:page-views-last-day]]</a></div>
</div>
<div class="col-sm-3 text-center pageview-stats">
<div><strong><i class="fa fa-clock-o"></i></strong></div>
<div><a href="#" class="updatePageviewsGraph" data-action="updateGraph" data-units="custom">[[admin/dashboard:page-views-custom]]</a></div>
</div>
</div>
</div>
</div>

@ -0,0 +1,49 @@
<div class="row">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th class="text-right">[[admin/dashboard:stats.yesterday]]</th>
<th class="text-right">[[admin/dashboard:stats.today]]</th>
<th></th>
<th class="text-right">[[admin/dashboard:stats.last-week]]</th>
<th class="text-right">[[admin/dashboard:stats.this-week]]</th>
<th></th>
<th class="text-right">[[admin/dashboard:stats.last-month]]</th>
<th class="text-right">[[admin/dashboard:stats.this-month]]</th>
<th></th>
<th class="text-right">[[admin/dashboard:stats.all]]</th>
</tr>
</thead>
<tbody>
<!-- BEGIN stats -->
<tr>
<td>
<strong>
{{{ if ../href }}}
<a href="{../href}">{../name}</a>
{{{ else }}}
{../name}
{{{ end }}}
</strong>
</td>
<td class="text-right formatted-number">{stats.yesterday}</td>
<td class="text-right formatted-number">{stats.today}</td>
<td class="{stats.dayTextClass}"><small>{stats.dayIncrease}%</small></td>
<td class="text-right formatted-number">{stats.lastweek}</td>
<td class="text-right formatted-number">{stats.thisweek}</td>
<td class="{stats.weekTextClass}"><small>{stats.weekIncrease}%</small></td>
<td class="text-right formatted-number">{stats.lastmonth}</td>
<td class="text-right formatted-number">{stats.thismonth}</td>
<td class="{stats.monthTextClass}"><small>{stats.monthIncrease}%</small></td>
<td class="text-right formatted-number">{stats.alltime}</td>
</tr>
<!-- END stats -->
</tbody>
</table>
</div>
</div>
Loading…
Cancel
Save