Merge branch 'feature/graphing' into 'master'

Added Graphing

See merge request fronk/thetool!753
This commit is contained in:
Luca Haid
2024-11-19 19:20:03 +00:00
7 changed files with 241 additions and 5 deletions

View File

@@ -0,0 +1,9 @@
<?php
/**
* @property mixed|null $name
*/
class Graphing extends mfBaseModel
{
}

View File

@@ -0,0 +1,64 @@
<?php
class GraphingController extends mfBaseController{
private string $ZABBIX_API_URL = ZABBIX_API_URL;
private string $ZABBIX_API_KEY = ZABBIX_API_KEY;
private Zabbix $zabbix;
protected function init(): void {
$me = new User();
$me->loadMe();
$this->layout()->set("me", $me);
$this->me = $me;
if (!$this->me->isAdmin()) {
$this->redirect("dashboard");
}
$this->zabbix = new Zabbix($this->ZABBIX_API_URL, $this->ZABBIX_API_KEY);
}
protected function indexAction() {
$this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]);
Helper::renderVue($this, "DeviceGraphing", $this->mod, []);
}
protected function dataAction() {
header('Content-Type: application/json');
$hostId = $this->request->hostId;
$hostInterfaceItems = $this->zabbix->getHostInterfaceItems($hostId, '');
// limit to 25 items
$hostInterfaceItems = array_slice($hostInterfaceItems, 0, 25);
$itemIds = array_map(function($item) {
return $item['itemid'];
}, $hostInterfaceItems);
$itemValues = $this->zabbix->getItemValues($itemIds, 1000);
$out = [];
foreach ($hostInterfaceItems as $item) {
$out[$item['itemid']] = [
'name' => str_replace('Bits', 'Mbps', $item['name']),
'units' => $item['units'],
'values' => []
];
}
foreach ($itemValues as $itemValue) {
$out[$itemValue['itemid']]['values'][] = [
'clock' => $itemValue['clock'],
'value' => $itemValue['value'] / 1000000
];
}
// sort by name
uasort($out, function($a, $b) {
return strcmp($a['name'], $b['name']);
});
die(json_encode($out));
}
}

View File

@@ -46,13 +46,13 @@ class Zabbix {
return $response['result'];
}
public function getItemValues($itemIds) {
public function getItemValues($itemIds, $limit = 15) {
$response = $this->zabbixRequest('history.get', array(
'itemids' => $itemIds,
'output' => 'extend',
'sortfield' => 'clock',
'sortorder' => 'DESC',
'limit' => 15
'limit' => $limit
));
return $response['result'];
}
@@ -64,10 +64,13 @@ class Zabbix {
return $response['result'];
}
public function getInterfaceItems($hostId, $interfaceName) {
public function getHostInterfaceItems($hostId) {
$response = $this->zabbixRequest('item.get', array(
'hostids' => $hostId,
'search' => array('name' => array($interfaceName, "Bits"))
'output' => ['itemid','name_resolved', 'key_', 'units'],
'search' => ['name' => ["Bits received", "Bits sent"]],
'searchByAny' => true,
'sortfield' => 'name'
));
return $response['result'];
}

View File

@@ -77,7 +77,7 @@ Vue.component('DeviceTable', {
<template v-if="row.zabbix_host_id !== '0' && row.zabbix_host_id !== null">
<a :href="window['TT_CONFIG']['ZABBIX_URL'] + '/zabbix.php?action=latest.view&hostids%5B%5D=' + row.zabbix_host_id" target="_blank" class="text-info" title="Zabbix"><i class="fas fa-server"></i></a>
<a :href="window['TT_CONFIG']['GRAFANA_URL'] + '/d/Ta3PtRWZk/mikrotik-dashboard?orgId=1&var-host=' + row.name" target="_blank" class="text-info" title="Grafana"><i class="fas fa-chart-line"></i></a>
<a :href="window['TT_CONFIG']['BASE_URL'] + '/Graphing?id=' + row.zabbix_host_id + '&hostname=' + row.name" target="_blank" class="text-info" title="Graphen"><i class="fas fa-chart-line"></i></a>
</template>
</template>

View File

@@ -0,0 +1,133 @@
Vue.component('tt-graph', {
template: `
<!-- use chart js with datasets etc everything needed for chart.js here to be usable width and height aswell -->
<tt-card>
<template v-slot:header>
<h3 style="text-align: center;user-select: none">{{ header }}</h3>
</template>
<div ref="container">
<canvas ref="chart" :style="{width: width + 'px', height: height + 'px'}"></canvas></div>
</tt-card>
`,
props: ['data', 'labels', 'header'],
data() {
return {
chart: null,
width: 400,
height: 220
}
},
mounted() {
const ctx = this.$refs.chart.getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: this.labels,
datasets: this.data
},
options: {
scales: {
y: {
beginAtZero: true
},
x: {
type: 'time',
time: {
// time is epoch
parser: 'X',
// unit: 'hour',
displayFormats: {
minute: 'DD.M. HH:mm',
}
},
// min: '00:00:00',
// max: '24:00:00',
ticks: {
autoSkipPadding: 25,
autoSkip: true,
maxRotation: 0
}
}
},
responsive: true,
},
});
// set width and height to the canvas element actual width and height
this.width = this.$refs.container.width;
this.height = this.$refs.container.height;
}
})
Vue.component('device-graphing', {
template: `
<div style="display: grid; grid-template-columns: 45vw 45vw; gap: 1rem;">
<h3 style="text-align: center;user-select: none;grid-column: 1 / span 2;">{{ hostname }}</h3>
<tt-loader v-if="graphs.length === 0"></tt-loader>
<template v-for="graph in graphs">
<tt-graph :data="graph.data" :labels="graph.labels" :header="graph.name"></tt-graph>
</template>
</div>
`,
data() {
return {
graphs: [],
hostname: ''
}
},
async mounted() {
// get hostname from url params
this.hostname = new URLSearchParams(window.location.search).get('hostname');
console.log(this.hostname);
// get id from url params
const id = new URLSearchParams(window.location.search).get('id');
const response = await axios.get('/Graphing/data?id=' + id);
const data = response.data;
const graphs = {};
for (const item in data) {
// Create graphs.[item.name.split(':')[0]] if it doesn't exist
if (!graphs[data[item].name.split(':')[0]]) {
graphs[data[item].name.split(':')[0]] = {
name: data[item].name.split(':')[0],
data: [],
labels: []
}
}
// Add the data to the graph but check if it's received or sent and already exists
if (data[item].name.split(':')[1].includes('received') && graphs[data[item].name.split(':')[0]].data.find(data => data.label.includes('received'))) {
continue;
}
if (data[item].name.split(':')[1].includes('sent') && graphs[data[item].name.split(':')[0]].data.find(data => data.label.includes('sent'))) {
continue;
}
graphs[data[item].name.split(':')[0]].data.push({
label: data[item].name.split(':')[1],
data: data[item].values.map(value => value.value),
fill: false,
borderColor: data[item].name.includes('received') ? 'rgb(75, 192, 192)' : 'rgb(192, 75, 75)',
tension: 0.1
});
}
// Add the labels to the this.graphs
for (const graph in graphs) {
graphs[graph].labels = data[Object.keys(data).find(key => data[key].name.includes(graph))].values.map(value => value.clock);
this.graphs.push(graphs[graph]);
}
}
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
/*!
* chartjs-adapter-moment v1.0.1
* https://www.chartjs.org
* (c) 2022 chartjs-adapter-moment Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));