add graphs for historical device metrics
This commit is contained in:
43
src/index.js
43
src/index.js
@ -84,6 +84,49 @@ app.get('/api/v1/nodes', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/v1/nodes/:nodeId/device-metrics', async (req, res) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const nodeId = parseInt(req.params.nodeId);
|
||||||
|
const count = req.query.count ? parseInt(req.query.count) : undefined;
|
||||||
|
|
||||||
|
// find node
|
||||||
|
const node = await prisma.node.findFirst({
|
||||||
|
where: {
|
||||||
|
node_id: nodeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// make sure node exists
|
||||||
|
if(!node){
|
||||||
|
res.status(404).json({
|
||||||
|
message: "Not Found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// get latest device metrics
|
||||||
|
const deviceMetrics = await prisma.deviceMetric.findMany({
|
||||||
|
where: {
|
||||||
|
node_id: node.node_id,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'desc',
|
||||||
|
},
|
||||||
|
take: count,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
device_metrics: deviceMetrics,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({
|
||||||
|
message: "Something went wrong, try again later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/v1/stats/hardware-models', async (req, res) => {
|
app.get('/api/v1/stats/hardware-models', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
@ -28,6 +28,9 @@
|
|||||||
<script src="https://unpkg.com/vue@3"></script>
|
<script src="https://unpkg.com/vue@3"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
|
||||||
|
<!-- chart js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.icon-online {
|
.icon-online {
|
||||||
@ -368,7 +371,7 @@
|
|||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<div class="block flex-1 px-4 py-2">
|
<div class="block flex-1 px-4 py-2">
|
||||||
<div class="relative flex min-w-0 flex-1 items-center">
|
<div class="relative flex min-w-0 flex-1 items-center">
|
||||||
<div class="truncate">
|
<div class="w-full">
|
||||||
<p class="truncate text-sm font-medium text-gray-900">Battery Level</p>
|
<p class="truncate text-sm font-medium text-gray-900">Battery Level</p>
|
||||||
<p class="truncate text-sm text-gray-700">
|
<p class="truncate text-sm text-gray-700">
|
||||||
<span v-if="selectedNode.battery_level">
|
<span v-if="selectedNode.battery_level">
|
||||||
@ -378,6 +381,9 @@
|
|||||||
<span v-else class="text-gray-500">???</span>
|
<span v-else class="text-gray-500">???</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas id="batteryLevelChart" style="width:150px;height:50px;"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -387,13 +393,16 @@
|
|||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<div class="block flex-1 px-4 py-2">
|
<div class="block flex-1 px-4 py-2">
|
||||||
<div class="relative flex min-w-0 flex-1 items-center">
|
<div class="relative flex min-w-0 flex-1 items-center">
|
||||||
<div class="truncate">
|
<div class="w-full">
|
||||||
<p class="truncate text-sm font-medium text-gray-900">Voltage</p>
|
<p class="truncate text-sm font-medium text-gray-900">Voltage</p>
|
||||||
<p class="truncate text-sm text-gray-700">
|
<p class="truncate text-sm text-gray-700">
|
||||||
<span v-if="selectedNode.voltage">{{ Number(selectedNode.voltage).toFixed(2) }}V</span>
|
<span v-if="selectedNode.voltage">{{ Number(selectedNode.voltage).toFixed(2) }}V</span>
|
||||||
<span v-else class="text-gray-500">???</span>
|
<span v-else class="text-gray-500">???</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas id="voltageChart" style="width:150px;height:50px;"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -403,13 +412,16 @@
|
|||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<div class="block flex-1 px-4 py-2">
|
<div class="block flex-1 px-4 py-2">
|
||||||
<div class="relative flex min-w-0 flex-1 items-center">
|
<div class="relative flex min-w-0 flex-1 items-center">
|
||||||
<div class="truncate">
|
<div class="w-full">
|
||||||
<p class="truncate text-sm font-medium text-gray-900">Channel Utilization</p>
|
<p class="truncate text-sm font-medium text-gray-900">Channel Utilization</p>
|
||||||
<p class="truncate text-sm text-gray-700">
|
<p class="truncate text-sm text-gray-700">
|
||||||
<span v-if="selectedNode.channel_utilization">{{ Number(selectedNode.channel_utilization).toFixed(2) }}%</span>
|
<span v-if="selectedNode.channel_utilization">{{ Number(selectedNode.channel_utilization).toFixed(2) }}%</span>
|
||||||
<span v-else class="text-gray-500">???</span>
|
<span v-else class="text-gray-500">???</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas id="channelUtilizationChart" style="width:150px;height:50px;"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -419,13 +431,16 @@
|
|||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<div class="block flex-1 px-4 py-2">
|
<div class="block flex-1 px-4 py-2">
|
||||||
<div class="relative flex min-w-0 flex-1 items-center">
|
<div class="relative flex min-w-0 flex-1 items-center">
|
||||||
<div class="truncate">
|
<div class="w-full">
|
||||||
<p class="truncate text-sm font-medium text-gray-900">Air Util Tx</p>
|
<p class="truncate text-sm font-medium text-gray-900">Air Util Tx</p>
|
||||||
<p class="truncate text-sm text-gray-700">
|
<p class="truncate text-sm text-gray-700">
|
||||||
<span v-if="selectedNode.air_util_tx">{{ Number(selectedNode.air_util_tx).toFixed(2) }}%</span>
|
<span v-if="selectedNode.air_util_tx">{{ Number(selectedNode.air_util_tx).toFixed(2) }}%</span>
|
||||||
<span v-else class="text-gray-500">???</span>
|
<span v-else class="text-gray-500">???</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas id="airUtilTxChart" style="width:150px;height:50px;"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -512,6 +527,7 @@
|
|||||||
hardwareModelStats: null,
|
hardwareModelStats: null,
|
||||||
|
|
||||||
selectedNode: null,
|
selectedNode: null,
|
||||||
|
selectedNodeDeviceMetrics: [],
|
||||||
|
|
||||||
moment: window.moment,
|
moment: window.moment,
|
||||||
|
|
||||||
@ -525,6 +541,7 @@
|
|||||||
// handle node callback from outside of vue
|
// handle node callback from outside of vue
|
||||||
window._onNodeClick = (node) => {
|
window._onNodeClick = (node) => {
|
||||||
this.selectedNode = node;
|
this.selectedNode = node;
|
||||||
|
this.loadNodeDeviceMetrics(node.node_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -536,6 +553,257 @@
|
|||||||
// do nothing
|
// do nothing
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
loadNodeDeviceMetrics: function(nodeId) {
|
||||||
|
window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
|
||||||
|
params: {
|
||||||
|
count: 100,
|
||||||
|
},
|
||||||
|
}).then((response) => {
|
||||||
|
this.selectedNodeDeviceMetrics = response.data.device_metrics;
|
||||||
|
this.renderDeviceMetricCharts();
|
||||||
|
}).catch(() => {
|
||||||
|
this.selectedNodeDeviceMetrics = [];
|
||||||
|
this.renderDeviceMetricCharts();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderDeviceMetricCharts: function() {
|
||||||
|
this.updateBatteryLevelChart();
|
||||||
|
this.updateVoltageChart();
|
||||||
|
this.updateChannelUtilizationChart();
|
||||||
|
this.updateAirUtilTxChart();
|
||||||
|
},
|
||||||
|
updateBatteryLevelChart: function() {
|
||||||
|
|
||||||
|
// get chart context
|
||||||
|
const ctx = window.document.getElementById('batteryLevelChart')?.getContext('2d');
|
||||||
|
if(!ctx){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new window.Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return new Date(deviceMetric.created_at).toLocaleTimeString();
|
||||||
|
}),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Battery Level',
|
||||||
|
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return deviceMetric.battery_level;
|
||||||
|
}),
|
||||||
|
borderColor: '#22c55e',
|
||||||
|
backgroundColor: '#e5e7eb',
|
||||||
|
fill: true,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false, // Hide x-axis labels
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: false, // Hide y-axis labels
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false, // Hide the legend
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
yAlign: 'top',
|
||||||
|
intersect: false,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: (item) => item.formattedValue + "%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0, // Set the radius to 0 to hide the dots
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
updateVoltageChart: function() {
|
||||||
|
|
||||||
|
// get chart context
|
||||||
|
const ctx = window.document.getElementById('voltageChart')?.getContext('2d');
|
||||||
|
if(!ctx){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new window.Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return new Date(deviceMetric.created_at).toLocaleTimeString();
|
||||||
|
}),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Voltage',
|
||||||
|
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return deviceMetric.voltage;
|
||||||
|
}),
|
||||||
|
borderColor: '#22c55e',
|
||||||
|
backgroundColor: '#e5e7eb',
|
||||||
|
fill: true,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false, // Hide x-axis labels
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: false, // Hide y-axis labels
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false, // Hide the legend
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
yAlign: 'top',
|
||||||
|
intersect: false,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: (item) => item.formattedValue + "V",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0, // Set the radius to 0 to hide the dots
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
updateChannelUtilizationChart: function() {
|
||||||
|
|
||||||
|
// get chart context
|
||||||
|
const ctx = window.document.getElementById('channelUtilizationChart')?.getContext('2d');
|
||||||
|
if(!ctx){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new window.Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return new Date(deviceMetric.created_at).toLocaleTimeString();
|
||||||
|
}),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Channel Utilization',
|
||||||
|
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return deviceMetric.channel_utilization;
|
||||||
|
}),
|
||||||
|
borderColor: '#22c55e',
|
||||||
|
backgroundColor: '#e5e7eb',
|
||||||
|
fill: true,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false, // Hide x-axis labels
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: false, // Hide y-axis labels
|
||||||
|
min: -5,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false, // Hide the legend
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
yAlign: 'top',
|
||||||
|
intersect: false,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: (item) => item.formattedValue + "%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0, // Set the radius to 0 to hide the dots
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
updateAirUtilTxChart: function() {
|
||||||
|
|
||||||
|
// get chart context
|
||||||
|
const ctx = window.document.getElementById('airUtilTxChart')?.getContext('2d');
|
||||||
|
if(!ctx){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new window.Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return new Date(deviceMetric.created_at).toLocaleTimeString();
|
||||||
|
}),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Air Util Tx',
|
||||||
|
data: this.selectedNodeDeviceMetrics.map((deviceMetric) => {
|
||||||
|
return deviceMetric.air_util_tx;
|
||||||
|
}),
|
||||||
|
borderColor: '#22c55e',
|
||||||
|
backgroundColor: '#e5e7eb',
|
||||||
|
fill: true,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false, // Hide x-axis labels
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: false, // Hide y-axis labels
|
||||||
|
min: -5,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false, // Hide the legend
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
yAlign: 'top',
|
||||||
|
intersect: false,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: (item) => item.formattedValue + "%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0, // Set the radius to 0 to hide the dots
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user