backports murph-skeleton
This commit is contained in:
parent
a5768ae4c0
commit
8b5c544c5b
|
@ -23,3 +23,4 @@ require('./modules/sortable.js')()
|
||||||
require('./modules/batch.js')()
|
require('./modules/batch.js')()
|
||||||
require('./modules/file-manager.js')()
|
require('./modules/file-manager.js')()
|
||||||
require('./modules/file-picker.js')()
|
require('./modules/file-picker.js')()
|
||||||
|
require('./modules/analytics.js')()
|
||||||
|
|
53
assets/js/admin/modules/analytics.js
Normal file
53
assets/js/admin/modules/analytics.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const $ = require('jquery')
|
||||||
|
const Chart = require('chart.js/auto').default
|
||||||
|
|
||||||
|
const drawChart = () => {
|
||||||
|
const ctx = document.getElementById('analytic-chart')
|
||||||
|
const options = {
|
||||||
|
type: 'bar',
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
labels: JSON.parse(ctx.getAttribute('data-labels')),
|
||||||
|
datasets: [{
|
||||||
|
label: ctx.getAttribute('data-label'),
|
||||||
|
data: JSON.parse(ctx.getAttribute('data-values')),
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chart = new Chart(ctx, options)
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const width = ctx.parentNode.parentNode.offsetWidth
|
||||||
|
const height = 250
|
||||||
|
|
||||||
|
chart.resize(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
resize()
|
||||||
|
|
||||||
|
window.addEventListener('resize', resize)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const body = $('body')
|
||||||
|
|
||||||
|
body.on('shown.bs.modal', '.modal', (e) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (document.getElementById('analytic-chart')) {
|
||||||
|
drawChart()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,8 +3,10 @@ const $ = require('jquery')
|
||||||
const openModal = function (url) {
|
const openModal = function (url) {
|
||||||
let container = $('#modal-container')
|
let container = $('#modal-container')
|
||||||
const body = $('body')
|
const body = $('body')
|
||||||
|
let doTrigger = true
|
||||||
|
|
||||||
if (!container.length) {
|
if (!container.length) {
|
||||||
|
let doTrigger = false
|
||||||
container = $('<div id="modal-container" class="modal">')
|
container = $('<div id="modal-container" class="modal">')
|
||||||
|
|
||||||
body.append(container)
|
body.append(container)
|
||||||
|
@ -20,6 +22,10 @@ const openModal = function (url) {
|
||||||
|
|
||||||
container.load(url, function () {
|
container.load(url, function () {
|
||||||
loader.remove()
|
loader.remove()
|
||||||
|
|
||||||
|
if (doTrigger) {
|
||||||
|
container.trigger('shown.bs.modal')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
133
core/Analytic/RangeAnalytic.php
Normal file
133
core/Analytic/RangeAnalytic.php
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Analytic;
|
||||||
|
|
||||||
|
use App\Core\Repository\Analytic\RefererRepositoryQuery;
|
||||||
|
use App\Core\Repository\Analytic\ViewRepositoryQuery;
|
||||||
|
use App\Core\Entity\Site\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class RangeAnalytic.
|
||||||
|
*
|
||||||
|
* @author Simon Vieille <simon@deblan.fr>
|
||||||
|
*/
|
||||||
|
class RangeAnalytic
|
||||||
|
{
|
||||||
|
protected ViewRepositoryQuery $viewQuery;
|
||||||
|
protected RefererRepositoryQuery $refererQuery;
|
||||||
|
|
||||||
|
public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery)
|
||||||
|
{
|
||||||
|
$this->viewQuery = $viewQuery;
|
||||||
|
$this->refererQuery = $refererQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViews(\DateTime $from, \DateTime $to, Node $node): array
|
||||||
|
{
|
||||||
|
$entities = $this->viewQuery->create()
|
||||||
|
->andWhere('.date >= :from')
|
||||||
|
->andWhere('.date <= :to')
|
||||||
|
->andWhere('.node = :node')
|
||||||
|
->orderBy('.date')
|
||||||
|
->setParameters([
|
||||||
|
':from' => $from,
|
||||||
|
':to' => $to,
|
||||||
|
':node' => $node->getId(),
|
||||||
|
])
|
||||||
|
->find()
|
||||||
|
;
|
||||||
|
|
||||||
|
$diff = $from->diff($to);
|
||||||
|
|
||||||
|
if ($diff->days >= 365) {
|
||||||
|
$format = 'Y-m';
|
||||||
|
} else {
|
||||||
|
$format = 'Y-m-d';
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$index = $entity->getDate()->format($format);
|
||||||
|
|
||||||
|
if (!isset($datas[$index])) {
|
||||||
|
$datas[$index] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index] += $entity->getViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPathViews(\DateTime $from, \DateTime $to, Node $node): array
|
||||||
|
{
|
||||||
|
$entities = $this->viewQuery->create()
|
||||||
|
->andWhere('.date >= :from')
|
||||||
|
->andWhere('.date <= :to')
|
||||||
|
->andWhere('.node = :node')
|
||||||
|
->orderBy('.date')
|
||||||
|
->setParameters([
|
||||||
|
':from' => $from,
|
||||||
|
':to' => $to,
|
||||||
|
':node' => $node->getId(),
|
||||||
|
])
|
||||||
|
->find()
|
||||||
|
;
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$index = $entity->getPath();
|
||||||
|
|
||||||
|
if (!isset($datas[$index])) {
|
||||||
|
$datas[$index] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index] += $entity->getViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferers(\DateTime $from, \DateTime $to, Node $node): array
|
||||||
|
{
|
||||||
|
$entities = $this->refererQuery->create()
|
||||||
|
->andWhere('.date >= :from')
|
||||||
|
->andWhere('.date <= :to')
|
||||||
|
->andWhere('.node = :node')
|
||||||
|
->orderBy('.date')
|
||||||
|
->setParameters([
|
||||||
|
':from' => $from,
|
||||||
|
':to' => $to,
|
||||||
|
':node' => $node->getId(),
|
||||||
|
])
|
||||||
|
->find()
|
||||||
|
;
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$index = parse_url($entity->getUri(), PHP_URL_HOST);
|
||||||
|
|
||||||
|
if (!isset($datas[$index])) {
|
||||||
|
$datas[$index] = [
|
||||||
|
'views' => 0,
|
||||||
|
'uris' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index]['views'] += $entity->getViews();
|
||||||
|
|
||||||
|
$path = parse_url($entity->getUri(), PHP_URL_PATH);
|
||||||
|
|
||||||
|
if (!isset($datas[$index]['uris'][$path])) {
|
||||||
|
$datas[$index]['uris'][$path] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[$index]['uris'][$path] += $entity->getViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
}
|
37
core/Controller/Analytic/AnalyticController.php
Normal file
37
core/Controller/Analytic/AnalyticController.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core\Controller\Analytic;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use App\Core\Entity\Site\Node;
|
||||||
|
use App\Core\Analytic\RangeAnalytic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/admin/analytic")
|
||||||
|
*/
|
||||||
|
class AnalyticController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/stats/{node}/{range}", name="admin_analytic_stats")
|
||||||
|
*/
|
||||||
|
public function stats(Node $node, RangeAnalytic $rangeAnalytic, string $range = '7days'): Response
|
||||||
|
{
|
||||||
|
if (!in_array($range, ['7days', '30days', '90days', '1year'])) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$views = $rangeAnalytic->getViews(new \DateTime('now - '.$range), new \DateTime(), $node);
|
||||||
|
$pathViews = $rangeAnalytic->getPathViews(new \DateTime('now - '.$range), new \DateTime(), $node);
|
||||||
|
$referers = $rangeAnalytic->getReferers(new \DateTime('now - '.$range), new \DateTime(), $node);
|
||||||
|
|
||||||
|
return $this->render('@Core/analytic/stats.html.twig', [
|
||||||
|
'range' => $range,
|
||||||
|
'views' => $views,
|
||||||
|
'pathViews' => $pathViews,
|
||||||
|
'referers' => $referers,
|
||||||
|
'node' => $node,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,11 +21,12 @@ use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/admin/site/node")
|
* @Route("/admin/site/node")
|
||||||
*/
|
*/
|
||||||
class NodeAdminController extends AdminController
|
class NodeAdminController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @Route("/new/{node}", name="admin_site_node_new")
|
* @Route("/new/{node}", name="admin_site_node_new")
|
||||||
|
@ -268,11 +269,6 @@ class NodeAdminController extends AdminController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSection(): string
|
|
||||||
{
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function handlePageAssociation(
|
protected function handlePageAssociation(
|
||||||
string $pageAction,
|
string $pageAction,
|
||||||
?Page $pageEntity,
|
?Page $pageEntity,
|
||||||
|
|
|
@ -18,28 +18,28 @@ class Referer implements EntityInterface
|
||||||
* @ORM\GeneratedValue
|
* @ORM\GeneratedValue
|
||||||
* @ORM\Column(type="integer")
|
* @ORM\Column(type="integer")
|
||||||
*/
|
*/
|
||||||
private $id;
|
protected $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews")
|
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews")
|
||||||
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
|
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
|
||||||
*/
|
*/
|
||||||
private $node;
|
protected $node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="string", length=255)
|
* @ORM\Column(type="string", length=255)
|
||||||
*/
|
*/
|
||||||
private $uri;
|
protected $uri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="integer", options={"default"=0})
|
* @ORM\Column(type="integer", options={"default"=0})
|
||||||
*/
|
*/
|
||||||
private $views = 0;
|
protected $views = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="date")
|
* @ORM\Column(type="date")
|
||||||
*/
|
*/
|
||||||
private $date;
|
protected $date;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,28 +18,28 @@ class View implements EntityInterface
|
||||||
* @ORM\GeneratedValue
|
* @ORM\GeneratedValue
|
||||||
* @ORM\Column(type="integer")
|
* @ORM\Column(type="integer")
|
||||||
*/
|
*/
|
||||||
private $id;
|
protected $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews")
|
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews")
|
||||||
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
|
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
|
||||||
*/
|
*/
|
||||||
private $node;
|
protected $node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="string", length=255)
|
* @ORM\Column(type="string", length=255)
|
||||||
*/
|
*/
|
||||||
private $path;
|
protected $path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="integer", options={"default"=0})
|
* @ORM\Column(type="integer", options={"default"=0})
|
||||||
*/
|
*/
|
||||||
private $views = 0;
|
protected $views = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="date")
|
* @ORM\Column(type="date")
|
||||||
*/
|
*/
|
||||||
private $date;
|
protected $date;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
|
|
|
@ -145,7 +145,7 @@ class Node implements EntityInterface
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="boolean", options={"default"=0})
|
* @ORM\Column(type="boolean", options={"default"=0})
|
||||||
*/
|
*/
|
||||||
protected $enableAnalytic = false;
|
protected $enableAnalytics = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\OneToMany(targetEntity=View::class, mappedBy="node")
|
* @ORM\OneToMany(targetEntity=View::class, mappedBy="node")
|
||||||
|
@ -569,14 +569,14 @@ class Node implements EntityInterface
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getEnableAnalytic(): ?bool
|
public function getEnableAnalytics(): ?bool
|
||||||
{
|
{
|
||||||
return $this->enableAnalytic;
|
return $this->enableAnalytics;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setEnableAnalytic(bool $enableAnalytic): self
|
public function setEnableAnalytics(bool $enableAnalytics): self
|
||||||
{
|
{
|
||||||
$this->enableAnalytic = $enableAnalytic;
|
$this->enableAnalytics = $enableAnalytics;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ class AnalyticListener
|
||||||
|
|
||||||
$node = $this->nodeRepository->findOneBy([
|
$node = $this->nodeRepository->findOneBy([
|
||||||
'id' => $request->attributes->get('_node'),
|
'id' => $request->attributes->get('_node'),
|
||||||
'enableViewCounter' => true,
|
'enableAnalytics' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$node) {
|
if (!$node) {
|
||||||
|
|
|
@ -61,10 +61,10 @@ class NodeType extends AbstractType
|
||||||
);
|
);
|
||||||
|
|
||||||
$builder->add(
|
$builder->add(
|
||||||
'enableAnalytic',
|
'enableAnalytics',
|
||||||
CheckboxType::class,
|
CheckboxType::class,
|
||||||
[
|
[
|
||||||
'label' => 'Enable analytic',
|
'label' => 'Enable analytics',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'attr' => [
|
'attr' => [
|
||||||
],
|
],
|
||||||
|
|
|
@ -39,7 +39,8 @@
|
||||||
"Never": "Jamais"
|
"Never": "Jamais"
|
||||||
"URL": "URL"
|
"URL": "URL"
|
||||||
"Disable URL": "Désactiver l'URL"
|
"Disable URL": "Désactiver l'URL"
|
||||||
"Enable analytic": "Activer l'analyse"
|
"Enable analytics": "Activer les analyses"
|
||||||
|
"Analytics": "Analyses"
|
||||||
"Controller": "Contrôleur"
|
"Controller": "Contrôleur"
|
||||||
"Leave blank for automatic generation": "Laisser vide pour une génération automatique"
|
"Leave blank for automatic generation": "Laisser vide pour une génération automatique"
|
||||||
"Leave blank to use the default one. Example: App\\Controller\\FooController::barAction": "Laisser vide pour utiliser celui par défaut. Exemple : App\\Controller\\FooController::barAction"
|
"Leave blank to use the default one. Example: App\\Controller\\FooController::barAction": "Laisser vide pour utiliser celui par défaut. Exemple : App\\Controller\\FooController::barAction"
|
||||||
|
|
111
core/Resources/views/analytic/stats.html.twig
Normal file
111
core/Resources/views/analytic/stats.html.twig
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<div class="modal-dialog modal-dialog-large">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{ 'Analytics'|trans }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ul class="nav nav-pills" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link {% if range == '7days' %}active{% endif %}" data-modal="{{ path('admin_analytic_stats', {node: node.id, range: '7days'}) }}" href="#">
|
||||||
|
{{ 'Last 7 days'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link {% if range == '30days' %}active{% endif %}" data-modal="{{ path('admin_analytic_stats', {node: node.id, range: '30days'}) }}" href="#">
|
||||||
|
{{ 'Last 30 days'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link {% if range == '90days' %}active{% endif %}" data-modal="{{ path('admin_analytic_stats', {node: node.id, range: '90days'}) }}" href="#">
|
||||||
|
{{ 'Last 90 days'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link {% if range == '1year' %}active{% endif %}" data-modal="{{ path('admin_analytic_stats', {node: node.id, range: '1year'}) }}" href="#">
|
||||||
|
{{ 'Last year'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% set values = [] %}
|
||||||
|
{% for item in views %}{% set values = values|merge([item]) %}{% endfor %}
|
||||||
|
|
||||||
|
<div class="tab-content pt-4">
|
||||||
|
<div id="analytic-chart-container">
|
||||||
|
<canvas id="analytic-chart" data-label="{{ 'Views'|trans }}" data-labels="{{ views|keys|json_encode }}" data-values="{{ values|json_encode }}"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="pl-md-3">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'Path'|trans }}</th>
|
||||||
|
<th width="100" class="text-right">{{ 'Views'|trans }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for path, views in pathViews %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ path }}</td>
|
||||||
|
<td class="text-right">{{ views }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="pl-md-3">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'Referers'|trans }}</th>
|
||||||
|
<th width="100" class="text-right">{{ 'Views'|trans }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for referer, info in referers %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<div class="float-right">
|
||||||
|
{{ info.views }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ referer }}
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
{% for path, views in info.uris %}
|
||||||
|
<div class="pl-3">
|
||||||
|
<div class="float-right">
|
||||||
|
{{ info.views }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ path }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</details>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Close'|trans }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -72,6 +72,7 @@
|
||||||
{% set move = path('admin_site_node_move', {entity: node.id}) %}
|
{% set move = path('admin_site_node_move', {entity: node.id}) %}
|
||||||
{% set edit = path('admin_site_node_edit', {entity: node.id}) %}
|
{% set edit = path('admin_site_node_edit', {entity: node.id}) %}
|
||||||
{% set new = path('admin_site_node_new', {node: node.id}) %}
|
{% set new = path('admin_site_node_new', {node: node.id}) %}
|
||||||
|
{% set analytics = path('admin_analytic_stats', {node: node.id}) %}
|
||||||
|
|
||||||
<div class="list-group-item" id="node-{{ node.id }}">
|
<div class="list-group-item" id="node-{{ node.id }}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -147,6 +148,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button data-modal="{{ analytics }}" class="btn btn-sm btn-light border border-dark mr-1 mb-1">
|
||||||
|
<span data-modal="{{ analytics }}" class="fa fa-chart-bar"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button data-modal="{{ move }}" class="btn btn-sm btn-dark mr-1 mb-1">
|
<button data-modal="{{ move }}" class="btn btn-sm btn-dark mr-1 mb-1">
|
||||||
<span data-modal="{{ move }}" class="fa fa-arrows-alt"></span>
|
<span data-modal="{{ move }}" class="fa fa-arrows-alt"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^4.3.1",
|
"bootstrap": "^4.3.1",
|
||||||
|
"chart.js": "^3.7.1",
|
||||||
"choices.js": "^9.0.1",
|
"choices.js": "^9.0.1",
|
||||||
"flag-icon-css": "^3.5.0",
|
"flag-icon-css": "^3.5.0",
|
||||||
"flatpickr": "^4.6.9",
|
"flatpickr": "^4.6.9",
|
||||||
|
|
|
@ -1883,6 +1883,11 @@ chalk@^4.1.2:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
chart.js@^3.7.1:
|
||||||
|
version "3.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.1.tgz#0516f690c6a8680c6c707e31a4c1807a6f400ada"
|
||||||
|
integrity sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA==
|
||||||
|
|
||||||
choices.js@^9.0.1:
|
choices.js@^9.0.1:
|
||||||
version "9.0.1"
|
version "9.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
|
resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
|
||||||
|
|
Loading…
Reference in a new issue