Skip to content

Commit cbe7cff

Browse files
Improvement of the printer graph (#14779)
* Improvement of the printer graph feat: added a period and a format for the page counter graph of the printers feat: replacing raw text by gettext fix: fix twig lint fix: renaming twig template fix: Fixed a bug that when you click on the flatpickr interface to define a custom range, it closes the dropdown in the background fix: graphic bug with custom range * feat: adding unit tests for PrinterLog class * Apply suggestions from code review fix: Moving buttons component in showMetrics function fix: Prevent injection by filtering with htmlspecialchars fix: enhance code readability * Apply suggestions from code review * fix: Updating unit tests * Apply suggestions from code review Co-authored-by: Adrien Clairembault <[email protected]> * fix: Fixing PHP Lint & adding PHPDoc * fix: Updating unit tests * fix: remove dropdown auto-focused --------- Co-authored-by: Adrien Clairembault <[email protected]>
1 parent af610dc commit cbe7cff

File tree

3 files changed

+320
-22
lines changed

3 files changed

+320
-22
lines changed

src/PrinterLog.php

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* ---------------------------------------------------------------------
3434
*/
3535

36+
use Glpi\Application\View\TemplateRenderer;
3637
use Glpi\Dashboard\Widget;
3738

3839
/**
@@ -103,19 +104,33 @@ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $
103104
/**
104105
* Get metrics
105106
*
106-
* @param Printer $printer Printer instance
107-
* @param array $user_filters User filters
107+
* @param Printer $printer Printer instance
108+
* @param array $user_filters User filters
109+
* @param string $interval Date interval string (e.g. 'P1Y' for 1 year)
110+
* @param DateTime|null $start_date Start date for the metrics range
111+
* @param DateTime $end_date End date for the metrics range
112+
* @param string $format Format for the metrics data ('dynamic', 'daily', 'weekly', 'monthly', 'yearly')
108113
*
109-
* @return array
114+
* @return array An array of printer metrics data
110115
*/
111-
public function getMetrics(Printer $printer, $user_filters = []): array
112-
{
116+
public function getMetrics(
117+
Printer $printer,
118+
$user_filters = [],
119+
$interval = 'P1Y',
120+
$start_date = null,
121+
$end_date = new DateTime(),
122+
$format = 'dynamic'
123+
): array {
113124
global $DB;
114125

115-
$bdate = new DateTime();
116-
$bdate->sub(new DateInterval('P1Y'));
126+
if (!$start_date) {
127+
$start_date = new DateTime();
128+
$start_date->sub(new DateInterval($interval));
129+
}
130+
117131
$filters = [
118-
'date' => ['>', $bdate->format('Y-m-d')]
132+
['date' => ['>=', $start_date->format('Y-m-d')]],
133+
['date' => ['<=', $end_date->format('Y-m-d')]]
119134
];
120135
$filters = array_merge($filters, $user_filters);
121136

@@ -128,15 +143,38 @@ public function getMetrics(Printer $printer, $user_filters = []): array
128143

129144
$series = iterator_to_array($iterator, false);
130145

131-
// Reduce the data to 25 points
132-
$count = count($series);
133-
$max_size = 25;
134-
if ($count > $max_size) {
135-
// Keep one row every X entry using modulo
136-
$modulo = round($count / $max_size);
146+
if ($format == 'dynamic') {
147+
// Reduce the data to 25 points
148+
$count = count($series);
149+
$max_size = 25;
150+
if ($count > $max_size) {
151+
// Keep one row every X entry using modulo
152+
$modulo = round($count / $max_size);
153+
$series = array_filter(
154+
$series,
155+
fn ($k) => (($count - ($k + 1)) % $modulo) == 0,
156+
ARRAY_FILTER_USE_KEY
157+
);
158+
}
159+
} else {
160+
$formats = [
161+
'daily' => 'Ymd', // Reduce the data to one point per day max
162+
'weekly' => 'YoW', // Reduce the data to one point per week max
163+
'monthly' => 'Ym', // Reduce the data to one point per month max
164+
'yearly' => 'Y', // Reduce the data to one point per year max
165+
];
166+
137167
$series = array_filter(
138168
$series,
139-
fn($k) => (($count - ($k + 1)) % $modulo) == 0,
169+
function ($k) use ($series, $format, $formats) {
170+
if (!isset($series[$k + 1])) {
171+
return true;
172+
}
173+
174+
$current_date = date($formats[$format], strtotime($series[$k]['date']));
175+
$next_date = date($formats[$format], strtotime($series[$k + 1]['date']));
176+
return $current_date !== $next_date;
177+
},
140178
ARRAY_FILTER_USE_KEY
141179
);
142180
}
@@ -151,7 +189,27 @@ public function getMetrics(Printer $printer, $user_filters = []): array
151189
*/
152190
public function showMetrics(Printer $printer)
153191
{
154-
$raw_metrics = $this->getMetrics($printer);
192+
$format = htmlspecialchars($_GET['date_format'] ?? 'dynamic');
193+
194+
if (isset($_GET['date_interval'])) {
195+
$raw_metrics = $this->getMetrics(
196+
$printer,
197+
interval: htmlspecialchars($_GET['date_interval']),
198+
format: $format,
199+
);
200+
} elseif (isset($_GET['date_start']) && isset($_GET['date_end'])) {
201+
$raw_metrics = $this->getMetrics(
202+
$printer,
203+
start_date: new DateTime(htmlspecialchars($_GET['date_start'])),
204+
end_date: new DateTime(htmlspecialchars($_GET['date_end'])),
205+
format: $format,
206+
);
207+
} else {
208+
$raw_metrics = $this->getMetrics(
209+
$printer,
210+
format: $format,
211+
);
212+
}
155213

156214
//build graph data
157215
$params = [
@@ -164,9 +222,10 @@ public function showMetrics(Printer $printer)
164222
$labels = [];
165223

166224
// Formatter to display the date (months names) in the correct language
167-
// Dates will be displayed as "d MMMM":
225+
// Dates will be displayed as "d MMM YYYY":
168226
// d = short day number (1, 12, ...)
169227
// MMM = short month name (jan, feb, ...)
228+
// YYYY = full year (2021, 2022, ...)
170229
// Note that PHP use ISO 8601 Date Output here which is different from
171230
// the "Constants for PHP Date Output" used in others functions
172231
// See https://framework.zend.com/manual/1.12/en/zend.date.constants.html#zend.date.constants.selfdefinedformats
@@ -176,7 +235,7 @@ public function showMetrics(Printer $printer)
176235
IntlDateFormatter::NONE,
177236
null,
178237
null,
179-
'd MMM'
238+
'd MMM YYYY'
180239
);
181240

182241
foreach ($raw_metrics as $metrics) {
@@ -202,12 +261,20 @@ public function showMetrics(Printer $printer)
202261
'icon' => $params['icon'],
203262
'color' => '#ffffff',
204263
'distributed' => false,
205-
'show_points' => false,
264+
'show_points' => true,
206265
'line_width' => 2,
207266
];
208267

268+
// display the printer graph buttons component
269+
TemplateRenderer::getInstance()->display('components/printer_graph_buttons.html.twig', [
270+
'start_date' => htmlspecialchars($_GET['date_start'] ?? ''),
271+
'end_date' => htmlspecialchars($_GET['date_end'] ?? ''),
272+
'interval' => htmlspecialchars($_GET['date_interval'] ?? 'P1Y'),
273+
'format' => $format,
274+
]);
275+
209276
//display graph
210-
echo "<div class='dashboard printer_barchart'>";
277+
echo "<div class='dashboard printer_barchart pt-2'>";
211278
echo Widget::multipleAreas($bar_conf);
212279
echo "</div>";
213280
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
{#
2+
# ---------------------------------------------------------------------
3+
#
4+
# GLPI - Gestionnaire Libre de Parc Informatique
5+
#
6+
# http://glpi-project.org
7+
#
8+
# @copyright 2015-2023 Teclib' and contributors.
9+
# @copyright 2003-2014 by the INDEPNET Development Team.
10+
# @licence https://www.gnu.org/licenses/gpl-3.0.html
11+
#
12+
# ---------------------------------------------------------------------
13+
#
14+
# LICENSE
15+
#
16+
# This file is part of GLPI.
17+
#
18+
# This program is free software: you can redistribute it and/or modify
19+
# it under the terms of the GNU General Public License as published by
20+
# the Free Software Foundation, either version 3 of the License, or
21+
# (at your option) any later version.
22+
#
23+
# This program is distributed in the hope that it will be useful,
24+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
25+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26+
# GNU General Public License for more details.
27+
#
28+
# You should have received a copy of the GNU General Public License
29+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
30+
#
31+
# ---------------------------------------------------------------------
32+
#}
33+
34+
{% import 'components/form/fields_macros.html.twig' as fields %}
35+
36+
{% set options = {
37+
'rand': random()
38+
} %}
39+
40+
{% if not timerange_presets %}
41+
{% set timerange_presets = {
42+
'P1D': __('Last day'),
43+
'P1W': __('Last 7 days'),
44+
'P1M': __('Last 30 days'),
45+
'P3M': __('Last quarter'),
46+
'P1Y': __('Last year'),
47+
'P1000Y': __('All time'),
48+
} %}
49+
{% endif %}
50+
51+
{% if not format_presets %}
52+
{% set format_presets = {
53+
'dynamic': __('Dynamic distribution'),
54+
'daily': __('Daily'),
55+
'weekly': __('Weekly'),
56+
'monthly': __('Monthly'),
57+
'yearly': __('Yearly'),
58+
} %}
59+
{% endif %}
60+
61+
<style>
62+
.rotate-45 {
63+
transform: rotate(45deg);
64+
transition: transform 0.2s ease-in-out;
65+
}
66+
.rotate-90 {
67+
transform: rotate(90deg);
68+
transition: transform 0.2s ease-in-out;
69+
}
70+
</style>
71+
72+
<div class="d-flex gap-2 w-full">
73+
<div id="select_range_dropdown" class="dropdown">
74+
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
75+
<i class="ti ti-calendar me-2"></i>
76+
{{ start_date and end_date ? __('Custom range') ~ ': ' ~ start_date|split('T')[0] ~ ' - ' ~ end_date|split('T')[0] : timerange_presets[interval] }}
77+
</button>
78+
<ul class="dropdown-menu">
79+
{% for key, value in timerange_presets %}
80+
<li>
81+
<span class="dropdown-item {{ interval == key and (not start_date or not end_date) ? 'active' }}" href="#" data-key="{{ key }}" onclick="update_date_preset_{{ options['rand'] }}(this)">{{ value }}</span>
82+
</li>
83+
{% endfor %}
84+
<li><hr class="dropdown-divider"></li>
85+
<li id="show_custom_range" class="d-flex align-items-center">
86+
<span class="dropdown-item {{ start_date and end_date ? 'active' }}" href="#">
87+
<i class="ti ti-plus {{ start_date and end_date ? 'rotate-45' : 'rotate-90' }}"></i>
88+
{{ __('Custom range') }}
89+
</span>
90+
</li>
91+
<li id="date_range_input" class="px-2 {{ not start_date or not end_date ? 'd-none' }}" style="width:11rem">
92+
{{ fields.dateField('range_date', '', '', options|merge({
93+
'no_label': true,
94+
'full_width': true,
95+
'mb': 'my-2',
96+
})) }}
97+
<button class="btn btn-primary w-full mb-1" type="button" onclick="update_custom_date_range_{{ options['rand'] }}()">{{ __('Apply') }}</button>
98+
</li>
99+
</ul>
100+
</div>
101+
<div id="select_format_dropdown" class="dropdown">
102+
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
103+
<i class="ti ti-chart-dots-3 me-2"></i>
104+
{{ format_presets[format] }}
105+
</button>
106+
<ul class="dropdown-menu">
107+
{% for key, value in format_presets %}
108+
<li>
109+
<span class="dropdown-item {{ format_presets[format] == value ? 'active' }}" href="#" data-key="{{ key }}" onclick="update_format_{{ options['rand'] }}(this)">{{ value }}</span>
110+
</li>
111+
{% endfor %}
112+
</ul>
113+
</div>
114+
</div>
115+
116+
<script>
117+
var date_range_data = {
118+
'format': '{{ format }}',
119+
'interval': '{{ interval }}',
120+
'start_date': '{{ start_date }}',
121+
'end_date': '{{ end_date }}',
122+
}
123+
124+
function reloadTab_{{ options['rand'] }}() {
125+
params = 'date_format=' + date_range_data['format'];
126+
127+
if(date_range_data['start_date'] && date_range_data['end_date']) {
128+
params += '&date_start=' + date_range_data['start_date'] + '&date_end=' + date_range_data['end_date'];
129+
} else {
130+
params += '&date_interval=' + date_range_data['interval'];
131+
}
132+
133+
reloadTab(params);
134+
}
135+
136+
function update_date_preset_{{ options['rand'] }}(element) {
137+
if (!$(element).hasClass('active')) {
138+
date_range_data['interval'] = element.dataset.key;
139+
date_range_data['start_date'] = null;
140+
date_range_data['end_date'] = null;
141+
142+
$('#select_range_dropdown button.dropdown-toggle').html(element.innerHTML);
143+
reloadTab_{{ options['rand'] }}();
144+
}
145+
}
146+
function update_format_{{ options['rand'] }}(element) {
147+
date_range_data['format'] = element.dataset.key;
148+
149+
$('#select_format_dropdown button.dropdown-toggle').html(element.innerHTML);
150+
reloadTab_{{ options['rand'] }}();
151+
}
152+
function update_custom_date_range_{{ options['rand'] }}() {
153+
let range_date = $('#range-date_{{ options['rand'] }}')[0];
154+
let start_date = range_date._flatpickr.selectedDates[0];
155+
let end_date = range_date._flatpickr.selectedDates[1];
156+
157+
if (start_date && end_date) {
158+
// Igore timezone offset
159+
date_range_data['start_date'] = new Date(start_date.getTime() - (start_date.getTimezoneOffset() * 60000)).toISOString();
160+
date_range_data['end_date'] = new Date(end_date.getTime() - (end_date.getTimezoneOffset() * 60000)).toISOString();
161+
date_range_data['interval'] = null;
162+
163+
let label = __('Custom Range') + ': ';
164+
label += date_range_data['start_date'].split('T')[0];
165+
label += ' - ' + date_range_data['end_date'].split('T')[0];
166+
$('#select_range_dropdown button.dropdown-toggle').html(label);
167+
reloadTab_{{ options['rand'] }}();
168+
}
169+
}
170+
171+
$(document).ready(function () {
172+
let range_date = $('#range-date_{{ options['rand'] }}')[0];
173+
let dropdownInst = new bootstrap.Dropdown($('#select_range_dropdown button.dropdown-toggle'));
174+
175+
$('#show_custom_range').click(function () {
176+
event.stopPropagation();
177+
$('#date_range_input').toggleClass('d-none');
178+
179+
if ($('#show_custom_range span i.ti-plus').hasClass('rotate-45')) {
180+
$('#show_custom_range span i.ti-plus').addClass('rotate-90');
181+
$('#show_custom_range span i.ti-plus').removeClass('rotate-45');
182+
} else {
183+
$('#show_custom_range span i.ti-plus').removeClass('rotate-90');
184+
$('#show_custom_range span i.ti-plus').addClass('rotate-45');
185+
}
186+
});
187+
188+
range_date._flatpickr.config.mode = 'range';
189+
range_date._flatpickr.config.onOpen.push(function () {
190+
dropdownInst._config.autoClose = false
191+
});
192+
range_date._flatpickr.config.onClose.push(function () {
193+
dropdownInst._config.autoClose = true
194+
});
195+
196+
{% if start_date and end_date %}
197+
range_date._flatpickr.setDate([new Date('{{ start_date }}'), new Date('{{ end_date }}')]);
198+
{% endif %}
199+
});
200+
</script>

0 commit comments

Comments
 (0)