فهرست منبع

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">
 	<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://discord.gg/nXRUZch">Discord</a>
 	<br/><br/>

+ 7 - 7
bin/v-export-rrd

@@ -9,7 +9,7 @@
 #----------------------------------------------------------#
 
 chart=$1
-timespan=${2-hour}
+timespan=${2-daily}
 
 # Includes
 # shellcheck source=/etc/hestiacp/hestia.conf
@@ -33,8 +33,8 @@ function generate_load_table() {
 	rrdtool xport --json -s $start -e $end --step $step \
 		DEF:la=$RRD/la/la.rrd:LA: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() {
@@ -115,21 +115,21 @@ function generate_net_table() {
 			DEF:inoctets=$RRD/net/$host.rrd:RX:AVERAGE \
 			DEF:outoctets=$RRD/net/$host.rrd:TX:AVERAGE \
 			XPORT:inoctets:"Input (rx)" \
-			XPORT:outoctets:"Output (rx)"
+			XPORT:outoctets:"Output (tx)"
 	else
 		echo "Does not exists"
 		exit 1
 	fi
 }
 
-if [ "$timespan" = "week" ]; then
+if [ "$timespan" = "weekly" ]; then
 	start=$(date -d "7 days ago" +%s)
 	# every 30 min
 	step=3600
-elif [ "$timespan" = "month" ]; then
+elif [ "$timespan" = "monthly" ]; then
 	start=$(date -d "1 month ago" +%s)
 	step=21600
-elif [ "$timespan" = "year" ]; then
+elif [ "$timespan" = "yearly" ]; then
 	start=$(date -d "1 year ago" +%s)
 	step=172800
 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,
 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/
 Discord:        https://discord.gg/nXRUZch
 GitHub:         https://www.github.com/hestiacp/hestiacp
@@ -2181,7 +2181,7 @@ cat $tmpfile
 rm -f $tmpfile
 
 # 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
 # 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,
 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/
 Discord:        https://discord.gg/nXRUZch
 GitHub:         https://www.github.com/hestiacp/hestiacp
@@ -2229,7 +2229,7 @@ cat $tmpfile
 rm -f $tmpfile
 
 # 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
 # Sort final configuration file

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

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

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

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

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

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

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

@@ -109,10 +109,6 @@
 	padding-left: 30px !important;
 }
 
-.u-rounded {
-	border-radius: var(--border-radius-base) !important;
-}
-
 .u-pos-relative {
 	position: relative !important;
 }
@@ -133,17 +129,16 @@
 	min-height: 600px !important;
 }
 
+.u-max-height300 {
+	max-height: 300px !important;
+}
+
 .u-side-by-side {
 	display: flex !important;
 	justify-content: space-between !important;
 	align-items: center !important;
 }
 
-.u-image-fluid {
-	max-width: 100% !important;
-	height: auto !important;
-}
-
 .u-list-bulleted {
 	list-style: disc !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();
 }
 
-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 {
-	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 {
-	$service = $_POST["service"];
+	$service = "la";
 }
 
 // Data
@@ -31,6 +31,33 @@ exec(
 	$output,
 	$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["service"] = $service;
+$data["unit"] = $serviceUnits[$service] ?? null;
 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/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/events.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>
 <!-- End toolbar -->
 
+<script defer src="/js/vendor/chart.min.js?<?= JS_LATEST_UPDATE ?>"></script>
+
 <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 -->
 		<?php foreach ($data as $key => $value) { ?>
 			<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>
 		<?php } ?>
 	</div>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است