refactor: use esm only import used chart types/plugins

This commit is contained in:
Barış Soner Uşaklı
2023-10-01 18:33:52 -04:00
parent 1bb98ba9a2
commit 56b31674e1
7 changed files with 1406 additions and 1347 deletions

View File

@@ -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', [ // eslint-disable-next-line import/prefer-default-export
'bootbox', 'alerts', 'chart.js/auto', export function init() {
], function (bootbox, alerts, { Chart }) { setupCharts();
const Errors = {};
Errors.init = function () { $('[data-action="clear"]').on('click', clear404);
Errors.setupCharts(); }
$('[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 () { ajaxify.refresh();
bootbox.confirm('[[admin/advanced/errors:clear404-confirm]]', function (ok) { alerts.success('[[admin/advanced/errors:clear404-success]]');
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;
} }
});
}
const data = { function setupCharts() {
'not-found': { const notFoundCanvas = document.getElementById('not-found');
labels: dailyLabels, const tooBusyCanvas = document.getElementById('toobusy');
datasets: [ let dailyLabels = utils.getDaysArray();
{
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,
},
],
},
};
new Chart(notFoundCanvas.getContext('2d'), { dailyLabels = dailyLabels.slice(-7);
type: 'line',
data: data['not-found'],
options: {
responsive: true,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
});
new Chart(tooBusyCanvas.getContext('2d'), { if (utils.isMobile()) {
type: 'line', Chart.defaults.plugins.tooltip.enabled = false;
data: data.toobusy, }
options: {
responsive: true, const data = {
plugins: { 'not-found': {
legend: { labels: dailyLabels,
display: false, 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,
},
},
},
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -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.register(
'chart.js/auto', 'categorySelector', LineController,
], function ({ Chart }, categorySelector) { CategoryScale,
const CategoryAnalytics = {}; LinearScale,
LineElement,
PointElement,
Tooltip,
Filler
);
CategoryAnalytics.init = function () { // eslint-disable-next-line import/prefer-default-export
categorySelector.init($('[component="category-selector"]'), { export function init() {
onSelect: function (selectedCategory) { categorySelector.init($('[component="category-selector"]'), {
ajaxify.go('admin/manage/categories/' + selectedCategory.cid + '/analytics'); onSelect: function (selectedCategory) {
}, ajaxify.go('admin/manage/categories/' + selectedCategory.cid + '/analytics');
showLinks: true, },
template: 'admin/partials/category/selector-dropdown-right', showLinks: true,
}); template: 'admin/partials/category/selector-dropdown-right',
});
const hourlyCanvas = document.getElementById('pageviews:hourly'); const hourlyCanvas = document.getElementById('pageviews:hourly');
const dailyCanvas = document.getElementById('pageviews:daily'); const dailyCanvas = document.getElementById('pageviews:daily');
const topicsCanvas = document.getElementById('topics:daily'); const topicsCanvas = document.getElementById('topics:daily');
const postsCanvas = document.getElementById('posts:daily'); const postsCanvas = document.getElementById('posts:daily');
const hourlyLabels = utils.getHoursArray().map(function (text, idx) { const hourlyLabels = utils.getHoursArray().map(function (text, idx) {
return idx % 3 ? '' : text; return idx % 3 ? '' : text;
}); });
const dailyLabels = utils.getDaysArray().map(function (text, idx) { const dailyLabels = utils.getDaysArray().map(function (text, idx) {
return idx % 3 ? '' : text; return idx % 3 ? '' : text;
}); });
if (utils.isMobile()) { if (utils.isMobile()) {
Chart.defaults.plugins.tooltip.enabled = false; Chart.defaults.plugins.tooltip.enabled = false;
} }
const commonDataSetOpts = { const commonDataSetOpts = {
label: '', label: '',
fill: true, fill: true,
tension: 0.25, tension: 0.25,
pointHoverBackgroundColor: '#fff', pointHoverBackgroundColor: '#fff',
pointBorderColor: '#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,
});
}; };
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,
});
}

View File

@@ -1,194 +1,215 @@
'use strict'; import {
Chart,
LineController,
CategoryScale,
LinearScale,
LineElement,
PointElement,
Tooltip,
Filler,
} from 'chart.js';
define('admin/modules/dashboard-line-graph', [ import * as Benchpress from 'benchpressjs';
'chart.js/auto', 'translator', 'benchpress', 'api', 'hooks', 'bootbox', import * as bootbox from 'bootbox';
], function ({ Chart }, translator, Benchpress, api, hooks, bootbox) { import * as translator from '../../modules/translator';
const Graph = { import * as api from '../../modules/api';
_current: null, import * as hooks from '../../modules/hooks';
};
let isMobile = false;
Graph.init = ({ set, dataset }) => {
const canvas = document.getElementById('analytics-traffic');
const canvasCtx = canvas.getContext('2d');
const trafficLabels = utils.getHoursArray();
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); Chart.register(
if (isMobile) { LineController,
Chart.defaults.plugins.tooltip.enabled = false; CategoryScale,
} LinearScale,
LineElement,
PointElement,
Tooltip,
Filler
);
Graph.handleUpdateControls({ set });
const t = translator.Translator.create(); let _current = null;
return new Promise((resolve) => { let isMobile = false;
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(); // 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, { handleUpdateControls({ set });
type: 'line',
data: data, const t = translator.Translator.create();
options: { return new Promise((resolve) => {
responsive: true, t.translateKey(`admin/menu:${ajaxify.data.template.name.replace('admin/', '')}`, []).then((key) => {
scales: { const data = {
'left-y-axis': { labels: trafficLabels,
type: 'linear', datasets: [
position: 'left', {
beginAtZero: true, label: key,
title: { fill: true,
display: true, tension: 0.25,
text: key, backgroundColor: 'rgba(151,187,205,0.2)',
}, borderColor: 'rgba(151,187,205,1)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointHoverBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: '#fff',
pointHoverBorderColor: 'rgba(151,187,205,1)',
data: dataset || [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',
},
}, },
}); interaction: {
intersect: false,
if (!dataset) { mode: 'index',
Graph.update(set).then(resolve); },
} else { },
resolve(Graph._current);
}
}); });
});
};
Graph.handleUpdateControls = ({ set }) => { if (!dataset) {
$('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () { update(set).then(resolve);
let until = new Date(); } else {
const amount = $(this).attr('data-amount'); resolve(_current);
if ($(this).attr('data-units') === 'days') {
until.setHours(0, 0, 0, 0);
} }
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 () { function handleUpdateControls({ set }) {
const targetEl = $(this); $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () {
let until = new Date();
Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) { const amount = $(this).attr('data-amount');
const modal = bootbox.dialog({ if ($(this).attr('data-units') === 'days') {
title: '[[admin/dashboard:page-views-custom]]', until.setHours(0, 0, 0, 0);
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]]'));
} }
until = until.getTime();
update(set, $(this).attr('data-units'), until, amount);
return new Promise((resolve) => { require(['translator'], function (translator) {
api.get(`/admin/analytics/${set}`, { units, until, amount }).then((dataset) => { translator.translate('[[admin/dashboard:page-views-custom]]', function (translated) {
if (units === 'days') { $('[data-action="updateGraph"][data-units="custom"]').text(translated);
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);
}); });
}); });
}; });
$('[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;
});

View File

@@ -1,300 +1,301 @@
'use strict'; import {
Chart,
LineController,
CategoryScale,
LinearScale,
LineElement,
PointElement,
Tooltip,
Filler,
} from 'chart.js';
define('forum/flags/list', [ import * as categoryFilter from '../../modules/categoryFilter';
'components', 'chart.js/auto', 'categoryFilter', import * as userFilter from '../../modules/userFilter';
'autocomplete', 'api', 'alerts', import * as autocomplete from '../../modules/autocomplete';
'userFilter', import * as api from '../../modules/api';
], function ( import * as alerts from '../../modules/alerts';
components, { Chart }, categoryFilter, import * as components from '../../modules/components';
autocomplete, api, alerts,
userFilter
) {
const Flags = {};
const selected = new Map([
['cids', []],
['assignee', []],
['targetUid', []],
['reporterId', []],
]);
Flags.init = function () { Chart.register(LineController, CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler);
Flags.enableFilterForm();
Flags.enableCheckboxes();
Flags.handleBulkActions();
if (ajaxify.data.filters.hasOwnProperty('cid')) { const selected = new Map([
selected.set('cids', Array.isArray(ajaxify.data.filters.cid) ? ['cids', []],
ajaxify.data.filters.cid : [ajaxify.data.filters.cid]); ['assignee', []],
} ['targetUid', []],
['reporterId', []],
]);
categoryFilter.init($('[component="category/dropdown"]'), { export function init() {
privilege: 'moderate', enableFilterForm();
selectedCids: selected.get('cids'), enableCheckboxes();
updateButton: function ({ selectedCids: cids }) { handleBulkActions();
selected.set('cids', cids);
applyFilters();
},
});
['assignee', 'targetUid', 'reporterId'].forEach((filter) => { if (ajaxify.data.filters.hasOwnProperty('cid')) {
if (ajaxify.data.filters.hasOwnProperty('filter')) { selected.set('cids', Array.isArray(ajaxify.data.filters.cid) ?
selected.set(filter, ajaxify.data.selected[filter]); ajaxify.data.filters.cid : [ajaxify.data.filters.cid]);
}
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'));
} }
Flags.enableCheckboxes = function () { categoryFilter.init($('[component="category/dropdown"]'), {
const flagsList = document.querySelector('[component="flags/list"]'); privilege: 'moderate',
const checkboxes = flagsList.querySelectorAll('[data-flag-id] input[type="checkbox"]'); selectedCids: selected.get('cids'),
const bulkEl = document.querySelector('[component="flags/bulk-actions"] button'); updateButton: function ({ selectedCids: cids }) {
let lastClicked; selected.set('cids', cids);
applyFilters();
},
});
document.querySelector('[data-action="toggle-all"]').addEventListener('click', function () { ['assignee', 'targetUid', 'reporterId'].forEach((filter) => {
const state = this.checked; 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) { components.get('flags/list')
el.checked = state; .on('click', '[data-flag-id]', function (e) {
}); if (['BUTTON', 'A'].includes(e.target.nodeName)) {
bulkEl.disabled = !state; return;
}
const flagId = this.getAttribute('data-flag-id');
ajaxify.go('flags/' + flagId);
}); });
flagsList.addEventListener('click', function (e) { $('#flags-daily-wrapper').one('shown.bs.collapse', function () {
const subselector = e.target.closest('input[type="checkbox"]'); handleGraphs();
if (subselector) { });
// Stop checkbox clicks from going into the flag details
e.stopImmediatePropagation();
if (lastClicked && e.shiftKey && lastClicked !== subselector) { autocomplete.user($('#filter-assignee, #filter-targetUid, #filter-reporterId'), (ev, ui) => {
// Select all the checkboxes in between setTimeout(() => { ev.target.value = ui.item.user.uid; });
const state = subselector.checked; });
let started = false; }
checkboxes.forEach(function (el) { export function enableFilterForm() {
if ([subselector, lastClicked].some(function (ref) { const $filtersEl = components.get('flags/filters');
return ref === el; if ($filtersEl && $filtersEl.get(0).nodeName !== 'FORM') {
})) { // Harmony; update hidden form and submit on change
started = !started; const filtersEl = $filtersEl.get(0);
} const formEl = filtersEl.querySelector('form');
if (started) { filtersEl.addEventListener('click', (e) => {
el.checked = state; 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 errors.forEach(function (res) {
bulkEl.disabled = !Array.prototype.some.call(checkboxes, function (el) { alerts.error(res.reason);
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();
}
});
};
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(); export function getSelected() {
new Chart(dailyCanvas.getContext('2d'), { const checkboxes = document.querySelectorAll('[component="flags/list"] [data-flag-id] input[type="checkbox"]');
type: 'line', const payload = [];
data: data['flags:daily'], checkboxes.forEach(function (el) {
options: { if (el.checked) {
responsive: true, payload.push(el.closest('[data-flag-id]').getAttribute('data-flag-id'));
animation: false, }
plugins: { });
legend: {
display: false, 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,
},
},
},
});
}

View File

@@ -1,126 +1,129 @@
'use strict'; import {
Chart,
LineController,
CategoryScale,
LinearScale,
LineElement,
PointElement,
Tooltip,
Filler,
} from 'chart.js';
define('forum/ip-blacklist', [ import * as Benchpress from 'benchpressjs';
'chart.js/auto', 'benchpress', 'bootbox', 'alerts', import * as bootbox from 'bootbox';
], function ({ Chart }, Benchpress, bootbox, alerts) { import * as alerts from '../modules/alerts';
const Blacklist = {};
Blacklist.init = function () { Chart.register(LineController, CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler);
const blacklist = $('#blacklist-rules');
blacklist.on('keyup', function () { export function init() {
$('#blacklist-rules-holder').val(blacklist.val()); const blacklist = $('#blacklist-rules');
});
$('[data-action="apply"]').on('click', function () { blacklist.on('keyup', function () {
socket.emit('blacklist.save', blacklist.val(), function (err) { $('#blacklist-rules-holder').val(blacklist.val());
if (err) { });
return alerts.error(err);
} $('[data-action="apply"]').on('click', function () {
alerts.alert({ socket.emit('blacklist.save', blacklist.val(), function (err) {
type: 'success', if (err) {
alert_id: 'blacklist-saved', return alerts.error(err);
title: '[[ip-blacklist:alerts.applied-success]]', }
}); alerts.alert({
type: 'success',
alert_id: 'blacklist-saved',
title: '[[ip-blacklist:alerts.applied-success]]',
}); });
}); });
});
$('[data-action="test"]').on('click', function () { $('[data-action="test"]').on('click', function () {
socket.emit('blacklist.validate', { socket.emit('blacklist.validate', {
rules: blacklist.val(), rules: blacklist.val(),
}, function (err, data) { }, function (err, data) {
if (err) { if (err) {
return alerts.error(err); return alerts.error(err);
} }
Benchpress.render('admin/partials/blacklist-validate', data).then(function (html) { Benchpress.render('admin/partials/blacklist-validate', data).then(function (html) {
bootbox.alert(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 chartOpts = {
const hourlyCanvas = document.getElementById('blacklist:hourly'); responsive: true,
const dailyCanvas = document.getElementById('blacklist:daily'); scales: {
const hourlyLabels = utils.getHoursArray().map(function (text, idx) { y: {
return idx % 3 ? '' : text; position: 'left',
}); type: 'linear',
const dailyLabels = utils.getDaysArray().slice(-7).map(function (text, idx) { beginAtZero: true,
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: '',
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,
});
}

View File

@@ -10,7 +10,7 @@
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="graph-container position-relative" id="analytics-traffic-container" style="aspect-ratio: 16/9;"> <div class="graph-container position-relative" id="analytics-traffic-container" style="aspect-ratio: 2;">
<canvas id="analytics-traffic" ></canvas> <canvas id="analytics-traffic" ></canvas>
</div> </div>
<hr/> <hr/>