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 @@
    -
    +