diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js
index f4d2a593c8..5767b45dd4 100644
--- a/public/src/admin/advanced/errors.js
+++ b/public/src/admin/advanced/errors.js
@@ -1,116 +1,121 @@
-'use strict';
+import {
+ Chart,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler,
+} from 'chart.js';
+
+import * as bootbox from 'bootbox';
+import * as alerts from '../../modules/alerts';
+
+Chart.register(
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler
+);
-define('admin/advanced/errors', [
- 'bootbox', 'alerts', 'chart.js/auto',
-], function (bootbox, alerts, { Chart }) {
- const Errors = {};
+// eslint-disable-next-line import/prefer-default-export
+export function init() {
+ setupCharts();
- Errors.init = function () {
- Errors.setupCharts();
+ $('[data-action="clear"]').on('click', clear404);
+}
- $('[data-action="clear"]').on('click', Errors.clear404);
- };
+function clear404() {
+ bootbox.confirm('[[admin/advanced/errors:clear404-confirm]]', function (ok) {
+ if (ok) {
+ socket.emit('admin.errors.clear', {}, function (err) {
+ if (err) {
+ return alerts.error(err);
+ }
- Errors.clear404 = function () {
- bootbox.confirm('[[admin/advanced/errors:clear404-confirm]]', function (ok) {
- if (ok) {
- socket.emit('admin.errors.clear', {}, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- ajaxify.refresh();
- alerts.success('[[admin/advanced/errors:clear404-success]]');
- });
- }
- });
- };
-
- Errors.setupCharts = function () {
- const notFoundCanvas = document.getElementById('not-found');
- const tooBusyCanvas = document.getElementById('toobusy');
- let dailyLabels = utils.getDaysArray();
-
- dailyLabels = dailyLabels.slice(-7);
-
- if (utils.isMobile()) {
- Chart.defaults.plugins.tooltip.enabled = false;
+ ajaxify.refresh();
+ alerts.success('[[admin/advanced/errors:clear404-success]]');
+ });
}
+ });
+}
- const data = {
- 'not-found': {
- labels: dailyLabels,
- datasets: [
- {
- label: '',
- fill: 'origin',
- tension: 0.25,
- backgroundColor: 'rgba(186,139,175,0.2)',
- borderColor: 'rgba(186,139,175,1)',
- pointBackgroundColor: 'rgba(186,139,175,1)',
- pointHoverBackgroundColor: '#fff',
- pointBorderColor: '#fff',
- pointHoverBorderColor: 'rgba(186,139,175,1)',
- data: ajaxify.data.analytics['not-found'],
- },
- ],
- },
- toobusy: {
- labels: dailyLabels,
- datasets: [
- {
- label: '',
- fill: 'origin',
- tension: 0.25,
- 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: ajaxify.data.analytics.toobusy,
- },
- ],
- },
- };
+function setupCharts() {
+ const notFoundCanvas = document.getElementById('not-found');
+ const tooBusyCanvas = document.getElementById('toobusy');
+ let dailyLabels = utils.getDaysArray();
- new Chart(notFoundCanvas.getContext('2d'), {
- type: 'line',
- data: data['not-found'],
- options: {
- responsive: true,
- plugins: {
- legend: {
- display: false,
- },
- },
- scales: {
- y: {
- beginAtZero: true,
- },
- },
- },
- });
+ dailyLabels = dailyLabels.slice(-7);
- new Chart(tooBusyCanvas.getContext('2d'), {
- type: 'line',
- data: data.toobusy,
- options: {
- responsive: true,
- plugins: {
- legend: {
- display: false,
- },
+ if (utils.isMobile()) {
+ Chart.defaults.plugins.tooltip.enabled = false;
+ }
+
+ const data = {
+ 'not-found': {
+ labels: dailyLabels,
+ datasets: [
+ {
+ label: '',
+ fill: 'origin',
+ tension: 0.25,
+ backgroundColor: 'rgba(186,139,175,0.2)',
+ borderColor: 'rgba(186,139,175,1)',
+ pointBackgroundColor: 'rgba(186,139,175,1)',
+ pointHoverBackgroundColor: '#fff',
+ pointBorderColor: '#fff',
+ pointHoverBorderColor: 'rgba(186,139,175,1)',
+ data: ajaxify.data.analytics['not-found'],
},
- scales: {
- y: {
- beginAtZero: true,
- },
+ ],
+ },
+ toobusy: {
+ labels: dailyLabels,
+ datasets: [
+ {
+ label: '',
+ fill: 'origin',
+ tension: 0.25,
+ 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: ajaxify.data.analytics.toobusy,
},
- },
- });
+ ],
+ },
};
- return Errors;
-});
+ new Chart(notFoundCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['not-found'],
+ options: {
+ responsive: true,
+ scales: {
+ y: {
+ beginAtZero: true,
+ },
+ },
+ },
+ });
+
+ new Chart(tooBusyCanvas.getContext('2d'), {
+ type: 'line',
+ data: data.toobusy,
+ options: {
+ responsive: true,
+ scales: {
+ y: {
+ beginAtZero: true,
+ },
+ },
+ },
+ });
+}
diff --git a/public/src/admin/dashboard.js b/public/src/admin/dashboard.js
index 2e37272813..9c027154c1 100644
--- a/public/src/admin/dashboard.js
+++ b/public/src/admin/dashboard.js
@@ -1,580 +1,600 @@
-'use strict';
+import {
+ Chart,
+ LineController,
+ DoughnutController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ ArcElement,
+ Tooltip,
+ Filler,
+} from 'chart.js';
+import * as Benchpress from 'benchpressjs';
+import * as bootbox from 'bootbox';
+import * as alerts from '../modules/alerts';
+import * as translator from '../modules/translator';
+import { formattedNumber } from '../modules/helpers';
-define('admin/dashboard', [
- 'chart.js/auto', 'translator', 'benchpress', 'bootbox', 'alerts', 'helpers',
-], function ({ Chart }, translator, Benchpress, bootbox, alerts, helpers) {
- const Admin = {};
- const intervals = {
- rooms: false,
- graphs: false,
- };
- let isMobile = false;
- const graphData = {
- rooms: {},
- traffic: {},
- };
- const currentGraph = {
- units: 'hours',
- until: undefined,
- };
+Chart.register(
+ LineController,
+ DoughnutController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ ArcElement,
+ Tooltip,
+ Filler
+);
- const DEFAULTS = {
- roomInterval: 10000,
- graphInterval: 15000,
- realtimeInterval: 1500,
- };
+const intervals = {
+ rooms: false,
+ graphs: false,
+};
+let isMobile = false;
+const graphData = {
+ rooms: {},
+ traffic: {},
+};
+const currentGraph = {
+ units: 'hours',
+ until: undefined,
+};
- const usedTopicColors = [];
+const DEFAULTS = {
+ roomInterval: 10000,
+ graphInterval: 15000,
+ realtimeInterval: 1500,
+};
- $(window).on('action:ajaxify.start', function () {
- clearInterval(intervals.rooms);
- clearInterval(intervals.graphs);
+const usedTopicColors = [];
- intervals.rooms = null;
- intervals.graphs = null;
- graphData.rooms = null;
- graphData.traffic = null;
- usedTopicColors.length = 0;
+$(window).on('action:ajaxify.start', function () {
+ clearInterval(intervals.rooms);
+ clearInterval(intervals.graphs);
+
+ intervals.rooms = null;
+ intervals.graphs = null;
+ graphData.rooms = null;
+ graphData.traffic = null;
+ usedTopicColors.length = 0;
+});
+
+// eslint-disable-next-line import/prefer-default-export
+export function init() {
+ app.enterRoom('admin');
+
+ isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+
+ setupDarkModeButton();
+ setupRealtimeButton();
+ setupGraphs(function () {
+ socket.emit('admin.rooms.getAll', updateRoomUsage);
+ initiateDashboard();
});
+ setupFullscreen();
+}
- Admin.init = function () {
- app.enterRoom('admin');
+function updateRoomUsage(err, data) {
+ if (err) {
+ return alerts.error(err);
+ }
- isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+ if (JSON.stringify(graphData.rooms) === JSON.stringify(data)) {
+ return;
+ }
- setupDarkModeButton();
- setupRealtimeButton();
- setupGraphs(function () {
- socket.emit('admin.rooms.getAll', Admin.updateRoomUsage);
- initiateDashboard();
+ graphData.rooms = data;
+
+ const html = '
' +
+ '
' + formattedNumber(data.onlineRegisteredCount) + '' +
+ '
[[admin/dashboard:active-users.users]]
' +
+ '
' +
+ '' +
+ '
' + formattedNumber(data.onlineGuestCount) + '' +
+ '
[[admin/dashboard:active-users.guests]]
' +
+ '
' +
+ '' +
+ '
' + formattedNumber(data.onlineRegisteredCount + data.onlineGuestCount) + '' +
+ '
[[admin/dashboard:active-users.total]]
' +
+ '
' +
+ '' +
+ '
' + formattedNumber(data.socketCount) + '' +
+ '
[[admin/dashboard:active-users.connections]]
' +
+ '
';
+
+ updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount);
+ updatePresenceGraph(data.users);
+ updateTopicsGraph(data.topTenTopics);
+
+ $('#active-users').translateHtml(html);
+}
+
+const graphs = {
+ traffic: null,
+ registered: null,
+ presence: null,
+ topics: null,
+};
+
+const topicColors = [
+ '#bf616a', '#5B90BF', '#d08770', '#ebcb8b',
+ '#a3be8c', '#96b5b4', '#8fa1b3', '#b48ead',
+ '#ab7967', '#46BFBD',
+];
+
+/* eslint-disable */
+// from chartjs.org
+function lighten(col, amt) {
+ let usePound = false;
+
+ if (col[0] === '#') {
+ col = col.slice(1);
+ usePound = true;
+ }
+
+ const num = parseInt(col, 16);
+
+ let r = (num >> 16) + amt;
+
+ if (r > 255) r = 255;
+ else if (r < 0) r = 0;
+
+ let b = ((num >> 8) & 0x00FF) + amt;
+
+ if (b > 255) b = 255;
+ else if (b < 0) b = 0;
+
+ let g = (num & 0x0000FF) + amt;
+
+ if (g > 255) g = 255;
+ else if (g < 0) g = 0;
+
+ return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16);
+}
+/* eslint-enable */
+
+function setupGraphs(callback) {
+ callback = callback || function () {};
+ const trafficCanvas = document.getElementById('analytics-traffic');
+ const registeredCanvas = document.getElementById('analytics-registered');
+ const presenceCanvas = document.getElementById('analytics-presence');
+ const topicsCanvas = document.getElementById('analytics-topics');
+ const trafficCtx = trafficCanvas.getContext('2d');
+ const registeredCtx = registeredCanvas.getContext('2d');
+ const presenceCtx = presenceCanvas.getContext('2d');
+ const topicsCtx = topicsCanvas.getContext('2d');
+ const trafficLabels = utils.getHoursArray();
+
+ if (isMobile) {
+ Chart.defaults.plugins.tooltip.enabled = false;
+ }
+
+ const t = translator.Translator.create();
+ Promise.all([
+ t.translateKey('admin/dashboard:graphs.page-views', []),
+ t.translateKey('admin/dashboard:graphs.page-views-registered', []),
+ t.translateKey('admin/dashboard:graphs.page-views-guest', []),
+ t.translateKey('admin/dashboard:graphs.page-views-bot', []),
+ t.translateKey('admin/dashboard:graphs.unique-visitors', []),
+ t.translateKey('admin/dashboard:graphs.registered-users', []),
+ t.translateKey('admin/dashboard:graphs.guest-users', []),
+ t.translateKey('admin/dashboard:on-categories', []),
+ t.translateKey('admin/dashboard:reading-posts', []),
+ t.translateKey('admin/dashboard:browsing-topics', []),
+ t.translateKey('admin/dashboard:recent', []),
+ t.translateKey('admin/dashboard:unread', []),
+ ]).then(function (translations) {
+ const tension = 0.25;
+ const data = {
+ labels: trafficLabels,
+ datasets: [
+ {
+ label: translations[0],
+ fill: 'origin',
+ tension: tension,
+ 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],
+ fill: 'origin',
+ tension: tension,
+ backgroundColor: '#ab464233',
+ borderColor: '#ab4642',
+ pointBackgroundColor: '#ab4642',
+ pointHoverBackgroundColor: '#ab4642',
+ pointBorderColor: '#fff',
+ pointHoverBorderColor: '#ab4642',
+ 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[2],
+ fill: 'origin',
+ tension: tension,
+ backgroundColor: '#ba8baf33',
+ borderColor: '#ba8baf',
+ pointBackgroundColor: '#ba8baf',
+ pointHoverBackgroundColor: '#ba8baf',
+ pointBorderColor: '#fff',
+ pointHoverBorderColor: '#ba8baf',
+ 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[3],
+ fill: 'origin',
+ tension: tension,
+ backgroundColor: '#f7ca8833',
+ borderColor: '#f7ca88',
+ pointBackgroundColor: '#f7ca88',
+ pointHoverBackgroundColor: '#f7ca88',
+ pointBorderColor: '#fff',
+ pointHoverBorderColor: '#f7ca88',
+ 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[4],
+ fill: 'origin',
+ tension: tension,
+ 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: [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();
+
+ data.datasets[0].yAxisID = 'left-y-axis';
+ data.datasets[1].yAxisID = 'left-y-axis';
+ data.datasets[2].yAxisID = 'left-y-axis';
+ data.datasets[3].yAxisID = 'left-y-axis';
+ data.datasets[4].yAxisID = 'right-y-axis';
+
+ graphs.traffic = new Chart(trafficCtx, {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ scales: {
+ 'left-y-axis': {
+ position: 'left',
+ type: 'linear',
+ title: {
+ display: true,
+ text: translations[0],
+ },
+ beginAtZero: true,
+ },
+ 'right-y-axis': {
+ position: 'right',
+ type: 'linear',
+ title: {
+ display: true,
+ text: translations[4],
+ },
+ beginAtZero: true,
+ },
+ },
+ interaction: {
+ intersect: false,
+ mode: 'index',
+ },
+ },
});
- setupFullscreen();
- };
- Admin.updateRoomUsage = function (err, data) {
+ const doughnutOpts = {
+ responsive: true,
+ };
+ graphs.registered = new Chart(registeredCtx, {
+ type: 'doughnut',
+ data: {
+ labels: translations.slice(5, 7),
+ datasets: [{
+ data: [1, 1],
+ backgroundColor: ['#F7464A', '#46BFBD'],
+ hoverBackgroundColor: ['#FF5A5E', '#5AD3D1'],
+ }],
+ },
+ options: doughnutOpts,
+ });
+
+ graphs.presence = new Chart(presenceCtx, {
+ type: 'doughnut',
+ data: {
+ labels: translations.slice(7, 12),
+ datasets: [{
+ data: [1, 1, 1, 1, 1],
+ backgroundColor: ['#F7464A', '#46BFBD', '#FDB45C', '#949FB1', '#9FB194'],
+ hoverBackgroundColor: ['#FF5A5E', '#5AD3D1', '#FFC870', '#A8B3C5', '#A8B3C5'],
+ }],
+ },
+ options: doughnutOpts,
+ });
+
+ graphs.topics = new Chart(topicsCtx, {
+ type: 'doughnut',
+ data: {
+ labels: [],
+ datasets: [{
+ data: [],
+ backgroundColor: [],
+ hoverBackgroundColor: [],
+ }],
+ },
+ options: doughnutOpts,
+ });
+
+ updateTrafficGraph();
+
+ $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () {
+ let until = new Date();
+ const amount = $(this).attr('data-amount');
+ if ($(this).attr('data-units') === 'days') {
+ until.setHours(0, 0, 0, 0);
+ }
+ until = until.getTime();
+ updateTrafficGraph($(this).attr('data-units'), until, amount);
+
+ 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 () {
+ const targetEl = $(this);
+
+ Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) {
+ const 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 () {
+ const date = new Date();
+ const today = date.toISOString().slice(0, 10);
+ date.setDate(date.getDate() - 1);
+ const yesterday = date.toISOString().slice(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
+ const formData = modal.find('form').serializeObject();
+ const validRegexp = /\d{4}-\d{2}-\d{2}/;
+
+ // Input validation
+ if (!formData.startRange && !formData.endRange) {
+ // No range? Assume last 30 days
+ updateTrafficGraph('days');
+ return;
+ } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) {
+ // Invalid Input
+ modal.find('.alert-danger').removeClass('hidden');
+ return false;
+ }
+
+ let until = new Date(formData.endRange);
+ until.setDate(until.getDate() + 1);
+ until = until.getTime();
+ const amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24);
+
+ updateTrafficGraph('days', until, amount);
+
+ // Update "custom range" label
+ targetEl.attr('data-startRange', formData.startRange);
+ targetEl.attr('data-endRange', formData.endRange);
+ targetEl.html(formData.startRange + ' – ' + formData.endRange);
+ }
+ });
+ });
+
+ callback();
+ });
+}
+
+function updateTrafficGraph(units, until, amount) {
+ // until and amount are optional
+
+ if (!app.isFocused) {
+ return;
+ }
+
+ socket.emit('admin.analytics.get', {
+ graph: 'traffic',
+ units: units || 'hours',
+ until: until,
+ amount: amount,
+ }, function (err, data) {
if (err) {
return alerts.error(err);
}
-
- if (JSON.stringify(graphData.rooms) === JSON.stringify(data)) {
+ if (JSON.stringify(graphData.traffic) === JSON.stringify(data)) {
return;
}
- graphData.rooms = data;
+ graphData.traffic = data;
- const html = '' +
- '
' + helpers.formattedNumber(data.onlineRegisteredCount) + '' +
- '
[[admin/dashboard:active-users.users]]
' +
- '
' +
- '' +
- '
' + helpers.formattedNumber(data.onlineGuestCount) + '' +
- '
[[admin/dashboard:active-users.guests]]
' +
- '
' +
- '' +
- '
' + helpers.formattedNumber(data.onlineRegisteredCount + data.onlineGuestCount) + '' +
- '
[[admin/dashboard:active-users.total]]
' +
- '
' +
- '' +
- '
' + helpers.formattedNumber(data.socketCount) + '' +
- '
[[admin/dashboard:active-users.connections]]
' +
- '
';
+ if (units === 'days') {
+ graphs.traffic.data.xLabels = utils.getDaysArray(until, amount);
+ } else {
+ graphs.traffic.data.xLabels = utils.getHoursArray();
- updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount);
- updatePresenceGraph(data.users);
- updateTopicsGraph(data.topTenTopics);
-
- $('#active-users').translateHtml(html);
- };
-
- const graphs = {
- traffic: null,
- registered: null,
- presence: null,
- topics: null,
- };
-
- const topicColors = ['#bf616a', '#5B90BF', '#d08770', '#ebcb8b', '#a3be8c', '#96b5b4', '#8fa1b3', '#b48ead', '#ab7967', '#46BFBD'];
-
- /* eslint-disable */
- // from chartjs.org
- function lighten(col, amt) {
- let usePound = false;
-
- if (col[0] === '#') {
- col = col.slice(1);
- usePound = true;
+ $('#pageViewsThirty').html(formattedNumber(data.summary.thirty));
+ $('#pageViewsSeven').html(formattedNumber(data.summary.seven));
+ $('#pageViewsPastDay').html(formattedNumber(data.pastDay));
}
- const num = parseInt(col, 16);
+ graphs.traffic.data.datasets[0].data = data.pageviews;
+ graphs.traffic.data.datasets[1].data = data.pageviewsRegistered;
+ graphs.traffic.data.datasets[2].data = data.pageviewsGuest;
+ graphs.traffic.data.datasets[3].data = data.pageviewsBot;
+ graphs.traffic.data.datasets[4].data = data.uniqueVisitors;
+ graphs.traffic.data.labels = graphs.traffic.data.xLabels;
- let r = (num >> 16) + amt;
+ graphs.traffic.update();
+ currentGraph.units = units;
+ currentGraph.until = until;
+ currentGraph.amount = amount;
- if (r > 255) r = 255;
- else if (r < 0) r = 0;
-
- let b = ((num >> 8) & 0x00FF) + amt;
-
- if (b > 255) b = 255;
- else if (b < 0) b = 0;
-
- let g = (num & 0x0000FF) + amt;
-
- if (g > 255) g = 255;
- else if (g < 0) g = 0;
-
- return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16);
- }
- /* eslint-enable */
-
- function setupGraphs(callback) {
- callback = callback || function () {};
- const trafficCanvas = document.getElementById('analytics-traffic');
- const registeredCanvas = document.getElementById('analytics-registered');
- const presenceCanvas = document.getElementById('analytics-presence');
- const topicsCanvas = document.getElementById('analytics-topics');
- const trafficCtx = trafficCanvas.getContext('2d');
- const registeredCtx = registeredCanvas.getContext('2d');
- const presenceCtx = presenceCanvas.getContext('2d');
- const topicsCtx = topicsCanvas.getContext('2d');
- const trafficLabels = utils.getHoursArray();
-
- if (isMobile) {
- Chart.defaults.plugins.tooltip.enabled = false;
- }
-
- const t = translator.Translator.create();
- Promise.all([
- t.translateKey('admin/dashboard:graphs.page-views', []),
- t.translateKey('admin/dashboard:graphs.page-views-registered', []),
- t.translateKey('admin/dashboard:graphs.page-views-guest', []),
- t.translateKey('admin/dashboard:graphs.page-views-bot', []),
- t.translateKey('admin/dashboard:graphs.unique-visitors', []),
- t.translateKey('admin/dashboard:graphs.registered-users', []),
- t.translateKey('admin/dashboard:graphs.guest-users', []),
- t.translateKey('admin/dashboard:on-categories', []),
- t.translateKey('admin/dashboard:reading-posts', []),
- t.translateKey('admin/dashboard:browsing-topics', []),
- t.translateKey('admin/dashboard:recent', []),
- t.translateKey('admin/dashboard:unread', []),
- ]).then(function (translations) {
- const tension = 0.25;
- const data = {
- labels: trafficLabels,
- datasets: [
- {
- label: translations[0],
- fill: 'origin',
- tension: tension,
- 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],
- fill: 'origin',
- tension: tension,
- backgroundColor: '#ab464233',
- borderColor: '#ab4642',
- pointBackgroundColor: '#ab4642',
- pointHoverBackgroundColor: '#ab4642',
- pointBorderColor: '#fff',
- pointHoverBorderColor: '#ab4642',
- 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[2],
- fill: 'origin',
- tension: tension,
- backgroundColor: '#ba8baf33',
- borderColor: '#ba8baf',
- pointBackgroundColor: '#ba8baf',
- pointHoverBackgroundColor: '#ba8baf',
- pointBorderColor: '#fff',
- pointHoverBorderColor: '#ba8baf',
- 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[3],
- fill: 'origin',
- tension: tension,
- backgroundColor: '#f7ca8833',
- borderColor: '#f7ca88',
- pointBackgroundColor: '#f7ca88',
- pointHoverBackgroundColor: '#f7ca88',
- pointBorderColor: '#fff',
- pointHoverBorderColor: '#f7ca88',
- 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[4],
- fill: 'origin',
- tension: tension,
- 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: [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();
-
- data.datasets[0].yAxisID = 'left-y-axis';
- data.datasets[1].yAxisID = 'left-y-axis';
- data.datasets[2].yAxisID = 'left-y-axis';
- data.datasets[3].yAxisID = 'left-y-axis';
- data.datasets[4].yAxisID = 'right-y-axis';
-
- graphs.traffic = new Chart(trafficCtx, {
- type: 'line',
- data: data,
- options: {
- responsive: true,
- scales: {
- 'left-y-axis': {
- position: 'left',
- type: 'linear',
- title: {
- display: true,
- text: translations[0],
- },
- beginAtZero: true,
- },
- 'right-y-axis': {
- position: 'right',
- type: 'linear',
- title: {
- display: true,
- text: translations[4],
- },
- beginAtZero: true,
- },
- },
- interaction: {
- intersect: false,
- mode: 'index',
- },
- },
- });
-
- const doughnutOpts = {
- responsive: true,
- maintainAspectRatio: true,
- plugins: {
- legend: {
- display: false,
- },
- },
- };
- graphs.registered = new Chart(registeredCtx, {
- type: 'doughnut',
- data: {
- labels: translations.slice(5, 7),
- datasets: [{
- data: [1, 1],
- backgroundColor: ['#F7464A', '#46BFBD'],
- hoverBackgroundColor: ['#FF5A5E', '#5AD3D1'],
- }],
- },
- options: doughnutOpts,
- });
-
- graphs.presence = new Chart(presenceCtx, {
- type: 'doughnut',
- data: {
- labels: translations.slice(7, 12),
- datasets: [{
- data: [1, 1, 1, 1, 1],
- backgroundColor: ['#F7464A', '#46BFBD', '#FDB45C', '#949FB1', '#9FB194'],
- hoverBackgroundColor: ['#FF5A5E', '#5AD3D1', '#FFC870', '#A8B3C5', '#A8B3C5'],
- }],
- },
- options: doughnutOpts,
- });
-
- graphs.topics = new Chart(topicsCtx, {
- type: 'doughnut',
- data: {
- labels: [],
- datasets: [{
- data: [],
- backgroundColor: [],
- hoverBackgroundColor: [],
- }],
- },
- options: doughnutOpts,
- });
-
- updateTrafficGraph();
-
- $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () {
- let until = new Date();
- const amount = $(this).attr('data-amount');
- if ($(this).attr('data-units') === 'days') {
- until.setHours(0, 0, 0, 0);
- }
- until = until.getTime();
- updateTrafficGraph($(this).attr('data-units'), until, amount);
-
- 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 () {
- const targetEl = $(this);
-
- Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) {
- const 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 () {
- const date = new Date();
- const today = date.toISOString().slice(0, 10);
- date.setDate(date.getDate() - 1);
- const yesterday = date.toISOString().slice(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
- const formData = modal.find('form').serializeObject();
- const validRegexp = /\d{4}-\d{2}-\d{2}/;
-
- // Input validation
- if (!formData.startRange && !formData.endRange) {
- // No range? Assume last 30 days
- updateTrafficGraph('days');
- return;
- } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) {
- // Invalid Input
- modal.find('.alert-danger').removeClass('hidden');
- return false;
- }
-
- let until = new Date(formData.endRange);
- until.setDate(until.getDate() + 1);
- until = until.getTime();
- const amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24);
-
- updateTrafficGraph('days', until, amount);
-
- // Update "custom range" label
- targetEl.attr('data-startRange', formData.startRange);
- targetEl.attr('data-endRange', formData.endRange);
- targetEl.html(formData.startRange + ' – ' + formData.endRange);
- }
- });
- });
-
- callback();
- });
- }
-
- function updateTrafficGraph(units, until, amount) {
- // until and amount are optional
-
- if (!app.isFocused) {
- return;
- }
-
- socket.emit('admin.analytics.get', {
- graph: 'traffic',
+ // Update the View as JSON button url
+ const apiEl = $('#view-as-json');
+ const newHref = $.param({
units: units || 'hours',
until: until,
- amount: amount,
- }, function (err, data) {
- if (err) {
- return alerts.error(err);
- }
- if (JSON.stringify(graphData.traffic) === JSON.stringify(data)) {
- return;
- }
+ count: amount,
+ });
+ apiEl.attr('href', config.relative_path + '/api/admin/analytics?' + newHref);
+ });
+}
- graphData.traffic = data;
+function updateRegisteredGraph(registered, guest) {
+ $('#analytics-legend .registered').parent().find('.count').text(registered);
+ $('#analytics-legend .guest').parent().find('.count').text(guest);
+ graphs.registered.data.datasets[0].data[0] = registered;
+ graphs.registered.data.datasets[0].data[1] = guest;
+ graphs.registered.update();
+}
- if (units === 'days') {
- graphs.traffic.data.xLabels = utils.getDaysArray(until, amount);
+function updatePresenceGraph(users) {
+ $('#analytics-presence-legend .on-categories').parent().find('.count').text(users.categories);
+ $('#analytics-presence-legend .reading-posts').parent().find('.count').text(users.topics);
+ $('#analytics-presence-legend .browsing-topics').parent().find('.count').text(users.category);
+ $('#analytics-presence-legend .recent').parent().find('.count').text(users.recent);
+ $('#analytics-presence-legend .unread').parent().find('.count').text(users.unread);
+ 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 (!topics.length) {
+ translator.translate('[[admin/dashboard:no-users-browsing]]', function (translated) {
+ topics = [{
+ title: translated,
+ count: 1,
+ }];
+ updateTopicsGraph(topics);
+ });
+ return;
+ }
+
+ graphs.topics.data.labels = [];
+ graphs.topics.data.datasets[0].data = [];
+ graphs.topics.data.datasets[0].backgroundColor = [];
+ graphs.topics.data.datasets[0].hoverBackgroundColor = [];
+
+ topics.forEach(function (topic, i) {
+ graphs.topics.data.labels.push(topic.title);
+ graphs.topics.data.datasets[0].data.push(topic.count);
+ graphs.topics.data.datasets[0].backgroundColor.push(topicColors[i]);
+ graphs.topics.data.datasets[0].hoverBackgroundColor.push(lighten(topicColors[i], 10));
+ });
+
+ function buildTopicsLegend() {
+ let html = '';
+ topics.forEach(function (t, i) {
+ const link = t.tid ? ' ' + t.title + '' : t.title;
+ const label = t.count === '0' ? t.title : link;
+
+ html += '' +
+ '' +
+ ' (' + t.count + ') ' + label + '' +
+ '';
+ });
+ $('#topics-legend').translateHtml(html);
+ }
+
+ buildTopicsLegend();
+ graphs.topics.update();
+}
+
+function setupDarkModeButton() {
+ let bsTheme = localStorage.getItem('data-bs-theme') || 'light';
+ $('#toggle-dark-mode').prop('checked', bsTheme === 'dark')
+ .on('click', function () {
+ const isChecked = $(this).is(':checked');
+ bsTheme = isChecked ? 'dark' : 'light';
+ $('html').attr('data-bs-theme', bsTheme);
+ localStorage.setItem('data-bs-theme', bsTheme);
+ });
+}
+
+function setupRealtimeButton() {
+ $('#toggle-realtime').on('click', function () {
+ initiateDashboard($(this).is(':checked'));
+ });
+}
+
+function initiateDashboard(realtime) {
+ clearInterval(intervals.rooms);
+ clearInterval(intervals.graphs);
+
+ intervals.rooms = setInterval(function () {
+ if (app.isFocused && socket.connected) {
+ socket.emit('admin.rooms.getAll', updateRoomUsage);
+ }
+ }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.roomInterval);
+
+ intervals.graphs = setInterval(function () {
+ updateTrafficGraph(currentGraph.units, currentGraph.until, currentGraph.amount);
+ }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval);
+}
+
+function setupFullscreen() {
+ const container = document.getElementById('analytics-panel');
+ const $container = $(container);
+ const btn = $container.find('#expand-analytics');
+ let fsMethod;
+ let exitMethod;
+
+ if (container.requestFullscreen) {
+ fsMethod = 'requestFullscreen';
+ exitMethod = 'exitFullscreen';
+ } else if (container.mozRequestFullScreen) {
+ fsMethod = 'mozRequestFullScreen';
+ exitMethod = 'mozCancelFullScreen';
+ } else if (container.webkitRequestFullscreen) {
+ fsMethod = 'webkitRequestFullscreen';
+ exitMethod = 'webkitCancelFullScreen';
+ } else if (container.msRequestFullscreen) {
+ fsMethod = 'msRequestFullscreen';
+ exitMethod = 'msCancelFullScreen';
+ }
+
+ if (fsMethod) {
+ btn.on('click', function () {
+ if ($container.hasClass('fullscreen')) {
+ document[exitMethod]();
+ $container.removeClass('fullscreen');
} else {
- graphs.traffic.data.xLabels = utils.getHoursArray();
-
- $('#pageViewsThirty').html(helpers.formattedNumber(data.summary.thirty));
- $('#pageViewsSeven').html(helpers.formattedNumber(data.summary.seven));
- $('#pageViewsPastDay').html(helpers.formattedNumber(data.pastDay));
+ container[fsMethod]();
+ $container.addClass('fullscreen');
}
-
- graphs.traffic.data.datasets[0].data = data.pageviews;
- graphs.traffic.data.datasets[1].data = data.pageviewsRegistered;
- graphs.traffic.data.datasets[2].data = data.pageviewsGuest;
- graphs.traffic.data.datasets[3].data = data.pageviewsBot;
- graphs.traffic.data.datasets[4].data = data.uniqueVisitors;
- graphs.traffic.data.labels = graphs.traffic.data.xLabels;
-
- graphs.traffic.update();
- currentGraph.units = units;
- currentGraph.until = until;
- currentGraph.amount = amount;
-
- // Update the View as JSON button url
- const apiEl = $('#view-as-json');
- const newHref = $.param({
- units: units || 'hours',
- until: until,
- count: amount,
- });
- apiEl.attr('href', config.relative_path + '/api/admin/analytics?' + newHref);
});
}
-
- function updateRegisteredGraph(registered, guest) {
- $('#analytics-legend .registered').parent().find('.count').text(registered);
- $('#analytics-legend .guest').parent().find('.count').text(guest);
- graphs.registered.data.datasets[0].data[0] = registered;
- graphs.registered.data.datasets[0].data[1] = guest;
- graphs.registered.update();
- }
-
- function updatePresenceGraph(users) {
- $('#analytics-presence-legend .on-categories').parent().find('.count').text(users.categories);
- $('#analytics-presence-legend .reading-posts').parent().find('.count').text(users.topics);
- $('#analytics-presence-legend .browsing-topics').parent().find('.count').text(users.category);
- $('#analytics-presence-legend .recent').parent().find('.count').text(users.recent);
- $('#analytics-presence-legend .unread').parent().find('.count').text(users.unread);
- 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 (!topics.length) {
- translator.translate('[[admin/dashboard:no-users-browsing]]', function (translated) {
- topics = [{
- title: translated,
- count: 1,
- }];
- updateTopicsGraph(topics);
- });
- return;
- }
-
- graphs.topics.data.labels = [];
- graphs.topics.data.datasets[0].data = [];
- graphs.topics.data.datasets[0].backgroundColor = [];
- graphs.topics.data.datasets[0].hoverBackgroundColor = [];
-
- topics.forEach(function (topic, i) {
- graphs.topics.data.labels.push(topic.title);
- graphs.topics.data.datasets[0].data.push(topic.count);
- graphs.topics.data.datasets[0].backgroundColor.push(topicColors[i]);
- graphs.topics.data.datasets[0].hoverBackgroundColor.push(lighten(topicColors[i], 10));
- });
-
- function buildTopicsLegend() {
- let html = '';
- topics.forEach(function (t, i) {
- const link = t.tid ? ' ' + t.title + '' : t.title;
- const label = t.count === '0' ? t.title : link;
-
- html += '' +
- '' +
- ' (' + t.count + ') ' + label + '' +
- '';
- });
- $('#topics-legend').translateHtml(html);
- }
-
- buildTopicsLegend();
- graphs.topics.update();
- }
-
- function setupDarkModeButton() {
- let bsTheme = localStorage.getItem('data-bs-theme') || 'light';
- $('#toggle-dark-mode').prop('checked', bsTheme === 'dark')
- .on('click', function () {
- const isChecked = $(this).is(':checked');
- bsTheme = isChecked ? 'dark' : 'light';
- $('html').attr('data-bs-theme', bsTheme);
- localStorage.setItem('data-bs-theme', bsTheme);
- });
- }
-
- function setupRealtimeButton() {
- $('#toggle-realtime').on('click', function () {
- initiateDashboard($(this).is(':checked'));
- });
- }
-
- function initiateDashboard(realtime) {
- clearInterval(intervals.rooms);
- clearInterval(intervals.graphs);
-
- intervals.rooms = setInterval(function () {
- if (app.isFocused && socket.connected) {
- socket.emit('admin.rooms.getAll', Admin.updateRoomUsage);
- }
- }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.roomInterval);
-
- intervals.graphs = setInterval(function () {
- updateTrafficGraph(currentGraph.units, currentGraph.until, currentGraph.amount);
- }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval);
- }
-
- function setupFullscreen() {
- const container = document.getElementById('analytics-panel');
- const $container = $(container);
- const btn = $container.find('#expand-analytics');
- let fsMethod;
- let exitMethod;
-
- if (container.requestFullscreen) {
- fsMethod = 'requestFullscreen';
- exitMethod = 'exitFullscreen';
- } else if (container.mozRequestFullScreen) {
- fsMethod = 'mozRequestFullScreen';
- exitMethod = 'mozCancelFullScreen';
- } else if (container.webkitRequestFullscreen) {
- fsMethod = 'webkitRequestFullscreen';
- exitMethod = 'webkitCancelFullScreen';
- } else if (container.msRequestFullscreen) {
- fsMethod = 'msRequestFullscreen';
- exitMethod = 'msCancelFullScreen';
- }
-
- if (fsMethod) {
- btn.on('click', function () {
- if ($container.hasClass('fullscreen')) {
- document[exitMethod]();
- $container.removeClass('fullscreen');
- } else {
- container[fsMethod]();
- $container.addClass('fullscreen');
- }
- });
- }
- }
-
- return Admin;
-});
+}
diff --git a/public/src/admin/manage/category-analytics.js b/public/src/admin/manage/category-analytics.js
index 93c4fa0abe..a86e5b3b1b 100644
--- a/public/src/admin/manage/category-analytics.js
+++ b/public/src/admin/manage/category-analytics.js
@@ -1,142 +1,151 @@
-'use strict';
+import {
+ Chart,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler,
+} from 'chart.js';
+import * as categorySelector from '../../modules/categorySelector';
-define('admin/manage/category-analytics', [
- 'chart.js/auto', 'categorySelector',
-], function ({ Chart }, categorySelector) {
- const CategoryAnalytics = {};
+Chart.register(
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler
+);
- CategoryAnalytics.init = function () {
- categorySelector.init($('[component="category-selector"]'), {
- onSelect: function (selectedCategory) {
- ajaxify.go('admin/manage/categories/' + selectedCategory.cid + '/analytics');
- },
- showLinks: true,
- template: 'admin/partials/category/selector-dropdown-right',
- });
+// eslint-disable-next-line import/prefer-default-export
+export function init() {
+ categorySelector.init($('[component="category-selector"]'), {
+ onSelect: function (selectedCategory) {
+ ajaxify.go('admin/manage/categories/' + selectedCategory.cid + '/analytics');
+ },
+ showLinks: true,
+ template: 'admin/partials/category/selector-dropdown-right',
+ });
- const hourlyCanvas = document.getElementById('pageviews:hourly');
- const dailyCanvas = document.getElementById('pageviews:daily');
- const topicsCanvas = document.getElementById('topics:daily');
- const postsCanvas = document.getElementById('posts:daily');
- const hourlyLabels = utils.getHoursArray().map(function (text, idx) {
- return idx % 3 ? '' : text;
- });
- const dailyLabels = utils.getDaysArray().map(function (text, idx) {
- return idx % 3 ? '' : text;
- });
+ const hourlyCanvas = document.getElementById('pageviews:hourly');
+ const dailyCanvas = document.getElementById('pageviews:daily');
+ const topicsCanvas = document.getElementById('topics:daily');
+ const postsCanvas = document.getElementById('posts:daily');
+ const hourlyLabels = utils.getHoursArray().map(function (text, idx) {
+ return idx % 3 ? '' : text;
+ });
+ const dailyLabels = utils.getDaysArray().map(function (text, idx) {
+ return idx % 3 ? '' : text;
+ });
- if (utils.isMobile()) {
- Chart.defaults.plugins.tooltip.enabled = false;
- }
+ if (utils.isMobile()) {
+ Chart.defaults.plugins.tooltip.enabled = false;
+ }
- const commonDataSetOpts = {
- label: '',
- fill: true,
- tension: 0.25,
- pointHoverBackgroundColor: '#fff',
- pointBorderColor: '#fff',
- };
-
- const data = {
- 'pageviews:hourly': {
- labels: hourlyLabels,
- datasets: [
- {
- ...commonDataSetOpts,
- backgroundColor: 'rgba(186,139,175,0.2)',
- borderColor: 'rgba(186,139,175,1)',
- pointBackgroundColor: 'rgba(186,139,175,1)',
- pointHoverBorderColor: 'rgba(186,139,175,1)',
- data: ajaxify.data.analytics['pageviews:hourly'],
- },
- ],
- },
- 'pageviews:daily': {
- labels: dailyLabels,
- datasets: [
- {
- ...commonDataSetOpts,
- backgroundColor: 'rgba(151,187,205,0.2)',
- borderColor: 'rgba(151,187,205,1)',
- pointBackgroundColor: 'rgba(151,187,205,1)',
- pointHoverBorderColor: 'rgba(151,187,205,1)',
- data: ajaxify.data.analytics['pageviews:daily'],
- },
- ],
- },
- 'topics:daily': {
- labels: dailyLabels.slice(-7),
- datasets: [
- {
- ...commonDataSetOpts,
- backgroundColor: 'rgba(171,70,66,0.2)',
- borderColor: 'rgba(171,70,66,1)',
- pointBackgroundColor: 'rgba(171,70,66,1)',
- pointHoverBorderColor: 'rgba(171,70,66,1)',
- data: ajaxify.data.analytics['topics:daily'],
- },
- ],
- },
- 'posts:daily': {
- labels: dailyLabels.slice(-7),
- datasets: [
- {
- ...commonDataSetOpts,
- backgroundColor: 'rgba(161,181,108,0.2)',
- borderColor: 'rgba(161,181,108,1)',
- pointBackgroundColor: 'rgba(161,181,108,1)',
- pointHoverBorderColor: 'rgba(161,181,108,1)',
- data: ajaxify.data.analytics['posts:daily'],
- },
- ],
- },
- };
-
- hourlyCanvas.width = $(hourlyCanvas).parent().width();
- dailyCanvas.width = $(dailyCanvas).parent().width();
- topicsCanvas.width = $(topicsCanvas).parent().width();
- postsCanvas.width = $(postsCanvas).parent().width();
-
- const chartOpts = {
- responsive: true,
- animation: false,
- plugins: {
- legend: {
- display: false,
- },
- },
- scales: {
- y: {
- beginAtZero: true,
- },
- },
- };
-
- new Chart(hourlyCanvas.getContext('2d'), {
- type: 'line',
- data: data['pageviews:hourly'],
- options: chartOpts,
- });
-
- new Chart(dailyCanvas.getContext('2d'), {
- type: 'line',
- data: data['pageviews:daily'],
- options: chartOpts,
- });
-
- new Chart(topicsCanvas.getContext('2d'), {
- type: 'line',
- data: data['topics:daily'],
- options: chartOpts,
- });
-
- new Chart(postsCanvas.getContext('2d'), {
- type: 'line',
- data: data['posts:daily'],
- options: chartOpts,
- });
+ const commonDataSetOpts = {
+ label: '',
+ fill: true,
+ tension: 0.25,
+ pointHoverBackgroundColor: '#fff',
+ pointBorderColor: '#fff',
};
- return CategoryAnalytics;
-});
+ const data = {
+ 'pageviews:hourly': {
+ labels: hourlyLabels,
+ datasets: [
+ {
+ ...commonDataSetOpts,
+ backgroundColor: 'rgba(186,139,175,0.2)',
+ borderColor: 'rgba(186,139,175,1)',
+ pointBackgroundColor: 'rgba(186,139,175,1)',
+ pointHoverBorderColor: 'rgba(186,139,175,1)',
+ data: ajaxify.data.analytics['pageviews:hourly'],
+ },
+ ],
+ },
+ 'pageviews:daily': {
+ labels: dailyLabels,
+ datasets: [
+ {
+ ...commonDataSetOpts,
+ backgroundColor: 'rgba(151,187,205,0.2)',
+ borderColor: 'rgba(151,187,205,1)',
+ pointBackgroundColor: 'rgba(151,187,205,1)',
+ pointHoverBorderColor: 'rgba(151,187,205,1)',
+ data: ajaxify.data.analytics['pageviews:daily'],
+ },
+ ],
+ },
+ 'topics:daily': {
+ labels: dailyLabels.slice(-7),
+ datasets: [
+ {
+ ...commonDataSetOpts,
+ backgroundColor: 'rgba(171,70,66,0.2)',
+ borderColor: 'rgba(171,70,66,1)',
+ pointBackgroundColor: 'rgba(171,70,66,1)',
+ pointHoverBorderColor: 'rgba(171,70,66,1)',
+ data: ajaxify.data.analytics['topics:daily'],
+ },
+ ],
+ },
+ 'posts:daily': {
+ labels: dailyLabels.slice(-7),
+ datasets: [
+ {
+ ...commonDataSetOpts,
+ backgroundColor: 'rgba(161,181,108,0.2)',
+ borderColor: 'rgba(161,181,108,1)',
+ pointBackgroundColor: 'rgba(161,181,108,1)',
+ pointHoverBorderColor: 'rgba(161,181,108,1)',
+ data: ajaxify.data.analytics['posts:daily'],
+ },
+ ],
+ },
+ };
+
+ hourlyCanvas.width = $(hourlyCanvas).parent().width();
+ dailyCanvas.width = $(dailyCanvas).parent().width();
+ topicsCanvas.width = $(topicsCanvas).parent().width();
+ postsCanvas.width = $(postsCanvas).parent().width();
+
+ const chartOpts = {
+ responsive: true,
+ animation: false,
+ scales: {
+ y: {
+ beginAtZero: true,
+ },
+ },
+ };
+
+ new Chart(hourlyCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['pageviews:hourly'],
+ options: chartOpts,
+ });
+
+ new Chart(dailyCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['pageviews:daily'],
+ options: chartOpts,
+ });
+
+ new Chart(topicsCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['topics:daily'],
+ options: chartOpts,
+ });
+
+ new Chart(postsCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['posts:daily'],
+ options: chartOpts,
+ });
+}
+
diff --git a/public/src/admin/modules/dashboard-line-graph.js b/public/src/admin/modules/dashboard-line-graph.js
index 3a95868b6c..8a000aceca 100644
--- a/public/src/admin/modules/dashboard-line-graph.js
+++ b/public/src/admin/modules/dashboard-line-graph.js
@@ -1,194 +1,215 @@
-'use strict';
+import {
+ Chart,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler,
+} from 'chart.js';
-define('admin/modules/dashboard-line-graph', [
- 'chart.js/auto', 'translator', 'benchpress', 'api', 'hooks', 'bootbox',
-], function ({ Chart }, translator, Benchpress, api, hooks, bootbox) {
- const Graph = {
- _current: null,
- };
- let isMobile = false;
+import * as Benchpress from 'benchpressjs';
+import * as bootbox from 'bootbox';
+import * as translator from '../../modules/translator';
+import * as api from '../../modules/api';
+import * as hooks from '../../modules/hooks';
- 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.plugins.tooltip.enabled = false;
- }
+Chart.register(
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler
+);
- Graph.handleUpdateControls({ set });
- const t = translator.Translator.create();
- return new Promise((resolve) => {
- t.translateKey(`admin/menu:${ajaxify.data.template.name.replace('admin/', '')}`, []).then((key) => {
- const data = {
- labels: trafficLabels,
- datasets: [
- {
- label: key,
- fill: true,
- tension: 0.25,
- 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 || [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- },
- ],
- };
+let _current = null;
+let isMobile = false;
- canvas.width = $(canvas).parent().width();
+// eslint-disable-next-line import/prefer-default-export
+export function init({ set, dataset }) {
+ const canvas = document.getElementById('analytics-traffic');
+ const canvasCtx = canvas.getContext('2d');
+ const trafficLabels = utils.getHoursArray();
- data.datasets[0].yAxisID = 'left-y-axis';
+ isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+ if (isMobile) {
+ Chart.defaults.plugins.tooltip.enabled = false;
+ }
- Graph._current = new Chart(canvasCtx, {
- type: 'line',
- data: data,
- options: {
- responsive: true,
- scales: {
- 'left-y-axis': {
- type: 'linear',
- position: 'left',
- beginAtZero: true,
- title: {
- display: true,
- text: key,
- },
+ handleUpdateControls({ set });
+
+ const t = translator.Translator.create();
+ return new Promise((resolve) => {
+ t.translateKey(`admin/menu:${ajaxify.data.template.name.replace('admin/', '')}`, []).then((key) => {
+ const data = {
+ labels: trafficLabels,
+ datasets: [
+ {
+ label: key,
+ fill: true,
+ tension: 0.25,
+ 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 || [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ },
+ ],
+ };
+
+ canvas.width = $(canvas).parent().width();
+
+ data.datasets[0].yAxisID = 'left-y-axis';
+
+ _current = new Chart(canvasCtx, {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ scales: {
+ 'left-y-axis': {
+ type: 'linear',
+ position: 'left',
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: key,
},
},
- interaction: {
- intersect: false,
- mode: 'index',
- },
},
- });
-
- if (!dataset) {
- Graph.update(set).then(resolve);
- } else {
- resolve(Graph._current);
- }
+ interaction: {
+ intersect: false,
+ mode: 'index',
+ },
+ },
});
- });
- };
- Graph.handleUpdateControls = ({ set }) => {
- $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () {
- let until = new Date();
- const amount = $(this).attr('data-amount');
- if ($(this).attr('data-units') === 'days') {
- until.setHours(0, 0, 0, 0);
+ if (!dataset) {
+ update(set).then(resolve);
+ } else {
+ resolve(_current);
}
- until = until.getTime();
- Graph.update(set, $(this).attr('data-units'), until, amount);
-
- 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 () {
- const targetEl = $(this);
-
- Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) {
- const 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 () {
- const date = new Date();
- const today = date.toISOString().slice(0, 10);
- date.setDate(date.getDate() - 1);
- const yesterday = date.toISOString().slice(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
- const formData = modal.find('form').serializeObject();
- const validRegexp = /\d{4}-\d{2}-\d{2}/;
-
- // Input validation
- if (!formData.startRange && !formData.endRange) {
- // No range? Assume last 30 days
- Graph.update(set, 'days');
- return;
- } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) {
- // Invalid Input
- modal.find('.alert-danger').removeClass('hidden');
- return false;
- }
-
- let until = new Date(formData.endRange);
- until.setDate(until.getDate() + 1);
- until = until.getTime();
- const amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24);
-
- Graph.update(set, 'days', until, amount);
-
- // 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 = ajaxify.data.query.units || 'hours',
- until = ajaxify.data.query.until,
- amount = ajaxify.data.query.count
- ) => {
- if (!Graph._current) {
- return Promise.reject(new Error('[[error:invalid-data]]'));
+function handleUpdateControls({ set }) {
+ $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () {
+ let until = new Date();
+ const amount = $(this).attr('data-amount');
+ if ($(this).attr('data-units') === 'days') {
+ until.setHours(0, 0, 0, 0);
}
+ until = until.getTime();
+ update(set, $(this).attr('data-units'), until, amount);
- return new Promise((resolve) => {
- 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 address bar and "View as JSON" button url
- const apiEl = $('#view-as-json');
- const newHref = $.param({
- units: units || 'hours',
- until: until,
- count: amount,
- });
- apiEl.attr('href', `${config.relative_path}/api/v3/admin/analytics/${ajaxify.data.set}?${newHref}`);
- const url = ajaxify.removeRelativePath(ajaxify.data.url.slice(1));
- ajaxify.updateHistory(`${url}?${newHref}`, true);
- hooks.fire('action:admin.dashboard.updateGraph', {
- graph: Graph._current,
- });
- resolve(Graph._current);
+ 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 () {
+ const targetEl = $(this);
+
+ Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) {
+ const 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 () {
+ const date = new Date();
+ const today = date.toISOString().slice(0, 10);
+ date.setDate(date.getDate() - 1);
+ const yesterday = date.toISOString().slice(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
+ const formData = modal.find('form').serializeObject();
+ const validRegexp = /\d{4}-\d{2}-\d{2}/;
+
+ // Input validation
+ if (!formData.startRange && !formData.endRange) {
+ // No range? Assume last 30 days
+ update(set, 'days');
+ return;
+ } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) {
+ // Invalid Input
+ modal.find('.alert-danger').removeClass('hidden');
+ return false;
+ }
+
+ let until = new Date(formData.endRange);
+ until.setDate(until.getDate() + 1);
+ until = until.getTime();
+ const amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24);
+
+ update(set, 'days', until, amount);
+
+ // Update "custom range" label
+ targetEl.attr('data-startRange', formData.startRange);
+ targetEl.attr('data-endRange', formData.endRange);
+ targetEl.html(formData.startRange + ' – ' + formData.endRange);
+ }
+ });
+ });
+}
+
+function update(
+ set,
+ units = ajaxify.data.query.units || 'hours',
+ until = ajaxify.data.query.until,
+ amount = ajaxify.data.query.count
+) {
+ if (!_current) {
+ return Promise.reject(new Error('[[error:invalid-data]]'));
+ }
+
+ return new Promise((resolve) => {
+ api.get(`/admin/analytics/${set}`, { units, until, amount }).then((dataset) => {
+ if (units === 'days') {
+ _current.data.xLabels = utils.getDaysArray(until, amount);
+ } else {
+ _current.data.xLabels = utils.getHoursArray();
+ }
+
+ _current.data.datasets[0].data = dataset;
+ _current.data.labels = _current.data.xLabels;
+ _current.update();
+
+ // Update address bar and "View as JSON" button url
+ const apiEl = $('#view-as-json');
+ const newHref = $.param({
+ units: units || 'hours',
+ until: until,
+ count: amount,
+ });
+ apiEl.attr('href', `${config.relative_path}/api/v3/admin/analytics/${ajaxify.data.set}?${newHref}`);
+ const url = ajaxify.removeRelativePath(ajaxify.data.url.slice(1));
+ ajaxify.updateHistory(`${url}?${newHref}`, true);
+ hooks.fire('action:admin.dashboard.updateGraph', {
+ graph: _current,
+ });
+ resolve(_current);
+ });
+ });
+}
- return Graph;
-});
diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js
index 5c92f46292..08e7fdb88c 100644
--- a/public/src/client/flags/list.js
+++ b/public/src/client/flags/list.js
@@ -1,300 +1,301 @@
-'use strict';
+import {
+ Chart,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler,
+} from 'chart.js';
-define('forum/flags/list', [
- 'components', 'chart.js/auto', 'categoryFilter',
- 'autocomplete', 'api', 'alerts',
- 'userFilter',
-], function (
- components, { Chart }, categoryFilter,
- autocomplete, api, alerts,
- userFilter
-) {
- const Flags = {};
- const selected = new Map([
- ['cids', []],
- ['assignee', []],
- ['targetUid', []],
- ['reporterId', []],
- ]);
+import * as categoryFilter from '../../modules/categoryFilter';
+import * as userFilter from '../../modules/userFilter';
+import * as autocomplete from '../../modules/autocomplete';
+import * as api from '../../modules/api';
+import * as alerts from '../../modules/alerts';
+import * as components from '../../modules/components';
- Flags.init = function () {
- Flags.enableFilterForm();
- Flags.enableCheckboxes();
- Flags.handleBulkActions();
+Chart.register(LineController, CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler);
- if (ajaxify.data.filters.hasOwnProperty('cid')) {
- selected.set('cids', Array.isArray(ajaxify.data.filters.cid) ?
- ajaxify.data.filters.cid : [ajaxify.data.filters.cid]);
- }
+const selected = new Map([
+ ['cids', []],
+ ['assignee', []],
+ ['targetUid', []],
+ ['reporterId', []],
+]);
- categoryFilter.init($('[component="category/dropdown"]'), {
- privilege: 'moderate',
- selectedCids: selected.get('cids'),
- updateButton: function ({ selectedCids: cids }) {
- selected.set('cids', cids);
- applyFilters();
- },
- });
+export function init() {
+ enableFilterForm();
+ enableCheckboxes();
+ handleBulkActions();
- ['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
- if (ajaxify.data.filters.hasOwnProperty('filter')) {
- selected.set(filter, ajaxify.data.selected[filter]);
- }
- const filterEl = $(`[component="flags/filter/${filter}"]`);
- userFilter.init(filterEl, {
- selectedUsers: selected.get(filter),
- template: 'partials/flags/filters',
- selectedBlock: `selected.${filter}`,
- onSelect: function (_selectedUsers) {
- selected.set(filter, _selectedUsers);
- },
- onHidden: function () {
- applyFilters();
- },
- });
- });
-
- components.get('flags/list')
- .on('click', '[data-flag-id]', function (e) {
- if (['BUTTON', 'A'].includes(e.target.nodeName)) {
- return;
- }
-
- const flagId = this.getAttribute('data-flag-id');
- ajaxify.go('flags/' + flagId);
- });
-
- $('#flags-daily-wrapper').one('shown.bs.collapse', function () {
- Flags.handleGraphs();
- });
-
- autocomplete.user($('#filter-assignee, #filter-targetUid, #filter-reporterId'), (ev, ui) => {
- setTimeout(() => { ev.target.value = ui.item.user.uid; });
- });
- };
-
- Flags.enableFilterForm = function () {
- const $filtersEl = components.get('flags/filters');
- if ($filtersEl && $filtersEl.get(0).nodeName !== 'FORM') {
- // Harmony; update hidden form and submit on change
- const filtersEl = $filtersEl.get(0);
- const formEl = filtersEl.querySelector('form');
-
- filtersEl.addEventListener('click', (e) => {
- const subselector = e.target.closest('[data-value]');
- if (!subselector) {
- return;
- }
-
- const name = subselector.getAttribute('data-name');
- const value = subselector.getAttribute('data-value');
-
- formEl[name].value = value;
-
- applyFilters();
- });
- } else {
- // Persona; parse ajaxify data to set form values to reflect current filters
- for (const filter in ajaxify.data.filters) {
- if (ajaxify.data.filters.hasOwnProperty(filter)) {
- $filtersEl.find('[name="' + filter + '"]').val(ajaxify.data.filters[filter]);
- }
- }
- $filtersEl.find('[name="sort"]').val(ajaxify.data.sort);
-
- document.getElementById('apply-filters').addEventListener('click', function () {
- applyFilters();
- });
-
- $filtersEl.find('button[data-target="#more-filters"]').click((ev) => {
- const textVariant = ev.target.getAttribute('data-text-variant');
- if (!textVariant) {
- return;
- }
- ev.target.setAttribute('data-text-variant', ev.target.textContent);
- ev.target.firstChild.textContent = textVariant;
- });
- }
- };
-
- function applyFilters() {
- let formEl = components.get('flags/filters').get(0);
- if (!formEl) {
- return;
- }
- if (formEl.nodeName !== 'FORM') {
- formEl = formEl.querySelector('form');
- }
-
- const payload = new FormData(formEl);
-
- // cid is special comes from categoryFilter module
- selected.get('cids').forEach(function (cid) {
- payload.append('cid', cid);
- });
-
- // these three fields are special; comes from userFilter module
- ['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
- selected.get(filter).forEach(({ uid }) => {
- payload.append(filter, uid);
- });
- });
-
- const length = Array.from(payload.values()).filter(Boolean);
- const qs = new URLSearchParams(payload).toString();
-
- ajaxify.go('flags?' + (length ? qs : 'reset=1'));
+ if (ajaxify.data.filters.hasOwnProperty('cid')) {
+ selected.set('cids', Array.isArray(ajaxify.data.filters.cid) ?
+ ajaxify.data.filters.cid : [ajaxify.data.filters.cid]);
}
- Flags.enableCheckboxes = function () {
- const flagsList = document.querySelector('[component="flags/list"]');
- const checkboxes = flagsList.querySelectorAll('[data-flag-id] input[type="checkbox"]');
- const bulkEl = document.querySelector('[component="flags/bulk-actions"] button');
- let lastClicked;
+ categoryFilter.init($('[component="category/dropdown"]'), {
+ privilege: 'moderate',
+ selectedCids: selected.get('cids'),
+ updateButton: function ({ selectedCids: cids }) {
+ selected.set('cids', cids);
+ applyFilters();
+ },
+ });
- document.querySelector('[data-action="toggle-all"]').addEventListener('click', function () {
- const state = this.checked;
+ ['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
+ if (ajaxify.data.filters.hasOwnProperty('filter')) {
+ selected.set(filter, ajaxify.data.selected[filter]);
+ }
+ const filterEl = $(`[component="flags/filter/${filter}"]`);
+ userFilter.init(filterEl, {
+ selectedUsers: selected.get(filter),
+ template: 'partials/flags/filters',
+ selectedBlock: `selected.${filter}`,
+ onSelect: function (_selectedUsers) {
+ selected.set(filter, _selectedUsers);
+ },
+ onHidden: function () {
+ applyFilters();
+ },
+ });
+ });
- checkboxes.forEach(function (el) {
- el.checked = state;
- });
- bulkEl.disabled = !state;
+ components.get('flags/list')
+ .on('click', '[data-flag-id]', function (e) {
+ if (['BUTTON', 'A'].includes(e.target.nodeName)) {
+ return;
+ }
+
+ const flagId = this.getAttribute('data-flag-id');
+ ajaxify.go('flags/' + flagId);
});
- flagsList.addEventListener('click', function (e) {
- const subselector = e.target.closest('input[type="checkbox"]');
- if (subselector) {
- // Stop checkbox clicks from going into the flag details
- e.stopImmediatePropagation();
+ $('#flags-daily-wrapper').one('shown.bs.collapse', function () {
+ handleGraphs();
+ });
- if (lastClicked && e.shiftKey && lastClicked !== subselector) {
- // Select all the checkboxes in between
- const state = subselector.checked;
- let started = false;
+ autocomplete.user($('#filter-assignee, #filter-targetUid, #filter-reporterId'), (ev, ui) => {
+ setTimeout(() => { ev.target.value = ui.item.user.uid; });
+ });
+}
- checkboxes.forEach(function (el) {
- if ([subselector, lastClicked].some(function (ref) {
- return ref === el;
- })) {
- started = !started;
- }
+export function enableFilterForm() {
+ const $filtersEl = components.get('flags/filters');
+ if ($filtersEl && $filtersEl.get(0).nodeName !== 'FORM') {
+ // Harmony; update hidden form and submit on change
+ const filtersEl = $filtersEl.get(0);
+ const formEl = filtersEl.querySelector('form');
- if (started) {
- el.checked = state;
- }
- });
+ filtersEl.addEventListener('click', (e) => {
+ const subselector = e.target.closest('[data-value]');
+ if (!subselector) {
+ return;
+ }
+
+ const name = subselector.getAttribute('data-name');
+ const value = subselector.getAttribute('data-value');
+
+ formEl[name].value = value;
+
+ applyFilters();
+ });
+ } else {
+ // Persona; parse ajaxify data to set form values to reflect current filters
+ for (const filter in ajaxify.data.filters) {
+ if (ajaxify.data.filters.hasOwnProperty(filter)) {
+ $filtersEl.find('[name="' + filter + '"]').val(ajaxify.data.filters[filter]);
+ }
+ }
+ $filtersEl.find('[name="sort"]').val(ajaxify.data.sort);
+
+ document.getElementById('apply-filters').addEventListener('click', function () {
+ applyFilters();
+ });
+
+ $filtersEl.find('button[data-target="#more-filters"]').click((ev) => {
+ const textVariant = ev.target.getAttribute('data-text-variant');
+ if (!textVariant) {
+ return;
+ }
+ ev.target.setAttribute('data-text-variant', ev.target.textContent);
+ ev.target.firstChild.textContent = textVariant;
+ });
+ }
+}
+
+function applyFilters() {
+ let formEl = components.get('flags/filters').get(0);
+ if (!formEl) {
+ return;
+ }
+ if (formEl.nodeName !== 'FORM') {
+ formEl = formEl.querySelector('form');
+ }
+
+ const payload = new FormData(formEl);
+
+ // cid is special comes from categoryFilter module
+ selected.get('cids').forEach(function (cid) {
+ payload.append('cid', cid);
+ });
+
+ // these three fields are special; comes from userFilter module
+ ['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
+ selected.get(filter).forEach(({ uid }) => {
+ payload.append(filter, uid);
+ });
+ });
+
+ const length = Array.from(payload.values()).filter(Boolean);
+ const qs = new URLSearchParams(payload).toString();
+
+ ajaxify.go('flags?' + (length ? qs : 'reset=1'));
+}
+
+export function enableCheckboxes() {
+ const flagsList = document.querySelector('[component="flags/list"]');
+ const checkboxes = flagsList.querySelectorAll('[data-flag-id] input[type="checkbox"]');
+ const bulkEl = document.querySelector('[component="flags/bulk-actions"] button');
+ let lastClicked;
+
+ document.querySelector('[data-action="toggle-all"]').addEventListener('click', function () {
+ const state = this.checked;
+
+ checkboxes.forEach(function (el) {
+ el.checked = state;
+ });
+ bulkEl.disabled = !state;
+ });
+
+ flagsList.addEventListener('click', function (e) {
+ const subselector = e.target.closest('input[type="checkbox"]');
+ if (subselector) {
+ // Stop checkbox clicks from going into the flag details
+ e.stopImmediatePropagation();
+
+ if (lastClicked && e.shiftKey && lastClicked !== subselector) {
+ // Select all the checkboxes in between
+ const state = subselector.checked;
+ let started = false;
+
+ checkboxes.forEach(function (el) {
+ if ([subselector, lastClicked].some(function (ref) {
+ return ref === el;
+ })) {
+ started = !started;
+ }
+
+ if (started) {
+ el.checked = state;
+ }
+ });
+ }
+
+ // (De)activate bulk actions button based on checkboxes' state
+ bulkEl.disabled = !Array.prototype.some.call(checkboxes, function (el) {
+ return el.checked;
+ });
+
+ lastClicked = subselector;
+ }
+
+ // If you miss the checkbox, don't descend into the flag details, either
+ if (e.target.querySelector('input[type="checkbox"]')) {
+ e.stopImmediatePropagation();
+ }
+ });
+}
+
+export function handleBulkActions() {
+ document.querySelector('[component="flags/bulk-actions"]').addEventListener('click', function (e) {
+ const subselector = e.target.closest('[data-action]');
+ if (subselector) {
+ const action = subselector.getAttribute('data-action');
+ const flagIds = getSelected();
+ const promises = flagIds.map((flagId) => {
+ const data = {};
+ if (action === 'bulk-assign') {
+ data.assignee = app.user.uid;
+ } else if (action === 'bulk-mark-resolved') {
+ data.state = 'resolved';
+ }
+ return api.put(`/flags/${flagId}`, data);
+ });
+
+ Promise.allSettled(promises).then(function (results) {
+ const fulfilled = results.filter(function (res) {
+ return res.status === 'fulfilled';
+ }).length;
+ const errors = results.filter(function (res) {
+ return res.status === 'rejected';
+ });
+ if (fulfilled) {
+ alerts.success('[[flags:bulk-success, ' + fulfilled + ']]');
+ ajaxify.refresh();
}
- // (De)activate bulk actions button based on checkboxes' state
- bulkEl.disabled = !Array.prototype.some.call(checkboxes, function (el) {
- return el.checked;
+ errors.forEach(function (res) {
+ alerts.error(res.reason);
});
-
- lastClicked = subselector;
- }
-
- // If you miss the checkbox, don't descend into the flag details, either
- if (e.target.querySelector('input[type="checkbox"]')) {
- e.stopImmediatePropagation();
- }
- });
- };
-
- Flags.handleBulkActions = function () {
- document.querySelector('[component="flags/bulk-actions"]').addEventListener('click', function (e) {
- const subselector = e.target.closest('[data-action]');
- if (subselector) {
- const action = subselector.getAttribute('data-action');
- const flagIds = Flags.getSelected();
- const promises = flagIds.map((flagId) => {
- const data = {};
- if (action === 'bulk-assign') {
- data.assignee = app.user.uid;
- } else if (action === 'bulk-mark-resolved') {
- data.state = 'resolved';
- }
- return api.put(`/flags/${flagId}`, data);
- });
-
- Promise.allSettled(promises).then(function (results) {
- const fulfilled = results.filter(function (res) {
- return res.status === 'fulfilled';
- }).length;
- const errors = results.filter(function (res) {
- return res.status === 'rejected';
- });
- if (fulfilled) {
- alerts.success('[[flags:bulk-success, ' + fulfilled + ']]');
- ajaxify.refresh();
- }
-
- errors.forEach(function (res) {
- alerts.error(res.reason);
- });
- });
- }
- });
- };
-
- Flags.getSelected = function () {
- const checkboxes = document.querySelectorAll('[component="flags/list"] [data-flag-id] input[type="checkbox"]');
- const payload = [];
- checkboxes.forEach(function (el) {
- if (el.checked) {
- payload.push(el.closest('[data-flag-id]').getAttribute('data-flag-id'));
- }
- });
-
- return payload;
- };
-
- Flags.handleGraphs = function () {
- const dailyCanvas = document.getElementById('flags:daily');
- const dailyLabels = utils.getDaysArray().map(function (text, idx) {
- return idx % 3 ? '' : text;
- });
-
- if (utils.isMobile()) {
- Chart.defaults.plugins.tooltip.enabled = false;
+ });
}
- const data = {
- 'flags:daily': {
- labels: dailyLabels,
- datasets: [
- {
- label: '',
- 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: ajaxify.data.analytics,
- },
- ],
- },
- };
+ });
+}
- dailyCanvas.width = $(dailyCanvas).parent().width();
- new Chart(dailyCanvas.getContext('2d'), {
- type: 'line',
- data: data['flags:daily'],
- options: {
- responsive: true,
- animation: false,
- plugins: {
- legend: {
- display: false,
- },
+export function getSelected() {
+ const checkboxes = document.querySelectorAll('[component="flags/list"] [data-flag-id] input[type="checkbox"]');
+ const payload = [];
+ checkboxes.forEach(function (el) {
+ if (el.checked) {
+ payload.push(el.closest('[data-flag-id]').getAttribute('data-flag-id'));
+ }
+ });
+
+ return payload;
+}
+
+export function handleGraphs() {
+ const dailyCanvas = document.getElementById('flags:daily');
+ const dailyLabels = utils.getDaysArray().map(function (text, idx) {
+ return idx % 3 ? '' : text;
+ });
+
+ if (utils.isMobile()) {
+ Chart.defaults.plugins.tooltip.enabled = false;
+ }
+ const data = {
+ 'flags:daily': {
+ labels: dailyLabels,
+ datasets: [
+ {
+ label: '',
+ 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: ajaxify.data.analytics,
},
- scales: {
- y: {
- beginAtZero: true,
- },
- },
- },
- });
+ ],
+ },
};
- return Flags;
-});
+ dailyCanvas.width = $(dailyCanvas).parent().width();
+ new Chart(dailyCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['flags:daily'],
+ options: {
+ responsive: true,
+ animation: false,
+ scales: {
+ y: {
+ beginAtZero: true,
+ },
+ },
+ },
+ });
+}
+
diff --git a/public/src/client/ip-blacklist.js b/public/src/client/ip-blacklist.js
index b94c84038a..a5a1ed051e 100644
--- a/public/src/client/ip-blacklist.js
+++ b/public/src/client/ip-blacklist.js
@@ -1,126 +1,129 @@
-'use strict';
+import {
+ Chart,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ LineElement,
+ PointElement,
+ Tooltip,
+ Filler,
+} from 'chart.js';
-define('forum/ip-blacklist', [
- 'chart.js/auto', 'benchpress', 'bootbox', 'alerts',
-], function ({ Chart }, Benchpress, bootbox, alerts) {
- const Blacklist = {};
+import * as Benchpress from 'benchpressjs';
+import * as bootbox from 'bootbox';
+import * as alerts from '../modules/alerts';
- Blacklist.init = function () {
- const blacklist = $('#blacklist-rules');
+Chart.register(LineController, CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler);
- blacklist.on('keyup', function () {
- $('#blacklist-rules-holder').val(blacklist.val());
- });
+export function init() {
+ const blacklist = $('#blacklist-rules');
- $('[data-action="apply"]').on('click', function () {
- socket.emit('blacklist.save', blacklist.val(), function (err) {
- if (err) {
- return alerts.error(err);
- }
- alerts.alert({
- type: 'success',
- alert_id: 'blacklist-saved',
- title: '[[ip-blacklist:alerts.applied-success]]',
- });
+ blacklist.on('keyup', function () {
+ $('#blacklist-rules-holder').val(blacklist.val());
+ });
+
+ $('[data-action="apply"]').on('click', function () {
+ socket.emit('blacklist.save', blacklist.val(), function (err) {
+ if (err) {
+ return alerts.error(err);
+ }
+ alerts.alert({
+ type: 'success',
+ alert_id: 'blacklist-saved',
+ title: '[[ip-blacklist:alerts.applied-success]]',
});
});
+ });
- $('[data-action="test"]').on('click', function () {
- socket.emit('blacklist.validate', {
- rules: blacklist.val(),
- }, function (err, data) {
- if (err) {
- return alerts.error(err);
- }
+ $('[data-action="test"]').on('click', function () {
+ socket.emit('blacklist.validate', {
+ rules: blacklist.val(),
+ }, function (err, data) {
+ if (err) {
+ return alerts.error(err);
+ }
- Benchpress.render('admin/partials/blacklist-validate', data).then(function (html) {
- bootbox.alert(html);
- });
+ Benchpress.render('admin/partials/blacklist-validate', data).then(function (html) {
+ bootbox.alert(html);
});
});
+ });
- Blacklist.setupAnalytics();
+ setupAnalytics();
+}
+
+export function setupAnalytics() {
+ const hourlyCanvas = document.getElementById('blacklist:hourly');
+ const dailyCanvas = document.getElementById('blacklist:daily');
+ const hourlyLabels = utils.getHoursArray().map(function (text, idx) {
+ return idx % 3 ? '' : text;
+ });
+ const dailyLabels = utils.getDaysArray().slice(-7).map(function (text, idx) {
+ return idx % 3 ? '' : text;
+ });
+
+ if (utils.isMobile()) {
+ Chart.defaults.plugins.tooltip.enabled = false;
+ }
+
+ const data = {
+ 'blacklist:hourly': {
+ labels: hourlyLabels,
+ datasets: [
+ {
+ label: '',
+ fill: 'origin',
+ tension: 0.25,
+ backgroundColor: 'rgba(186,139,175,0.2)',
+ borderColor: 'rgba(186,139,175,1)',
+ pointBackgroundColor: 'rgba(186,139,175,1)',
+ pointHoverBackgroundColor: '#fff',
+ pointBorderColor: '#fff',
+ pointHoverBorderColor: 'rgba(186,139,175,1)',
+ data: ajaxify.data.analytics.hourly,
+ },
+ ],
+ },
+ 'blacklist:daily': {
+ labels: dailyLabels,
+ datasets: [
+ {
+ label: '',
+ fill: 'origin',
+ tension: 0.25,
+ 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: ajaxify.data.analytics.daily,
+ },
+ ],
+ },
};
- Blacklist.setupAnalytics = function () {
- const hourlyCanvas = document.getElementById('blacklist:hourly');
- const dailyCanvas = document.getElementById('blacklist:daily');
- const hourlyLabels = utils.getHoursArray().map(function (text, idx) {
- return idx % 3 ? '' : text;
- });
- const dailyLabels = utils.getDaysArray().slice(-7).map(function (text, idx) {
- return idx % 3 ? '' : text;
- });
-
- if (utils.isMobile()) {
- Chart.defaults.plugins.tooltip.enabled = false;
- }
-
- const data = {
- 'blacklist:hourly': {
- labels: hourlyLabels,
- datasets: [
- {
- label: '',
- fill: 'origin',
- tension: 0.25,
- backgroundColor: 'rgba(186,139,175,0.2)',
- borderColor: 'rgba(186,139,175,1)',
- pointBackgroundColor: 'rgba(186,139,175,1)',
- pointHoverBackgroundColor: '#fff',
- pointBorderColor: '#fff',
- pointHoverBorderColor: 'rgba(186,139,175,1)',
- data: ajaxify.data.analytics.hourly,
- },
- ],
+ const chartOpts = {
+ responsive: true,
+ scales: {
+ y: {
+ position: 'left',
+ type: 'linear',
+ beginAtZero: true,
},
- 'blacklist:daily': {
- labels: dailyLabels,
- datasets: [
- {
- label: '',
- 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: ajaxify.data.analytics.daily,
- },
- ],
- },
- };
-
- const chartOpts = {
- responsive: true,
- maintainAspectRatio: true,
- plugins: {
- legend: {
- display: false,
- },
- },
- scales: {
- y: {
- position: 'left',
- type: 'linear',
- beginAtZero: true,
- },
- },
- };
-
-
- new Chart(hourlyCanvas.getContext('2d'), {
- type: 'line',
- data: data['blacklist:hourly'],
- options: chartOpts,
- });
-
- new Chart(dailyCanvas.getContext('2d'), {
- type: 'line',
- data: data['blacklist:daily'],
- options: chartOpts,
- });
+ },
};
- return Blacklist;
-});
+ new Chart(hourlyCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['blacklist:hourly'],
+ options: chartOpts,
+ });
+
+ new Chart(dailyCanvas.getContext('2d'), {
+ type: 'line',
+ data: data['blacklist:daily'],
+ options: chartOpts,
+ });
+}
+
diff --git a/src/views/admin/partials/dashboard/graph.tpl b/src/views/admin/partials/dashboard/graph.tpl
index e9ee7b6bf8..193ebfdee3 100644
--- a/src/views/admin/partials/dashboard/graph.tpl
+++ b/src/views/admin/partials/dashboard/graph.tpl
@@ -10,7 +10,7 @@