mirror of
https://github.com/flarum/core.git
synced 2025-07-31 13:40:20 +02:00
perf(statistics): rewrite for performance on very large communities (#3531)
Co-authored-by: Sami Mazouz <ilyasmazouz@gmail.com>
This commit is contained in:
@@ -7,15 +7,15 @@
|
||||
"frappe-charts": "^1.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mithril": "^2.0.8",
|
||||
"prettier": "^2.5.1",
|
||||
"@types/mithril": "^2.0.11",
|
||||
"prettier": "^2.7.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"@flarum/prettier-config": "^1.0.0",
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript-coverage-report": "^0.6.1"
|
||||
"typescript": "^4.7.4",
|
||||
"typescript-coverage-report": "^0.6.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
|
@@ -0,0 +1,82 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
|
||||
import abbreviateNumber from 'flarum/common/utils/abbreviateNumber';
|
||||
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export default class MiniStatisticsWidget extends DashboardWidget {
|
||||
entities = ['users', 'discussions', 'posts'];
|
||||
|
||||
lifetimeData: any;
|
||||
|
||||
loadingLifetime = true;
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<IDashboardWidgetAttrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.loadLifetimeData();
|
||||
}
|
||||
|
||||
async loadLifetimeData() {
|
||||
this.loadingLifetime = true;
|
||||
m.redraw();
|
||||
|
||||
const data = await app.request({
|
||||
method: 'GET',
|
||||
url: app.forum.attribute('apiUrl') + '/statistics',
|
||||
params: {
|
||||
period: 'lifetime',
|
||||
},
|
||||
});
|
||||
|
||||
this.lifetimeData = data;
|
||||
this.loadingLifetime = false;
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'StatisticsWidget StatisticsWidget--mini';
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="StatisticsWidget-table">
|
||||
<h4 className="StatisticsWidget-title">{app.translator.trans('flarum-statistics.admin.statistics.mini_heading')}</h4>
|
||||
|
||||
<div className="StatisticsWidget-entities">
|
||||
<div className="StatisticsWidget-labels">
|
||||
<div className="StatisticsWidget-label">{app.translator.trans('flarum-statistics.admin.statistics.total_label')}</div>
|
||||
</div>
|
||||
|
||||
{this.entities.map((entity) => {
|
||||
const totalCount = this.loadingLifetime ? app.translator.trans('flarum-statistics.admin.statistics.loading') : this.getTotalCount(entity);
|
||||
|
||||
return (
|
||||
<div className="StatisticsWidget-entity">
|
||||
<h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3>
|
||||
<div className="StatisticsWidget-total" title={totalCount}>
|
||||
{this.loadingLifetime ? <LoadingIndicator display="inline" /> : abbreviateNumber(totalCount as number)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="StatisticsWidget-viewFull">
|
||||
<Link href={app.route('extension', { id: 'flarum-statistics' })}>
|
||||
{app.translator.trans('flarum-statistics.admin.statistics.view_full')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getTotalCount(entity: string): number {
|
||||
return this.lifetimeData[entity];
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
import ExtensionPage from 'flarum/admin/components/ExtensionPage';
|
||||
|
||||
import StatisticsWidget from './StatisticsWidget';
|
||||
|
||||
export default class StatisticsPage extends ExtensionPage {
|
||||
content() {
|
||||
return (
|
||||
<div className="StatisticsPage">
|
||||
<div className="container">
|
||||
<StatisticsWidget />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import app from 'flarum/admin/app';
|
||||
import DashboardWidget from 'flarum/admin/components/DashboardWidget';
|
||||
import SelectDropdown from 'flarum/common/components/SelectDropdown';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import abbreviateNumber from 'flarum/common/utils/abbreviateNumber';
|
||||
|
||||
import { Chart } from 'frappe-charts/dist/frappe-charts.esm.js';
|
||||
|
||||
export default class StatisticsWidget extends DashboardWidget {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
// Create a Date object which represents the start of the day in the
|
||||
// configured timezone. To do this we convert a UTC time into that timezone,
|
||||
// reset to the first hour of the day, and then convert back into UTC time.
|
||||
// We'll be working with seconds rather than milliseconds throughout too.
|
||||
let today = new Date();
|
||||
today.setTime(today.getTime() + app.data.statistics.timezoneOffset * 1000);
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
today.setTime(today.getTime() - app.data.statistics.timezoneOffset * 1000);
|
||||
today = today / 1000;
|
||||
|
||||
this.entities = ['users', 'discussions', 'posts'];
|
||||
this.periods = {
|
||||
today: { start: today, end: today + 86400, step: 3600 },
|
||||
last_7_days: { start: today - 86400 * 7, end: today, step: 86400 },
|
||||
last_28_days: { start: today - 86400 * 28, end: today, step: 86400 },
|
||||
last_12_months: { start: today - 86400 * 364, end: today, step: 86400 * 7 },
|
||||
};
|
||||
|
||||
this.selectedEntity = 'users';
|
||||
this.selectedPeriod = 'last_7_days';
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'StatisticsWidget';
|
||||
}
|
||||
|
||||
content() {
|
||||
const thisPeriod = this.periods[this.selectedPeriod];
|
||||
|
||||
return (
|
||||
<div className="StatisticsWidget-table">
|
||||
<div className="StatisticsWidget-labels">
|
||||
<div className="StatisticsWidget-label">{app.translator.trans('flarum-statistics.admin.statistics.total_label')}</div>
|
||||
<div className="StatisticsWidget-label">
|
||||
<SelectDropdown buttonClassName="Button Button--text" caretIcon="fas fa-caret-down">
|
||||
{Object.keys(this.periods).map((period) => (
|
||||
<Button
|
||||
active={period === this.selectedPeriod}
|
||||
onclick={this.changePeriod.bind(this, period)}
|
||||
icon={period === this.selectedPeriod ? 'fas fa-check' : true}
|
||||
>
|
||||
{app.translator.trans(`flarum-statistics.admin.statistics.${period}_label`)}
|
||||
</Button>
|
||||
))}
|
||||
</SelectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.entities.map((entity) => {
|
||||
const totalCount = this.getTotalCount(entity);
|
||||
const thisPeriodCount = this.getPeriodCount(entity, thisPeriod);
|
||||
const lastPeriodCount = this.getPeriodCount(entity, this.getLastPeriod(thisPeriod));
|
||||
const periodChange = lastPeriodCount > 0 && ((thisPeriodCount - lastPeriodCount) / lastPeriodCount) * 100;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={'StatisticsWidget-entity' + (this.selectedEntity === entity ? ' active' : '')}
|
||||
onclick={this.changeEntity.bind(this, entity)}
|
||||
>
|
||||
<h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3>
|
||||
<div className="StatisticsWidget-total" title={totalCount}>
|
||||
{abbreviateNumber(totalCount)}
|
||||
</div>
|
||||
<div className="StatisticsWidget-period" title={thisPeriodCount}>
|
||||
{abbreviateNumber(thisPeriodCount)}{' '}
|
||||
{periodChange ? (
|
||||
<span className={'StatisticsWidget-change StatisticsWidget-change--' + (periodChange > 0 ? 'up' : 'down')}>
|
||||
{icon('fas fa-arrow-' + (periodChange > 0 ? 'up' : 'down'))}
|
||||
{Math.abs(periodChange.toFixed(1))}%
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="StatisticsWidget-chart" oncreate={this.drawChart.bind(this)} onupdate={this.drawChart.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
drawChart(vnode) {
|
||||
if (this.chart && this.entity === this.selectedEntity && this.period === this.selectedPeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = app.data.statistics.timezoneOffset;
|
||||
const period = this.periods[this.selectedPeriod];
|
||||
const periodLength = period.end - period.start;
|
||||
const labels = [];
|
||||
const thisPeriod = [];
|
||||
const lastPeriod = [];
|
||||
|
||||
for (let i = period.start; i < period.end; i += period.step) {
|
||||
let label;
|
||||
|
||||
if (period.step < 86400) {
|
||||
label = dayjs.unix(i + offset).format('h A');
|
||||
} else {
|
||||
label = dayjs.unix(i + offset).format('D MMM');
|
||||
|
||||
if (period.step > 86400) {
|
||||
label += ' - ' + dayjs.unix(i + offset + period.step - 1).format('D MMM');
|
||||
}
|
||||
}
|
||||
|
||||
labels.push(label);
|
||||
|
||||
thisPeriod.push(this.getPeriodCount(this.selectedEntity, { start: i, end: i + period.step }));
|
||||
|
||||
lastPeriod.push(this.getPeriodCount(this.selectedEntity, { start: i - periodLength, end: i - periodLength + period.step }));
|
||||
}
|
||||
|
||||
const datasets = [{ values: lastPeriod }, { values: thisPeriod }];
|
||||
const data = {
|
||||
labels,
|
||||
datasets,
|
||||
};
|
||||
|
||||
if (!this.chart) {
|
||||
this.chart = new Chart(vnode.dom, {
|
||||
data,
|
||||
type: 'line',
|
||||
height: 280,
|
||||
axisOptions: {
|
||||
xAxisMode: 'tick',
|
||||
yAxisMode: 'span',
|
||||
xIsSeries: true,
|
||||
},
|
||||
lineOptions: {
|
||||
hideDots: 1,
|
||||
},
|
||||
colors: ['black', app.forum.attribute('themePrimaryColor')],
|
||||
});
|
||||
} else {
|
||||
this.chart.update(data);
|
||||
}
|
||||
|
||||
this.entity = this.selectedEntity;
|
||||
this.period = this.selectedPeriod;
|
||||
}
|
||||
|
||||
changeEntity(entity) {
|
||||
this.selectedEntity = entity;
|
||||
}
|
||||
|
||||
changePeriod(period) {
|
||||
this.selectedPeriod = period;
|
||||
}
|
||||
|
||||
getTotalCount(entity) {
|
||||
return app.data.statistics[entity].total;
|
||||
}
|
||||
|
||||
getPeriodCount(entity, period) {
|
||||
const timed = app.data.statistics[entity].timed;
|
||||
let count = 0;
|
||||
|
||||
for (const time in timed) {
|
||||
if (time >= period.start && time < period.end) {
|
||||
count += parseInt(timed[time]);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
getLastPeriod(thisPeriod) {
|
||||
return {
|
||||
start: thisPeriod.start - (thisPeriod.end - thisPeriod.start),
|
||||
end: thisPeriod.start,
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,278 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
import SelectDropdown from 'flarum/common/components/SelectDropdown';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import abbreviateNumber from 'flarum/common/utils/abbreviateNumber';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
|
||||
import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget';
|
||||
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
// @ts-expect-error No typings available
|
||||
import { Chart } from 'frappe-charts';
|
||||
|
||||
interface IPeriodDeclaration {
|
||||
start: number;
|
||||
end: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export default class StatisticsWidget extends DashboardWidget {
|
||||
entities = ['users', 'discussions', 'posts'];
|
||||
periods: undefined | Record<string, IPeriodDeclaration>;
|
||||
|
||||
chart: any;
|
||||
|
||||
timedData: any;
|
||||
lifetimeData: any;
|
||||
|
||||
loadingLifetime = true;
|
||||
loadingTimed = true;
|
||||
|
||||
selectedEntity = 'users';
|
||||
selectedPeriod: undefined | string;
|
||||
|
||||
chartEntity?: string;
|
||||
chartPeriod?: string;
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<IDashboardWidgetAttrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.loadLifetimeData();
|
||||
this.loadTimedData();
|
||||
}
|
||||
|
||||
async loadLifetimeData() {
|
||||
this.loadingLifetime = true;
|
||||
m.redraw();
|
||||
|
||||
const data = await app.request({
|
||||
method: 'GET',
|
||||
url: app.forum.attribute('apiUrl') + '/statistics',
|
||||
params: {
|
||||
period: 'lifetime',
|
||||
},
|
||||
});
|
||||
|
||||
this.lifetimeData = data;
|
||||
this.loadingLifetime = false;
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
async loadTimedData() {
|
||||
this.loadingTimed = true;
|
||||
m.redraw();
|
||||
|
||||
const data = await app.request({
|
||||
method: 'GET',
|
||||
url: app.forum.attribute('apiUrl') + '/statistics',
|
||||
});
|
||||
|
||||
this.timedData = data;
|
||||
this.loadingTimed = false;
|
||||
|
||||
// Create a Date object which represents the start of the day in the
|
||||
// configured timezone. To do this we convert a UTC time into that timezone,
|
||||
// reset to the first hour of the day, and then convert back into UTC time.
|
||||
// We'll be working with seconds rather than milliseconds throughout too.
|
||||
let todayDate = new Date();
|
||||
todayDate.setTime(todayDate.getTime() + this.timedData.timezoneOffset * 1000);
|
||||
todayDate.setUTCHours(0, 0, 0, 0);
|
||||
todayDate.setTime(todayDate.getTime() - this.timedData.timezoneOffset * 1000);
|
||||
|
||||
const today = todayDate.getTime() / 1000;
|
||||
|
||||
this.periods = {
|
||||
today: { start: today, end: today + 86400, step: 3600 },
|
||||
last_7_days: { start: today - 86400 * 7, end: today, step: 86400 },
|
||||
previous_7_days: { start: today - 86400 * 14, end: today - 86400 * 7, step: 86400 },
|
||||
last_28_days: { start: today - 86400 * 28, end: today, step: 86400 },
|
||||
previous_28_days: { start: today - 86400 * 28 * 2, end: today - 86400 * 28, step: 86400 },
|
||||
last_12_months: { start: today - 86400 * 364, end: today, step: 86400 * 7 },
|
||||
};
|
||||
|
||||
this.selectedPeriod = 'last_7_days';
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'StatisticsWidget';
|
||||
}
|
||||
|
||||
content() {
|
||||
const thisPeriod = this.loadingTimed ? null : this.periods![this.selectedPeriod!];
|
||||
|
||||
return (
|
||||
<div className="StatisticsWidget-table">
|
||||
<div className="StatisticsWidget-entities">
|
||||
<div className="StatisticsWidget-labels">
|
||||
<div className="StatisticsWidget-label">{app.translator.trans('flarum-statistics.admin.statistics.total_label')}</div>
|
||||
<div className="StatisticsWidget-label">
|
||||
{this.loadingTimed ? (
|
||||
<LoadingIndicator size="small" display="inline" />
|
||||
) : (
|
||||
<SelectDropdown disabled={this.loadingTimed} buttonClassName="Button Button--text" caretIcon="fas fa-caret-down">
|
||||
{Object.keys(this.periods!).map((period) => (
|
||||
<Button
|
||||
key={period}
|
||||
active={period === this.selectedPeriod}
|
||||
onclick={this.changePeriod.bind(this, period)}
|
||||
icon={period === this.selectedPeriod ? 'fas fa-check' : true}
|
||||
>
|
||||
{app.translator.trans(`flarum-statistics.admin.statistics.${period}_label`)}
|
||||
</Button>
|
||||
))}
|
||||
</SelectDropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.entities.map((entity) => {
|
||||
const totalCount = this.loadingLifetime ? app.translator.trans('flarum-statistics.admin.statistics.loading') : this.getTotalCount(entity);
|
||||
const thisPeriodCount = this.loadingTimed
|
||||
? app.translator.trans('flarum-statistics.admin.statistics.loading')
|
||||
: this.getPeriodCount(entity, thisPeriod!);
|
||||
const lastPeriodCount = this.loadingTimed
|
||||
? app.translator.trans('flarum-statistics.admin.statistics.loading')
|
||||
: this.getPeriodCount(entity, this.getLastPeriod(thisPeriod!));
|
||||
const periodChange =
|
||||
this.loadingTimed || lastPeriodCount === 0
|
||||
? 0
|
||||
: (((thisPeriodCount as number) - (lastPeriodCount as number)) / (lastPeriodCount as number)) * 100;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={'Button--ua-reset StatisticsWidget-entity' + (this.selectedEntity === entity ? ' active' : '')}
|
||||
onclick={this.changeEntity.bind(this, entity)}
|
||||
>
|
||||
<h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3>
|
||||
<div className="StatisticsWidget-total" title={totalCount}>
|
||||
{this.loadingLifetime ? <LoadingIndicator display="inline" /> : abbreviateNumber(totalCount as number)}
|
||||
</div>
|
||||
<div className="StatisticsWidget-period" title={thisPeriodCount}>
|
||||
{this.loadingTimed ? <LoadingIndicator display="inline" /> : abbreviateNumber(thisPeriodCount as number)}
|
||||
{periodChange !== 0 && (
|
||||
<>
|
||||
{' '}
|
||||
<span className={'StatisticsWidget-change StatisticsWidget-change--' + (periodChange > 0 ? 'up' : 'down')}>
|
||||
{icon('fas fa-arrow-' + (periodChange > 0 ? 'up' : 'down'))}
|
||||
{Math.abs(periodChange).toFixed(1)}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{this.loadingTimed ? (
|
||||
<div className="StatisticsWidget-chart">
|
||||
<LoadingIndicator size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="StatisticsWidget-chart" oncreate={this.drawChart.bind(this)} onupdate={this.drawChart.bind(this)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
drawChart(vnode: Mithril.VnodeDOM<any, any>) {
|
||||
if (this.chart && this.chartEntity === this.selectedEntity && this.chartPeriod === this.selectedPeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = this.timedData.timezoneOffset;
|
||||
const period = this.periods![this.selectedPeriod!];
|
||||
const periodLength = period.end - period.start;
|
||||
const labels = [];
|
||||
const thisPeriod = [];
|
||||
const lastPeriod = [];
|
||||
|
||||
for (let i = period.start; i < period.end; i += period.step) {
|
||||
let label;
|
||||
|
||||
if (period.step < 86400) {
|
||||
label = dayjs.unix(i + offset).format('h A');
|
||||
} else {
|
||||
label = dayjs.unix(i + offset).format('D MMM');
|
||||
|
||||
if (period.step > 86400) {
|
||||
label += ' - ' + dayjs.unix(i + offset + period.step - 1).format('D MMM');
|
||||
}
|
||||
}
|
||||
|
||||
labels.push(label);
|
||||
|
||||
thisPeriod.push(this.getPeriodCount(this.selectedEntity, { start: i, end: i + period.step }));
|
||||
|
||||
lastPeriod.push(this.getPeriodCount(this.selectedEntity, { start: i - periodLength, end: i - periodLength + period.step }));
|
||||
}
|
||||
|
||||
const datasets = [{ values: lastPeriod }, { values: thisPeriod }];
|
||||
const data = {
|
||||
labels,
|
||||
datasets,
|
||||
};
|
||||
|
||||
if (!this.chart) {
|
||||
this.chart = new Chart(vnode.dom, {
|
||||
data,
|
||||
type: 'line',
|
||||
height: 280,
|
||||
axisOptions: {
|
||||
xAxisMode: 'tick',
|
||||
yAxisMode: 'span',
|
||||
xIsSeries: true,
|
||||
},
|
||||
lineOptions: {
|
||||
hideDots: 1,
|
||||
},
|
||||
colors: ['black', app.forum.attribute('themePrimaryColor')],
|
||||
});
|
||||
} else {
|
||||
this.chart.update(data);
|
||||
}
|
||||
|
||||
this.chartEntity = this.selectedEntity;
|
||||
this.chartPeriod = this.selectedPeriod;
|
||||
}
|
||||
|
||||
changeEntity(entity: string) {
|
||||
this.selectedEntity = entity;
|
||||
}
|
||||
|
||||
changePeriod(period: string) {
|
||||
this.selectedPeriod = period;
|
||||
}
|
||||
|
||||
getTotalCount(entity: string): number {
|
||||
return this.lifetimeData[entity];
|
||||
}
|
||||
|
||||
getPeriodCount(entity: string, period: { start: number; end: number }) {
|
||||
const timed: Record<string, number> = this.timedData[entity];
|
||||
let count = 0;
|
||||
|
||||
for (const t in timed) {
|
||||
const time = parseInt(t);
|
||||
|
||||
if (time >= period.start && time < period.end) {
|
||||
count += timed[time];
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
getLastPeriod(thisPeriod: { start: number; end: number }) {
|
||||
return {
|
||||
start: thisPeriod.start - (thisPeriod.end - thisPeriod.start),
|
||||
end: thisPeriod.start,
|
||||
};
|
||||
}
|
||||
}
|
@@ -3,12 +3,13 @@ import { extend } from 'flarum/common/extend';
|
||||
|
||||
import DashboardPage from 'flarum/admin/components/DashboardPage';
|
||||
|
||||
import StatisticsWidget from './components/StatisticsWidget';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import type Mithril from 'mithril';
|
||||
import MiniStatisticsWidget from './components/MiniStatisticsWidget';
|
||||
import StatisticsPage from './components/StatisticsPage';
|
||||
|
||||
app.initializers.add('flarum-statistics', () => {
|
||||
extend(DashboardPage.prototype, 'availableWidgets', function (widgets: ItemList<Mithril.Children>) {
|
||||
widgets.add('statistics', <StatisticsWidget />, 20);
|
||||
extend(DashboardPage.prototype, 'availableWidgets', function (widgets) {
|
||||
widgets.add('statistics', <MiniStatisticsWidget />, 20);
|
||||
});
|
||||
|
||||
app.extensionData.for('flarum-statistics').registerPage(StatisticsPage);
|
||||
});
|
||||
|
Reference in New Issue
Block a user