mirror of
https://github.com/flarum/core.git
synced 2025-08-06 16:36:47 +02:00
Merge remote-tracking branch 'extensions_statistics/REWRITE'
This commit is contained in:
19
extensions/statistics/.editorconfig
Normal file
19
extensions/statistics/.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,xml,json}]
|
||||
indent_size = 4
|
19
extensions/statistics/.gitattributes
vendored
Normal file
19
extensions/statistics/.gitattributes
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitmodules export-ignore
|
||||
.github export-ignore
|
||||
.travis export-ignore
|
||||
.travis.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
phpunit.xml export-ignore
|
||||
tests export-ignore
|
||||
|
||||
js/dist/* -diff
|
||||
js/dist/* linguist-generated
|
||||
js/dist-typings/* linguist-generated
|
||||
js/yarn.lock -diff
|
||||
js/package-lock.json -diff
|
||||
|
||||
* text=auto eol=lf
|
15
extensions/statistics/.github/workflows/backend.yml
vendored
Normal file
15
extensions/statistics/.github/workflows/backend.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Statistics PHP
|
||||
|
||||
on: [workflow_dispatch, push, pull_request]
|
||||
|
||||
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
|
||||
# This will break your current script.
|
||||
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: flarum/.github/.github/workflows/REUSABLE_backend.yml@main
|
||||
with:
|
||||
enable_backend_testing: true
|
||||
|
||||
backend_directory: .
|
23
extensions/statistics/.github/workflows/frontend.yml
vendored
Normal file
23
extensions/statistics/.github/workflows/frontend.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Statistics JS
|
||||
|
||||
on: [workflow_dispatch, push, pull_request]
|
||||
|
||||
# The reusable workflow definitions will be moved to the `flarum/framework` repo soon.
|
||||
# This will break your current script.
|
||||
# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure.
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: flarum/.github/.github/workflows/REUSABLE_frontend.yml@main
|
||||
with:
|
||||
enable_bundlewatch: false
|
||||
enable_prettier: true
|
||||
enable_typescript: true
|
||||
|
||||
frontend_directory: ./js
|
||||
backend_directory: .
|
||||
js_package_manager: yarn
|
||||
main_git_branch: master
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
12
extensions/statistics/.gitignore
vendored
Normal file
12
extensions/statistics/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/vendor
|
||||
composer.lock
|
||||
composer.phar
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
tests/.phpunit.result.cache
|
||||
/tests/integration/tmp
|
||||
.vagrant
|
||||
.idea/*
|
||||
.vscode
|
||||
js/coverage-ts
|
14
extensions/statistics/.styleci.yml
Normal file
14
extensions/statistics/.styleci.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
preset: recommended
|
||||
|
||||
enabled:
|
||||
- logical_not_operators_with_successor_space
|
||||
|
||||
disabled:
|
||||
- align_double_arrow
|
||||
- blank_line_after_opening_tag
|
||||
- multiline_array_trailing_comma
|
||||
- new_with_braces
|
||||
- phpdoc_align
|
||||
- phpdoc_order
|
||||
- phpdoc_separation
|
||||
- phpdoc_types
|
46
extensions/statistics/CHANGELOG.md
Normal file
46
extensions/statistics/CHANGELOG.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## [1.2.0](https://github.com/flarum/statistics/compare/v1.1.0...v1.2.0)
|
||||
|
||||
No changes.
|
||||
|
||||
## [1.1.0](https://github.com/flarum/statistics/compare/v1.0.0...v1.1.0)
|
||||
|
||||
No changes.
|
||||
|
||||
## [1.0.0](https://github.com/flarum/statistics/compare/v0.1.0-beta.16...v1.0.0)
|
||||
|
||||
### Changed
|
||||
- Compatibility with Flarum v1.0.0.
|
||||
|
||||
## [0.1.0-beta.16](https://github.com/flarum/statistics/compare/v0.1.0-beta.15...v0.1.0-beta.16)
|
||||
|
||||
### Changed
|
||||
- Moved locale files from translation pack to extension (https://github.com/flarum/statistics/pull/13)
|
||||
|
||||
### Fixed
|
||||
- Summed value being interpreted as string (https://github.com/flarum/statistics/pull/14)
|
||||
|
||||
## [0.1.0-beta.15](https://github.com/flarum/statistics/compare/v0.1.0-beta.14...v0.1.0-beta.15)
|
||||
|
||||
### Changed
|
||||
- Updated composer.json and admin javascript for new admin area.
|
||||
- Updated to use newest extenders.
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/flarum/statistics/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
||||
|
||||
### Changed
|
||||
- Switched from momentjs to dayjs
|
||||
- Updated mithril to version 2
|
||||
- Load language strings correctly on en-/disable
|
||||
- Updated JS dependencies
|
||||
|
||||
## [0.1.0-beta.13](https://github.com/flarum/statistics/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||
|
||||
### Changed
|
||||
- Updated JS dependencies
|
||||
|
||||
## [0.1.0-beta.9](https://github.com/flarum/statistics/compare/v0.1.0-beta.8...v0.1.0-beta.9)
|
||||
|
||||
### Changed
|
||||
- Update compiled JavaScript sources
|
22
extensions/statistics/LICENSE
Normal file
22
extensions/statistics/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
81
extensions/statistics/composer.json
Normal file
81
extensions/statistics/composer.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "flarum/statistics",
|
||||
"description": "Add a basic statistics widget on the Dashboard.",
|
||||
"type": "flarum-extension",
|
||||
"keywords": [
|
||||
"administration"
|
||||
],
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/flarum/core/issues",
|
||||
"source": "https://github.com/flarum/statistics",
|
||||
"forum": "https://discuss.flarum.org"
|
||||
},
|
||||
"homepage": "https://flarum.org",
|
||||
"funding": [
|
||||
{
|
||||
"type": "website",
|
||||
"url": "https://flarum.org/donate/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Flarum\\Statistics\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
},
|
||||
"flarum-extension": {
|
||||
"title": "Statistics",
|
||||
"category": "feature",
|
||||
"icon": {
|
||||
"name": "fas fa-chart-bar",
|
||||
"backgroundColor": "#6932d1",
|
||||
"color": "#fff"
|
||||
}
|
||||
},
|
||||
"flarum-cli": {
|
||||
"excludeScaffolding": [
|
||||
"js/src/admin/index.ts"
|
||||
],
|
||||
"modules": {
|
||||
"admin": true,
|
||||
"forum": false,
|
||||
"js": true,
|
||||
"jsCommon": false,
|
||||
"css": true,
|
||||
"gitConf": true,
|
||||
"githubActions": true,
|
||||
"prettier": true,
|
||||
"typescript": true,
|
||||
"bundlewatch": false,
|
||||
"backendTesting": true,
|
||||
"editorConfig": true,
|
||||
"styleci": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:integration"
|
||||
],
|
||||
"test:unit": "phpunit -c tests/phpunit.unit.xml",
|
||||
"test:integration": "phpunit -c tests/phpunit.integration.xml",
|
||||
"test:setup": "@php tests/integration/setup.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"test": "Runs all tests.",
|
||||
"test:unit": "Runs all unit tests.",
|
||||
"test:integration": "Runs all integration tests.",
|
||||
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
|
||||
},
|
||||
"require-dev": {
|
||||
"flarum/testing": "^1.0.0"
|
||||
}
|
||||
}
|
22
extensions/statistics/extend.php
Normal file
22
extensions/statistics/extend.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
use Flarum\Extend;
|
||||
use Flarum\Statistics\AddStatisticsData;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('admin'))
|
||||
->js(__DIR__.'/js/dist/admin.js')
|
||||
->css(__DIR__.'/less/admin.less')
|
||||
->content(AddStatisticsData::class),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
];
|
9
extensions/statistics/js/.gitignore
vendored
Normal file
9
extensions/statistics/js/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules
|
1
extensions/statistics/js/admin.ts
Normal file
1
extensions/statistics/js/admin.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src/admin';
|
42
extensions/statistics/js/dist-typings/components/StatisticsWidget.d.ts
vendored
Normal file
42
extensions/statistics/js/dist-typings/components/StatisticsWidget.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
export default class StatisticsWidget {
|
||||
oninit(vnode: any): void;
|
||||
entities: string[] | undefined;
|
||||
periods: {
|
||||
today: {
|
||||
start: Date;
|
||||
end: any;
|
||||
step: number;
|
||||
};
|
||||
last_7_days: {
|
||||
start: number;
|
||||
end: Date;
|
||||
step: number;
|
||||
};
|
||||
last_28_days: {
|
||||
start: number;
|
||||
end: Date;
|
||||
step: number;
|
||||
};
|
||||
last_12_months: {
|
||||
start: number;
|
||||
end: Date;
|
||||
step: number;
|
||||
};
|
||||
} | undefined;
|
||||
selectedEntity: any;
|
||||
selectedPeriod: any;
|
||||
className(): string;
|
||||
content(): JSX.Element;
|
||||
drawChart(vnode: any): void;
|
||||
chart: any;
|
||||
entity: any;
|
||||
period: any;
|
||||
changeEntity(entity: any): void;
|
||||
changePeriod(period: any): void;
|
||||
getTotalCount(entity: any): any;
|
||||
getPeriodCount(entity: any, period: any): number;
|
||||
getLastPeriod(thisPeriod: any): {
|
||||
start: number;
|
||||
end: any;
|
||||
};
|
||||
}
|
1
extensions/statistics/js/dist-typings/index.d.ts
generated
vendored
Normal file
1
extensions/statistics/js/dist-typings/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
2
extensions/statistics/js/dist/admin.js
generated
vendored
Normal file
2
extensions/statistics/js/dist/admin.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
extensions/statistics/js/dist/admin.js.map
generated
vendored
Normal file
1
extensions/statistics/js/dist/admin.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
31
extensions/statistics/js/package.json
Normal file
31
extensions/statistics/js/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@flarum/statistics",
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"dependencies": {
|
||||
"frappe-charts": "^1.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mithril": "^2.0.8",
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0",
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript-coverage-report": "^0.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"format": "prettier --write src",
|
||||
"format-check": "prettier --check src",
|
||||
"analyze": "cross-env ANALYZER=true yarn run build",
|
||||
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
|
||||
"build-typings": "yarn run clean-typings && tsc && [ -e src/@types ] && cp -r src/@types dist-typings/@types",
|
||||
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
|
||||
"check-typings-coverage": "typescript-coverage-report",
|
||||
"ci": "yarn install --immutable --immutable-cache"
|
||||
}
|
||||
}
|
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
}
|
14
extensions/statistics/js/src/admin/index.tsx
Normal file
14
extensions/statistics/js/src/admin/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import app from 'flarum/admin/app';
|
||||
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';
|
||||
|
||||
app.initializers.add('flarum-statistics', () => {
|
||||
extend(DashboardPage.prototype, 'availableWidgets', function (widgets: ItemList<Mithril.Children>) {
|
||||
widgets.add('statistics', <StatisticsWidget />, 20);
|
||||
});
|
||||
});
|
16
extensions/statistics/js/tsconfig.json
Normal file
16
extensions/statistics/js/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use Flarum's tsconfig as a starting point
|
||||
"extends": "flarum-tsconfig",
|
||||
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
|
||||
// and also tells your Typescript server to read core's global typings for
|
||||
// access to `dayjs` and `$` in the global namespace.
|
||||
"include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*", "@types/**/*"],
|
||||
"compilerOptions": {
|
||||
// This will output typings to `dist-typings`
|
||||
"declarationDir": "./dist-typings",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"]
|
||||
}
|
||||
}
|
||||
}
|
1
extensions/statistics/js/webpack.config.js
Normal file
1
extensions/statistics/js/webpack.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('flarum-webpack-config')();
|
2848
extensions/statistics/js/yarn.lock
Normal file
2848
extensions/statistics/js/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
197
extensions/statistics/less/admin.less
Normal file
197
extensions/statistics/less/admin.less
Normal file
@@ -0,0 +1,197 @@
|
||||
.StatisticsWidget-table {
|
||||
margin-top: -20px;
|
||||
}
|
||||
.StatisticsWidget-labels {
|
||||
float: left;
|
||||
min-width: 130px;
|
||||
padding-right: 10px;
|
||||
padding-top: 45px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: @muted-color;
|
||||
}
|
||||
.StatisticsWidget-label {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.StatisticsWidget-entity {
|
||||
float: left;
|
||||
min-width: 130px;
|
||||
padding: 15px 20px;
|
||||
color: @text-color;
|
||||
font-size: 20px;
|
||||
|
||||
&:hover {
|
||||
background: mix(@control-bg, @body-bg, 50%);
|
||||
text-decoration: none;
|
||||
}
|
||||
&.active {
|
||||
border-top: 4px solid @primary-color;
|
||||
padding-top: 15 - 4px;
|
||||
}
|
||||
}
|
||||
.StatisticsWidget-change {
|
||||
font-size: 11px;
|
||||
}
|
||||
.StatisticsWidget-change--up {
|
||||
color: #00a502;
|
||||
}
|
||||
.StatisticsWidget-change--down {
|
||||
color: #d0011b;
|
||||
}
|
||||
.StatisticsWidget-heading {
|
||||
height: 30px;
|
||||
padding-top: 5px;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
color: @muted-color;
|
||||
|
||||
.active & {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
.StatisticsWidget-total,
|
||||
.StatisticsWidget-period,
|
||||
.StatisticsWidget-label {
|
||||
height: 35px;
|
||||
}
|
||||
.StatisticsWidget-total {
|
||||
font-weight: bold;
|
||||
}
|
||||
.StatisticsWidget-chart {
|
||||
clear: left;
|
||||
margin: -20px -10px;
|
||||
}
|
||||
.StatisticsWidget-chart .chart-container {
|
||||
.dataset-0 {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.chart-legend {
|
||||
display: none;
|
||||
}
|
||||
// Hide the "last period" data from the tooltip
|
||||
.graph-svg-tip ul.data-point-list > li:first-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* Frappe Charts 1.0.0 by @frappe - https://frappe.io/charts
|
||||
* License - MIT https://github.com/frappe/charts/blob/master/LICENSE
|
||||
*/
|
||||
.chart-container {
|
||||
position: relative; /* for absolutely positioned tooltip */
|
||||
|
||||
/* https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ */
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
|
||||
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
|
||||
.axis, .chart-label {
|
||||
fill: #555b51;
|
||||
|
||||
line {
|
||||
stroke: #dadada;
|
||||
}
|
||||
}
|
||||
.dataset-units {
|
||||
circle {
|
||||
stroke: #fff;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
path {
|
||||
fill: none;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
.dataset-path {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
.path-group {
|
||||
path {
|
||||
fill: none;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
line.dashed {
|
||||
stroke-dasharray: 5, 3;
|
||||
}
|
||||
.axis-line {
|
||||
.specific-value {
|
||||
text-anchor: start;
|
||||
}
|
||||
.y-line {
|
||||
text-anchor: end;
|
||||
}
|
||||
.x-line {
|
||||
text-anchor: middle;
|
||||
}
|
||||
}
|
||||
.legend-dataset-text {
|
||||
fill: #6c7680;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.graph-svg-tip {
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
color: #959da5;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 3px;
|
||||
ul {
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
}
|
||||
ol {
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
}
|
||||
ul.data-point-list {
|
||||
li {
|
||||
min-width: 90px;
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
strong {
|
||||
color: #dfe2e5;
|
||||
font-weight: 600;
|
||||
}
|
||||
.svg-pointer {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
margin: 0 0 0 -5px;
|
||||
content: ' ';
|
||||
border: 5px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
&.comparison {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
.title {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
}
|
20
extensions/statistics/locale/en.yml
Normal file
20
extensions/statistics/locale/en.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
flarum-statistics:
|
||||
|
||||
##
|
||||
# UNIQUE KEYS - The following keys are used in only one location each.
|
||||
##
|
||||
|
||||
# Translations in this namespace are used by the admin interface.
|
||||
admin:
|
||||
|
||||
# These translations are used in the Statistics dashboard widget.
|
||||
statistics:
|
||||
active_users_text: "{count} active"
|
||||
discussions_heading: => core.ref.discussions
|
||||
last_12_months_label: Last 12 months
|
||||
last_28_days_label: Last 28 days
|
||||
last_7_days_label: Last 7 days
|
||||
posts_heading: => core.ref.posts
|
||||
today_label: Today
|
||||
total_label: Total
|
||||
users_heading: => core.ref.users
|
110
extensions/statistics/src/AddStatisticsData.php
Normal file
110
extensions/statistics/src/AddStatisticsData.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Flarum\Statistics;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Frontend\Document;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class AddStatisticsData
|
||||
{
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function __invoke(Document $view)
|
||||
{
|
||||
$view->payload['statistics'] = array_merge(
|
||||
$this->getStatistics(),
|
||||
['timezoneOffset' => $this->getUserTimezone()->getOffset(new DateTime)]
|
||||
);
|
||||
}
|
||||
|
||||
private function getStatistics()
|
||||
{
|
||||
$entities = [
|
||||
'users' => [User::query(), 'joined_at'],
|
||||
'discussions' => [Discussion::query(), 'created_at'],
|
||||
'posts' => [Post::where('type', 'comment'), 'created_at']
|
||||
];
|
||||
|
||||
return array_map(function ($entity) {
|
||||
return [
|
||||
'total' => $entity[0]->count(),
|
||||
'timed' => $this->getTimedCounts($entity[0], $entity[1])
|
||||
];
|
||||
}, $entities);
|
||||
}
|
||||
|
||||
private function getTimedCounts(Builder $query, $column)
|
||||
{
|
||||
// Calculate the offset between the server timezone (which is used for
|
||||
// dates stored in the database) and the user's timezone (set via the
|
||||
// settings table). We will use this to make sure we aggregate the
|
||||
// daily/hourly statistics according to the user's timezone.
|
||||
$offset = $this->getTimezoneOffset();
|
||||
|
||||
$results = $query
|
||||
->selectRaw(
|
||||
'DATE_FORMAT(
|
||||
@date := DATE_ADD('.$column.', INTERVAL ? SECOND), -- convert to user timezone
|
||||
IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 48 hours, group by hour
|
||||
) as time_group',
|
||||
[$offset, new DateTime('-48 hours')]
|
||||
)
|
||||
->selectRaw('COUNT(id) as count')
|
||||
->where($column, '>', new DateTime('-24 months'))
|
||||
->groupBy('time_group')
|
||||
->pluck('count', 'time_group');
|
||||
|
||||
// Now that we have the aggregated statistics, convert each time group
|
||||
// into a UNIX timestamp.
|
||||
$userTimezone = $this->getUserTimezone();
|
||||
|
||||
$timed = [];
|
||||
|
||||
$results->each(function ($count, $time) use (&$timed, $userTimezone) {
|
||||
$time = new DateTime($time, $userTimezone);
|
||||
$timed[$time->getTimestamp()] = (int) $count;
|
||||
});
|
||||
|
||||
return $timed;
|
||||
}
|
||||
|
||||
private function getTimezoneOffset()
|
||||
{
|
||||
$now = new DateTime;
|
||||
|
||||
$dataTimezone = new DateTimeZone(date_default_timezone_get());
|
||||
|
||||
return $this->getUserTimezone()->getOffset($now) - $dataTimezone->getOffset($now);
|
||||
}
|
||||
|
||||
private function getUserTimezone()
|
||||
{
|
||||
return new DateTimeZone($this->settings->get('flarum-statistics.timezone', date_default_timezone_get()));
|
||||
}
|
||||
}
|
0
extensions/statistics/tests/fixtures/.gitkeep
vendored
Normal file
0
extensions/statistics/tests/fixtures/.gitkeep
vendored
Normal file
16
extensions/statistics/tests/integration/setup.php
Normal file
16
extensions/statistics/tests/integration/setup.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Testing\integration\Setup\SetupScript;
|
||||
|
||||
require __DIR__.'/../../vendor/autoload.php';
|
||||
|
||||
$setup = new SetupScript();
|
||||
|
||||
$setup->run();
|
25
extensions/statistics/tests/phpunit.integration.xml
Normal file
25
extensions/statistics/tests/phpunit.integration.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="true"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">../src/</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="Flarum Integration Tests">
|
||||
<directory suffix="Test.php">./integration</directory>
|
||||
<exclude>./integration/tmp</exclude>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
27
extensions/statistics/tests/phpunit.unit.xml
Normal file
27
extensions/statistics/tests/phpunit.unit.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">../src/</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="Flarum Unit Tests">
|
||||
<directory suffix="Test.php">./unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<listeners>
|
||||
<listener class="\Mockery\Adapter\Phpunit\TestListener" />
|
||||
</listeners>
|
||||
</phpunit>
|
0
extensions/statistics/tests/unit/.gitkeep
Normal file
0
extensions/statistics/tests/unit/.gitkeep
Normal file
Reference in New Issue
Block a user