mirror of
https://github.com/flarum/core.git
synced 2025-08-03 15:07:53 +02:00
Initial commit
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]
|
||||||
|
indent_size = 4
|
5
extensions/statistics/.gitattributes
vendored
Normal file
5
extensions/statistics/.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.gitattributes export-ignore
|
||||||
|
.gitignore export-ignore
|
||||||
|
.travis.yml export-ignore
|
||||||
|
|
||||||
|
js/*/dist/*.js -diff
|
6
extensions/statistics/.gitignore
vendored
Normal file
6
extensions/statistics/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/vendor
|
||||||
|
composer.phar
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
bower_components
|
||||||
|
node_modules
|
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
|
21
extensions/statistics/LICENSE
Normal file
21
extensions/statistics/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Toby Zerner
|
||||||
|
|
||||||
|
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.
|
18
extensions/statistics/bootstrap.php
Normal file
18
extensions/statistics/bootstrap.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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\Statistics\Listener;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
|
||||||
|
return function (Dispatcher $events) {
|
||||||
|
$events->subscribe(Listener\AddClientAssets::class);
|
||||||
|
$events->subscribe(Listener\AddStatisticsData::class);
|
||||||
|
};
|
33
extensions/statistics/composer.json
Normal file
33
extensions/statistics/composer.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "flarum/flarum-ext-statistics",
|
||||||
|
"description": "Add a basic statistics widget on the Dashboard.",
|
||||||
|
"type": "flarum-extension",
|
||||||
|
"keywords": ["administration"],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Toby Zerner",
|
||||||
|
"email": "toby.zerner@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/flarum/core/issues",
|
||||||
|
"source": "https://github.com/flarum/flarum-ext-statistics"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"flarum/core": "^0.1.0-beta.7"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Flarum\\Statistics\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "0.1.x-dev"
|
||||||
|
},
|
||||||
|
"flarum-extension": {
|
||||||
|
"title": "Statistics"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
extensions/statistics/js/admin/Gulpfile.js
Normal file
10
extensions/statistics/js/admin/Gulpfile.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
var gulp = require('flarum-gulp');
|
||||||
|
|
||||||
|
gulp({
|
||||||
|
modules: {
|
||||||
|
'flarum/statistics': 'src/**/*.js'
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
'node_modules/frappe-charts/dist/frappe-charts.min.iife.js'
|
||||||
|
]
|
||||||
|
});
|
239
extensions/statistics/js/admin/dist/extension.js
vendored
Normal file
239
extensions/statistics/js/admin/dist/extension.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
extensions/statistics/js/admin/package.json
Normal file
10
extensions/statistics/js/admin/package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"gulp": "^3.9.1",
|
||||||
|
"flarum-gulp": "^0.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"frappe-charts": "0.0.8"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* 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 DashboardWidget from 'flarum/components/DashboardWidget';
|
||||||
|
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
import listItems from 'flarum/helpers/listItems';
|
||||||
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
|
||||||
|
export default class StatisticsWidget extends DashboardWidget {
|
||||||
|
init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
this.entities = ['users', 'discussions', 'posts'];
|
||||||
|
this.periods = {
|
||||||
|
last_7_days: {start: now - 86400000 * 7, end: now, step: 86400000},
|
||||||
|
last_28_days: {start: now - 86400000 * 28, end: now, step: 86400000},
|
||||||
|
last_12_months: {start: now - 86400000 * 365, end: now, step: 86400000 * 7}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectedEntity = 'users';
|
||||||
|
this.selectedPeriod = 'last_12_months';
|
||||||
|
}
|
||||||
|
|
||||||
|
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="caret-down">
|
||||||
|
{Object.keys(this.periods).map(period => (
|
||||||
|
<Button active={period === this.selectedPeriod} onclick={this.changePeriod.bind(this, period)}>
|
||||||
|
{app.translator.trans('flarum-statistics.admin.statistics.'+period+'_label')}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</SelectDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.entities.map(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">{this.getTotalCount(entity)}</div>
|
||||||
|
<div className="StatisticsWidget-period">
|
||||||
|
{thisPeriodCount}{' '}
|
||||||
|
{periodChange ? (
|
||||||
|
<span className={'StatisticsWidget-change StatisticsWidget-change--'+(periodChange > 0 ? 'up' : 'down')}>
|
||||||
|
{icon('arrow-'+(periodChange > 0 ? 'up' : 'down'))}
|
||||||
|
{Math.abs(periodChange.toFixed(1))}%
|
||||||
|
</span>
|
||||||
|
) : ''}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="StatisticsWidget-chart" config={this.drawChart.bind(this)}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawChart(elm, isInitialized, context) {
|
||||||
|
const entity = this.selectedEntity;
|
||||||
|
const period = this.periods[this.selectedPeriod];
|
||||||
|
const daily = app.data.statistics[this.selectedEntity].daily;
|
||||||
|
const labels = [];
|
||||||
|
const thisPeriod = [];
|
||||||
|
const lastPeriod = [];
|
||||||
|
|
||||||
|
for (let i = period.start; i < period.end; i += period.step) {
|
||||||
|
const date = new Date(i);
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
labels.push(moment(date).format('D MMM'));
|
||||||
|
thisPeriod.push(this.getPeriodCount(entity, {start: i, end: i + period.step}));
|
||||||
|
const periodLength = period.end - period.start;
|
||||||
|
lastPeriod.push(this.getPeriodCount(entity, {start: i - periodLength, end: i - periodLength + period.step}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasets = [
|
||||||
|
{values: lastPeriod, title: 'Last period ➡'},
|
||||||
|
{values: thisPeriod, title: 'This period'}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!context.chart) {
|
||||||
|
context.chart = new Chart({
|
||||||
|
parent: elm,
|
||||||
|
data: {labels, datasets},
|
||||||
|
type: 'line',
|
||||||
|
height: 200,
|
||||||
|
x_axis_mode: 'tick', // for short label ticks
|
||||||
|
y_axis_mode: 'span', // for long horizontal lines, or 'tick'
|
||||||
|
is_series: 1,
|
||||||
|
show_dots: 0,
|
||||||
|
colors: ['rgba(0,0,0,0.2)', app.forum.attribute('themePrimaryColor')],
|
||||||
|
format_tooltip_x: d => d,
|
||||||
|
format_tooltip_y: d => d
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
context.chart.update_values(
|
||||||
|
datasets,
|
||||||
|
labels
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeEntity(entity) {
|
||||||
|
this.selectedEntity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
changePeriod(period) {
|
||||||
|
this.selectedPeriod = period;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalCount(entity) {
|
||||||
|
return app.data.statistics[entity].total;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeriodCount(entity, period) {
|
||||||
|
const daily = app.data.statistics[entity].daily;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const day in daily) {
|
||||||
|
const date = new Date(day);
|
||||||
|
|
||||||
|
if (date > period.start && date < period.end) {
|
||||||
|
count += daily[day];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastPeriod(thisPeriod) {
|
||||||
|
return {
|
||||||
|
start: thisPeriod.start - (thisPeriod.end - thisPeriod.start),
|
||||||
|
end: thisPeriod.start
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
12
extensions/statistics/js/admin/src/main.js
Normal file
12
extensions/statistics/js/admin/src/main.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import app from 'flarum/app';
|
||||||
|
import { extend } from 'flarum/extend';
|
||||||
|
|
||||||
|
import DashboardPage from 'flarum/components/DashboardPage';
|
||||||
|
|
||||||
|
import StatisticsWidget from 'flarum/statistics/components/StatisticsWidget';
|
||||||
|
|
||||||
|
app.initializers.add('flarum-statistics', () => {
|
||||||
|
extend(DashboardPage.prototype, 'availableWidgets', widgets => {
|
||||||
|
widgets.push(<StatisticsWidget/>);
|
||||||
|
});
|
||||||
|
});
|
66
extensions/statistics/less/admin/extension.less
Normal file
66
extensions/statistics/less/admin/extension.less
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
.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: 100px;
|
||||||
|
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;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: -20px -10px;
|
||||||
|
}
|
40
extensions/statistics/src/Listener/AddClientAssets.php
Normal file
40
extensions/statistics/src/Listener/AddClientAssets.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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\Listener;
|
||||||
|
|
||||||
|
use Flarum\Event\ConfigureWebApp;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
|
||||||
|
class AddClientAssets
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Dispatcher $events
|
||||||
|
*/
|
||||||
|
public function subscribe(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->listen(ConfigureWebApp::class, [$this, 'addAssets']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConfigureClientView $event
|
||||||
|
*/
|
||||||
|
public function addAssets(ConfigureWebApp $event)
|
||||||
|
{
|
||||||
|
if ($event->isAdmin()) {
|
||||||
|
$event->addAssets([
|
||||||
|
__DIR__.'/../../js/admin/dist/extension.js',
|
||||||
|
__DIR__.'/../../less/admin/extension.less'
|
||||||
|
]);
|
||||||
|
$event->addBootstrapper('flarum/statistics/main');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
extensions/statistics/src/Listener/AddStatisticsData.php
Normal file
59
extensions/statistics/src/Listener/AddStatisticsData.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?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\Listener;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Flarum\Core\Discussion;
|
||||||
|
use Flarum\Core\Post;
|
||||||
|
use Flarum\Core\User;
|
||||||
|
use Flarum\Event\ConfigureWebApp;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
|
||||||
|
class AddStatisticsData
|
||||||
|
{
|
||||||
|
public function subscribe(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->listen(ConfigureWebApp::class, [$this, 'addStatisticsData']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addStatisticsData(ConfigureWebApp $event)
|
||||||
|
{
|
||||||
|
$event->view->setVariable('statistics', $this->getStatistics());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStatistics()
|
||||||
|
{
|
||||||
|
$entities = [
|
||||||
|
'users' => [User::query(), 'join_time'],
|
||||||
|
'discussions' => [Discussion::query(), 'start_time'],
|
||||||
|
'posts' => [Post::where('type', 'comment'), 'time']
|
||||||
|
];
|
||||||
|
|
||||||
|
return array_map(function ($entity) {
|
||||||
|
return [
|
||||||
|
'total' => $entity[0]->count(),
|
||||||
|
'daily' => $this->getDailyCounts($entity[0], $entity[1])
|
||||||
|
];
|
||||||
|
}, $entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDailyCounts(Builder $query, $column)
|
||||||
|
{
|
||||||
|
return $query
|
||||||
|
->selectRaw('DATE('.$column.') as date')
|
||||||
|
->selectRaw('COUNT(id) as count')
|
||||||
|
->where($column, '>', new DateTime('-24 months'))
|
||||||
|
->groupBy('date')
|
||||||
|
->lists('count', 'date');
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user