Преглед изворни кода

Re-implement RRD charts in Chart.js (#3452)

* Update Chart.js version

* Re-implement Chart.js RRD charts

* Only load Chart.js on RRD page

* Improve charts on dark theme

* Improve chart labelling

* Increase size of charts

* Refactor chart theming

* Add Y axis labels and refine

* Set chart line colors in theme

* Limit height of charts

* Improve $serviceUnits code

* Improve service codes

Mysql and missing postgresql could also be
mysql_remotehost or pgsql_remotehost

* Use daily, weekly, montly instead day week and month

* Tidy RRD export labels

* Tidy

* Simplify docs links

---------

Co-authored-by: Jaap Marcus <9754650+jaapmarcus@users.noreply.github.com>
Alec Rust пре 2 година
родитељ
комит
3a254e1688

+ 1 - 1
README.md

@@ -8,7 +8,7 @@
 
 
 <p align="center">
 <p align="center">
 	<a href="https://www.hestiacp.com/">HestiaCP.com</a> |
 	<a href="https://www.hestiacp.com/">HestiaCP.com</a> |
-	<a href="https://hestiacp.com/docs/introduction/getting-started.html">Documentation</a> |
+	<a href="https://hestiacp.com/docs/">Documentation</a> |
 	<a href="https://forum.hestiacp.com/">Forum</a> |
 	<a href="https://forum.hestiacp.com/">Forum</a> |
 	<a href="https://discord.gg/nXRUZch">Discord</a>
 	<a href="https://discord.gg/nXRUZch">Discord</a>
 	<br/><br/>
 	<br/><br/>

+ 7 - 7
bin/v-export-rrd

@@ -9,7 +9,7 @@
 #----------------------------------------------------------#
 #----------------------------------------------------------#
 
 
 chart=$1
 chart=$1
-timespan=${2-hour}
+timespan=${2-daily}
 
 
 # Includes
 # Includes
 # shellcheck source=/etc/hestiacp/hestia.conf
 # shellcheck source=/etc/hestiacp/hestia.conf
@@ -33,8 +33,8 @@ function generate_load_table() {
 	rrdtool xport --json -s $start -e $end --step $step \
 	rrdtool xport --json -s $start -e $end --step $step \
 		DEF:la=$RRD/la/la.rrd:LA:AVERAGE \
 		DEF:la=$RRD/la/la.rrd:LA:AVERAGE \
 		DEF:pr=$RRD/la/la.rrd:PR:AVERAGE \
 		DEF:pr=$RRD/la/la.rrd:PR:AVERAGE \
-		XPORT:la:load \
-		XPORT:pr:Proccess
+		XPORT:la:Load \
+		XPORT:pr:Processes
 }
 }
 
 
 function generate_mem_table() {
 function generate_mem_table() {
@@ -115,21 +115,21 @@ function generate_net_table() {
 			DEF:inoctets=$RRD/net/$host.rrd:RX:AVERAGE \
 			DEF:inoctets=$RRD/net/$host.rrd:RX:AVERAGE \
 			DEF:outoctets=$RRD/net/$host.rrd:TX:AVERAGE \
 			DEF:outoctets=$RRD/net/$host.rrd:TX:AVERAGE \
 			XPORT:inoctets:"Input (rx)" \
 			XPORT:inoctets:"Input (rx)" \
-			XPORT:outoctets:"Output (rx)"
+			XPORT:outoctets:"Output (tx)"
 	else
 	else
 		echo "Does not exists"
 		echo "Does not exists"
 		exit 1
 		exit 1
 	fi
 	fi
 }
 }
 
 
-if [ "$timespan" = "week" ]; then
+if [ "$timespan" = "weekly" ]; then
 	start=$(date -d "7 days ago" +%s)
 	start=$(date -d "7 days ago" +%s)
 	# every 30 min
 	# every 30 min
 	step=3600
 	step=3600
-elif [ "$timespan" = "month" ]; then
+elif [ "$timespan" = "monthly" ]; then
 	start=$(date -d "1 month ago" +%s)
 	start=$(date -d "1 month ago" +%s)
 	step=21600
 	step=21600
-elif [ "$timespan" = "year" ]; then
+elif [ "$timespan" = "yearly" ]; then
 	start=$(date -d "1 year ago" +%s)
 	start=$(date -d "1 year ago" +%s)
 	step=172800
 	step=172800
 else
 else

+ 2 - 2
install/hst-install-debian.sh

@@ -2154,7 +2154,7 @@ we hope that you enjoy using it as much as we do!
 Please feel free to contact us at any time if you have any questions,
 Please feel free to contact us at any time if you have any questions,
 or if you encounter any bugs or problems:
 or if you encounter any bugs or problems:
 
 
-Documentation:  https://hestiacp.com/docs/introduction/getting-started.html
+Documentation:  https://hestiacp.com/docs/
 Forum:          https://forum.hestiacp.com/
 Forum:          https://forum.hestiacp.com/
 Discord:        https://discord.gg/nXRUZch
 Discord:        https://discord.gg/nXRUZch
 GitHub:         https://www.github.com/hestiacp/hestiacp
 GitHub:         https://www.github.com/hestiacp/hestiacp
@@ -2181,7 +2181,7 @@ cat $tmpfile
 rm -f $tmpfile
 rm -f $tmpfile
 
 
 # Add welcome message to notification panel
 # Add welcome message to notification panel
-$HESTIA/bin/v-add-user-notification admin 'Welcome to Hestia Control Panel!' '<br>You are now ready to begin <a href="/add/user/">adding user accounts</a> and <a href="/add/web/">domains</a>. For help and assistance, view the <a href="https://hestiacp.com/docs/introduction/getting-started.html" target="_blank">documentation</a> or visit our <a href="https://forum.hestiacp.com/" target="_blank">user forum</a>.<br><br>Please report any bugs or issues via <a href="https://github.com/hestiacp/hestiacp/issues" target="_blank"><i class="fab fa-github"></i> GitHub</a>.<br><br><b>Have a wonderful day!</b><br><br><i class="fas fa-heart icon-red"></i> The Hestia Control Panel development team'
+$HESTIA/bin/v-add-user-notification admin 'Welcome to Hestia Control Panel!' '<br>You are now ready to begin <a href="/add/user/">adding user accounts</a> and <a href="/add/web/">domains</a>. For help and assistance, view the <a href="https://hestiacp.com/docs/" target="_blank">documentation</a> or visit our <a href="https://forum.hestiacp.com/" target="_blank">user forum</a>.<br><br>Please report any bugs or issues via <a href="https://github.com/hestiacp/hestiacp/issues" target="_blank"><i class="fab fa-github"></i> GitHub</a>.<br><br><b>Have a wonderful day!</b><br><br><i class="fas fa-heart icon-red"></i> The Hestia Control Panel development team'
 
 
 # Clean-up
 # Clean-up
 # Sort final configuration file
 # Sort final configuration file

+ 2 - 2
install/hst-install-ubuntu.sh

@@ -2202,7 +2202,7 @@ we hope that you enjoy using it as much as we do!
 Please feel free to contact us at any time if you have any questions,
 Please feel free to contact us at any time if you have any questions,
 or if you encounter any bugs or problems:
 or if you encounter any bugs or problems:
 
 
-Documentation:  https://hestiacp.com/docs/introduction/getting-started.html
+Documentation:  https://hestiacp.com/docs/
 Forum:          https://forum.hestiacp.com/
 Forum:          https://forum.hestiacp.com/
 Discord:        https://discord.gg/nXRUZch
 Discord:        https://discord.gg/nXRUZch
 GitHub:         https://www.github.com/hestiacp/hestiacp
 GitHub:         https://www.github.com/hestiacp/hestiacp
@@ -2229,7 +2229,7 @@ cat $tmpfile
 rm -f $tmpfile
 rm -f $tmpfile
 
 
 # Add welcome message to notification panel
 # Add welcome message to notification panel
-$HESTIA/bin/v-add-user-notification admin 'Welcome to Hestia Control Panel!' '<br>You are now ready to begin <a href="/add/user/">adding user accounts</a> and <a href="/add/web/">domains</a>. For help and assistance, view the <a href="https://hestiacp.com/docs/introduction/getting-started.html" target="_blank">documentation</a> or visit our <a href="https://forum.hestiacp.com/" target="_blank">user forum</a>.<br><br>Please report any bugs or issues via <a href="https://github.com/hestiacp/hestiacp/issues" target="_blank"><i class="fab fa-github"></i> GitHub</a>.<br><br><b>Have a wonderful day!</b><br><br><i class="fas fa-heart icon-red"></i> The Hestia Control Panel development team'
+$HESTIA/bin/v-add-user-notification admin 'Welcome to Hestia Control Panel!' '<br>You are now ready to begin <a href="/add/user/">adding user accounts</a> and <a href="/add/web/">domains</a>. For help and assistance, view the <a href="https://hestiacp.com/docs/" target="_blank">documentation</a> or visit our <a href="https://forum.hestiacp.com/" target="_blank">user forum</a>.<br><br>Please report any bugs or issues via <a href="https://github.com/hestiacp/hestiacp/issues" target="_blank"><i class="fab fa-github"></i> GitHub</a>.<br><br><b>Have a wonderful day!</b><br><br><i class="fas fa-heart icon-red"></i> The Hestia Control Panel development team'
 
 
 # Clean-up
 # Clean-up
 # Sort final configuration file
 # Sort final configuration file

+ 4 - 0
web/css/src/themes/dark.css

@@ -21,6 +21,10 @@
 	--icon-color-maroon: #ff3478;
 	--icon-color-maroon: #ff3478;
 	--icon-color-green: #37cf39;
 	--icon-color-green: #37cf39;
 	--icon-color-blue: #0092f4;
 	--icon-color-blue: #0092f4;
+
+	/* Charts */
+	--chart-label-color: #cdcdcd;
+	--chart-grid-color: #434343;
 }
 }
 
 
 b,
 b,

+ 21 - 21
web/css/src/themes/default.css

@@ -35,6 +35,13 @@
 	--icon-color-orange: #ffc043;
 	--icon-color-orange: #ffc043;
 	--icon-color-blue: #326b9b;
 	--icon-color-blue: #326b9b;
 	--icon-color-lightblue: #6eb6f0;
 	--icon-color-lightblue: #6eb6f0;
+
+	/* Charts */
+	--chart-label-color: #7c7c7c;
+	--chart-grid-color: #e9e9e9;
+	--chart-line-1-color: var(--icon-color-blue);
+	--chart-line-2-color: var(--icon-color-maroon);
+	--chart-line-3-color: var(--icon-color-green);
 }
 }
 
 
 /* App header
 /* App header
@@ -382,7 +389,7 @@
 		background-color: rgb(0 0 0 / 5%);
 		background-color: rgb(0 0 0 / 5%);
 
 
 		& .main-menu-toggle-label {
 		& .main-menu-toggle-label {
-			color: #c36;
+			color: var(--color-text-link-hover);
 		}
 		}
 	}
 	}
 
 
@@ -442,16 +449,16 @@
 		background-color: rgb(0 0 0 / 5%);
 		background-color: rgb(0 0 0 / 5%);
 
 
 		& .main-menu-item-label {
 		& .main-menu-item-label {
-			color: #c36;
+			color: var(--color-text-link-hover);
 		}
 		}
 	}
 	}
 
 
 	&.active {
 	&.active {
 		& .main-menu-item-label {
 		& .main-menu-item-label {
-			color: #c36;
+			color: var(--color-text-link-hover);
 
 
 			& .fas {
 			& .fas {
-				color: #c36;
+				color: var(--color-text-link-hover);
 			}
 			}
 		}
 		}
 	}
 	}
@@ -467,7 +474,7 @@
 		&:hover,
 		&:hover,
 		&.active {
 		&.active {
 			background-color: transparent;
 			background-color: transparent;
-			border-bottom-color: #c36;
+			border-bottom-color: var(--color-text-link-hover);
 		}
 		}
 	}
 	}
 
 
@@ -765,12 +772,9 @@
 	text-transform: uppercase;
 	text-transform: uppercase;
 	font-weight: 600;
 	font-weight: 600;
 
 
-	&.selected {
-		color: #c36;
-	}
-
+	&.selected,
 	&:hover {
 	&:hover {
-		color: #c36;
+		color: var(--color-text-link-hover);
 	}
 	}
 
 
 	&:active {
 	&:active {
@@ -1393,10 +1397,6 @@
 	max-width: 920px;
 	max-width: 920px;
 }
 }
 
 
-.form-container-narrow {
-	max-width: 810px;
-}
-
 @media (--viewport-medium) {
 @media (--viewport-medium) {
 	.sidebar-right-container {
 	.sidebar-right-container {
 		display: grid;
 		display: grid;
@@ -1558,7 +1558,7 @@
 }
 }
 
 
 .form-link {
 .form-link {
-	color: #326b9b;
+	color: var(--color-text-link);
 	cursor: pointer;
 	cursor: pointer;
 	text-decoration: underline;
 	text-decoration: underline;
 	font-weight: 600;
 	font-weight: 600;
@@ -1836,7 +1836,7 @@
 	font-weight: 500;
 	font-weight: 500;
 	padding: 3px 25px;
 	padding: 3px 25px;
 	user-select: none;
 	user-select: none;
-	color: #30659d;
+	color: var(--color-text-link);
 	min-width: 100px;
 	min-width: 100px;
 	text-align: center;
 	text-align: center;
 	text-shadow: 0 1px 1px rgb(255 255 255 / 85%);
 	text-shadow: 0 1px 1px rgb(255 255 255 / 85%);
@@ -2066,7 +2066,7 @@
 	box-shadow: inset 0 0 1px rgb(255 255 255 / 100%), inset 0 0 4px rgb(255 255 255 / 80%),
 	box-shadow: inset 0 0 1px rgb(255 255 255 / 100%), inset 0 0 4px rgb(255 255 255 / 80%),
 		0 4px 6px rgb(190 190 190 / 40%);
 		0 4px 6px rgb(190 190 190 / 40%);
 	font-weight: 600;
 	font-weight: 600;
-	color: #30659d;
+	color: var(--color-text-link);
 	cursor: pointer;
 	cursor: pointer;
 	position: relative;
 	position: relative;
 	padding: 8px 18px;
 	padding: 8px 18px;
@@ -2168,7 +2168,7 @@
 	text-transform: uppercase;
 	text-transform: uppercase;
 
 
 	@media (--viewport-small) {
 	@media (--viewport-small) {
-		color: #326b9b;
+		color: var(--color-text-link);
 
 
 		&:hover {
 		&:hover {
 			color: #0077c6;
 			color: #0077c6;
@@ -2377,7 +2377,7 @@
 
 
 	&:hover,
 	&:hover,
 	&[aria-selected="true"] {
 	&[aria-selected="true"] {
-		color: #c36;
+		color: var(--color-text-link-hover);
 	}
 	}
 
 
 	&:active {
 	&:active {
@@ -2548,10 +2548,10 @@
 	color: #53ba55;
 	color: #53ba55;
 
 
 	& a {
 	& a {
-		color: #326b9b;
+		color: var(--color-text-link);
 
 
 		&:hover {
 		&:hover {
-			color: #c36;
+			color: var(--color-text-link-hover);
 		}
 		}
 
 
 		&:active {
 		&:active {

+ 2 - 2
web/css/src/themes/vestia.css

@@ -182,7 +182,7 @@ strong {
 	}
 	}
 
 
 	& #btn-back {
 	& #btn-back {
-		color: #30659d;
+		color: #326b9b;
 		background: none;
 		background: none;
 		text-shadow: none;
 		text-shadow: none;
 		text-transform: none;
 		text-transform: none;
@@ -278,7 +278,7 @@ strong {
 }
 }
 
 
 .button-secondary {
 .button-secondary {
-	color: #414141;
+	color: var(--color-text-link);
 	text-shadow: none;
 	text-shadow: none;
 	text-transform: none;
 	text-transform: none;
 	border-color: #bbb;
 	border-color: #bbb;

+ 4 - 9
web/css/src/utilities.css

@@ -109,10 +109,6 @@
 	padding-left: 30px !important;
 	padding-left: 30px !important;
 }
 }
 
 
-.u-rounded {
-	border-radius: var(--border-radius-base) !important;
-}
-
 .u-pos-relative {
 .u-pos-relative {
 	position: relative !important;
 	position: relative !important;
 }
 }
@@ -133,17 +129,16 @@
 	min-height: 600px !important;
 	min-height: 600px !important;
 }
 }
 
 
+.u-max-height300 {
+	max-height: 300px !important;
+}
+
 .u-side-by-side {
 .u-side-by-side {
 	display: flex !important;
 	display: flex !important;
 	justify-content: space-between !important;
 	justify-content: space-between !important;
 	align-items: center !important;
 	align-items: center !important;
 }
 }
 
 
-.u-image-fluid {
-	max-width: 100% !important;
-	height: auto !important;
-}
-
 .u-list-bulleted {
 .u-list-bulleted {
 	list-style: disc !important;
 	list-style: disc !important;
 	padding-left: 40px !important;
 	padding-left: 40px !important;

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
web/css/themes/dark.min.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
web/css/themes/default.min.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
web/css/themes/vestia.min.css


+ 105 - 45
web/js/pages/list_rrd.js

@@ -1,49 +1,109 @@
-// Default max of 3 lines are drawn for memory. Colors need to be update to work better
-colors = ['rgba(255,52,120,0.5)', 'rgba(255,52,0,0.5)', 'rgba(255,255,120,0.5)'];
-// Other markups are working see https://www.chartjs.org/docs/latest/
-
-// TODO: Make charts responsive
-(function () {
-	document.querySelectorAll('canvas').forEach(async (el) => {
-		const response = await fetch('/list/rrd/ajax.php', {
-			method: 'POST',
-			headers: {
-				'Content-Type': 'application/json',
+document.addEventListener('DOMContentLoaded', main);
+
+async function main() {
+	const chartCanvases = document.querySelectorAll('.js-rrd-chart');
+
+	for (const chartCanvas of chartCanvases) {
+		const service = chartCanvas.getAttribute('data-service');
+		const period = chartCanvas.getAttribute('data-period');
+		const rrdData = await fetchRrdData(service, period);
+		const chartData = prepareChartData(rrdData, period);
+		const chartOptions = getChartOptions(rrdData.unit);
+
+		new Chart(chartCanvas, {
+			type: 'line',
+			data: chartData,
+			options: chartOptions,
+		});
+	}
+}
+
+async function fetchRrdData(service, period) {
+	const response = await fetch('/list/rrd/ajax.php', {
+		method: 'POST',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({ service, period }),
+	});
+
+	return response.json();
+}
+
+function prepareChartData(rrdData, period) {
+	const totalDatasets = rrdData.meta.legend.length;
+
+	return {
+		labels: rrdData.data.map((_, index) => {
+			const timestamp = rrdData.meta.start + index * rrdData.meta.step;
+			const date = new Date(timestamp * 1000);
+			return formatLabel(date, period);
+		}),
+		datasets: rrdData.meta.legend.map((legend, legendIndex) => {
+			const lineColor = getCssVariable(`--chart-line-${legendIndex + 1}-color`);
+
+			return {
+				label: legend,
+				data: rrdData.data.map((dataPoint) => dataPoint[legendIndex]),
+				tension: 0.3,
+				pointStyle: false,
+				fill: legendIndex === 0 && totalDatasets > 1,
+				borderWidth: 2,
+				borderColor: lineColor,
+				backgroundColor: lineColor,
+			};
+		}),
+	};
+}
+
+function formatLabel(date, period) {
+	const options = {
+		daily: { hour: '2-digit', minute: '2-digit' },
+		weekly: { weekday: 'short', day: 'numeric' },
+		monthly: { month: 'short', day: 'numeric' },
+		yearly: { month: 'long' },
+	};
+
+	return date.toLocaleString([], options[period]);
+}
+
+function getChartOptions(unit) {
+	const labelColor = getCssVariable('--chart-label-color');
+	const gridColor = getCssVariable('--chart-grid-color');
+
+	return {
+		plugins: {
+			legend: {
+				position: 'bottom',
+				labels: {
+					color: labelColor,
+				},
 			},
 			},
-			body: {
-				service: el.getAttribute('id'),
-				period: el.getAttribute('period'),
+		},
+		scales: {
+			x: {
+				ticks: {
+					color: labelColor,
+				},
+				grid: {
+					color: gridColor,
+				},
 			},
 			},
-		});
-		const rrdData = await response.clone().json();
-
-		// data is stored as start, end time and step between each step
-		const labels = [];
-		for (i = rrdData.meta.start; i < rrdData.meta.end; i = i + rrdData.meta.step) {
-			labels.push(new Date(i * 1000).toLocaleString());
-		}
-
-		// rrdData.data stores data as i[x,y] useless for chartjs split in separate datasets
-		const datasets = [];
-		for (i = 0; i < rrdData.meta.legend.length; i++) {
-			const data = [];
-			for (b of rrdData.data) {
-				data.push(b[i]);
-			}
-			dataset = { label: rrdData.meta.legend[i], data: data, borderColor: colors[i] };
-			datasets.push(dataset);
-		}
-
-		// draw chart
-		const ctx = document.getElementById(rrdData.service).getContext('2d');
-		new Chart(ctx, {
-			type: 'line',
-			data: { labels, datasets },
-			options: {
-				scales: {
-					y: { beginAtZero: true },
+			y: {
+				title: {
+					display: !!unit,
+					text: unit,
+					color: labelColor,
+				},
+				ticks: {
+					color: labelColor,
+				},
+				grid: {
+					color: gridColor,
 				},
 				},
 			},
 			},
-		});
-	});
-})();
+		},
+	};
+}
+
+function getCssVariable(variableName) {
+	return getComputedStyle(document.documentElement).getPropertyValue(variableName).trim();
+}

Разлика између датотеке није приказан због своје велике величине
+ 2 - 2
web/js/vendor/chart.min.js


+ 37 - 10
web/list/rrd/ajax.php

@@ -9,20 +9,20 @@ if ($_SESSION["userContext"] != "admin") {
 	exit();
 	exit();
 }
 }
 
 
-if (empty($_POST["period"])) {
-	$period = "daily";
+$requestPayload = json_decode(file_get_contents("php://input"), true);
+
+$allowedPeriods = ["daily", "weekly", "monthly", "yearly"];
+
+if (!empty($requestPayload["period"]) && in_array($requestPayload["period"], $allowedPeriods)) {
+	$period = $requestPayload["period"];
 } else {
 } else {
-	if (in_array($_POST["period"], ["day", "week", "month", "year"])) {
-		$period = $_POST["period"];
-	} else {
-		$period = "daily";
-	}
+	$period = "daily";
 }
 }
 
 
-if (empty($_POST["service"])) {
-	$service = "la";
+if (!empty($requestPayload["service"])) {
+	$service = $requestPayload["service"];
 } else {
 } else {
-	$service = $_POST["service"];
+	$service = "la";
 }
 }
 
 
 // Data
 // Data
@@ -31,6 +31,33 @@ exec(
 	$output,
 	$output,
 	$return_var,
 	$return_var,
 );
 );
+
+if ($return_var != 0) {
+	http_response_code(500);
+	exit("Error fetching RRD data");
+}
+
+$serviceUnits = [
+	"la" => "Points",
+	"mem" => "Mbytes",
+	"apache2" => "Connections",
+	"nginx" => "Connections",
+	"mail" => "Queue Size",
+	"ftp" => "Connections",
+	"ssh" => "Connections",
+];
+
+if (preg_match("/^net_/", $service)) {
+	$serviceUnits[$service] = "KBytes";
+}
+if (preg_match("/^pgsql_/", $service)) {
+	$serviceUnits[$service] = "Queries";
+}
+if (preg_match("/^mysql_/", $service)) {
+	$serviceUnits[$service] = "Queries";
+}
+
 $data = json_decode(implode("", $output), true);
 $data = json_decode(implode("", $output), true);
 $data["service"] = $service;
 $data["service"] = $service;
+$data["unit"] = $serviceUnits[$service] ?? null;
 echo json_encode($data);
 echo json_encode($data);

+ 0 - 1
web/templates/includes/js.php

@@ -1,6 +1,5 @@
 <script defer src="/js/main.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/main.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/vendor/jquery-3.6.4.min.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/vendor/jquery-3.6.4.min.js?<?= JS_LATEST_UPDATE ?>"></script>
-<script defer src="/js/vendor/chart.min.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/shortcuts.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/shortcuts.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/events.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/events.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/init.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/init.js?<?= JS_LATEST_UPDATE ?>"></script>

+ 9 - 7
web/templates/pages/list_rrd.php

@@ -15,17 +15,19 @@
 </div>
 </div>
 <!-- End toolbar -->
 <!-- End toolbar -->
 
 
+<script defer src="/js/vendor/chart.min.js?<?= JS_LATEST_UPDATE ?>"></script>
+
 <div class="container animate__animated animate__fadeIn">
 <div class="container animate__animated animate__fadeIn">
-	<div class="form-container form-container-narrow">
+	<div class="form-container form-container-wide">
 		<!-- Begin graph list item loop -->
 		<!-- Begin graph list item loop -->
 		<?php foreach ($data as $key => $value) { ?>
 		<?php foreach ($data as $key => $value) { ?>
 			<div class="u-mb40">
 			<div class="u-mb40">
-				<h2 class="u-mb20">
-					<?= _($data[$key]["TITLE"]) ?>
-				</h2>
-				<a href="/list/rrd/image.php?/rrd/<?= $data[$key]["TYPE"] . "/" . $period . "-" . $data[$key]["RRD"] . ".png" ?>" class="u-block" target="_blank">
-					<img class="u-image-fluid u-rounded" src="/list/rrd/image.php?/rrd/<?= $data[$key]["TYPE"] . "/" . $period . "-" . $data[$key]["RRD"] . ".png" ?>" alt="">
-				</a>
+				<h2 class="u-mb20"><?= htmlspecialchars($data[$key]["TITLE"]) ?></h2>
+				<canvas
+					class="u-max-height300 js-rrd-chart"
+					data-service="<?= $data[$key]["TYPE"] !== 'net' ? htmlspecialchars($data[$key]["RRD"]) : 'net_' . htmlspecialchars($data[$key]["RRD"]); ?>"
+					data-period="<?= htmlspecialchars($period) ?>"
+				></canvas>
 			</div>
 			</div>
 		<?php } ?>
 		<?php } ?>
 	</div>
 	</div>

Неке датотеке нису приказане због велике количине промена