mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-07 01:12:54 +02:00
Added new searching capabilities to our node reports
This commit is contained in:
@@ -28,5 +28,3 @@ Other Options:
|
||||
--topic
|
||||
MQTT Topic, default is 'msh/US/bayarea/#'
|
||||
|
||||
|
||||
<img src="images/main.png" alt="Main Page" >
|
||||
|
||||
+1
-2
@@ -524,7 +524,7 @@ async def get_nodes(role=None, channel=None, hw_model=None):
|
||||
"""
|
||||
try:
|
||||
async with database.async_session() as session:
|
||||
print(channel) # Debugging output (consider replacing with logging)
|
||||
#print(channel) # Debugging output (consider replacing with logging)
|
||||
|
||||
# Start with a base query selecting all nodes
|
||||
query = select(Node)
|
||||
@@ -546,7 +546,6 @@ async def get_nodes(role=None, channel=None, hw_model=None):
|
||||
# Execute the query and retrieve results
|
||||
result = await session.execute(query)
|
||||
nodes = result.scalars().all()
|
||||
|
||||
return nodes # Return the list of nodes
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -16,6 +16,7 @@ th, td {
|
||||
th {
|
||||
background-color: #1f1f1f;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
@@ -25,37 +26,86 @@ tr:nth-child(even) {
|
||||
tr:nth-child(odd) {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.search, .filter-role, .filter-channel, .export-btn {
|
||||
padding: 8px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-role, .filter-channel {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div>
|
||||
<div id="node-list">
|
||||
<div class="search-container">
|
||||
<input class="search" placeholder="Search nodes..." />
|
||||
|
||||
<!-- Filter by Role Dropdown -->
|
||||
<select class="filter-role" onchange="applyFilters()">
|
||||
<option value="">Filter by Role</option>
|
||||
{% for node in nodes|groupby('role') %}
|
||||
<option value="{{ node.grouper }}">{{ node.grouper }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<!-- Filter by Channel Dropdown -->
|
||||
<select class="filter-channel" onchange="applyFilters()">
|
||||
<option value="">Filter by Channel</option>
|
||||
{% for node in nodes|groupby('channel') %}
|
||||
<option value="{{ node.grouper }}">{{ node.grouper }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="export-btn" onclick="exportToCSV()">Export to CSV</button>
|
||||
</div>
|
||||
|
||||
{% if nodes %}
|
||||
<table>
|
||||
<table id="node-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Long Name</th>
|
||||
<th>Short Name</th>
|
||||
<th>HW Model</th>
|
||||
<th>Firmware</th>
|
||||
<th>Role</th>
|
||||
<th>Last Latitude</th>
|
||||
<th>Last Longitude</th>
|
||||
<th>Channel</th>
|
||||
<th>Last Update</th>
|
||||
<th class="sort" data-sort="long_name">Long Name</th>
|
||||
<th class="sort" data-sort="short_name">Short Name</th>
|
||||
<th class="sort" data-sort="hw_model">HW Model</th>
|
||||
<th class="sort" data-sort="firmware">Firmware</th>
|
||||
<th class="sort" data-sort="role">Role</th>
|
||||
<th class="sort" data-sort="last_lat">Last Latitude</th>
|
||||
<th class="sort" data-sort="last_long">Last Longitude</th>
|
||||
<th class="sort" data-sort="channel">Channel</th>
|
||||
<th class="sort" data-sort="last_update">Last Update</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="list">
|
||||
{% for node in nodes %}
|
||||
<tr>
|
||||
<td> <a href="/packet_list/{{node.node_id }}">{{node.long_name}}</a></td>
|
||||
<td>{{ node.short_name }}</td>
|
||||
<td><a href="/nodelist?hw_model={{node.hw_model}}">{{node.hw_model if node.hw_model else "N/A"}}</a></td>
|
||||
<td>{{ node.firmware }}</td>
|
||||
<td><a href="/nodelist?role={{node.role}}">{{node.role if node.role else "N/A"}}</a></td>
|
||||
<td>{{ "{:.7f}".format(node.last_lat / 10**7) if node.last_lat else "N/A" }}</td>
|
||||
<td>{{ "{:.7f}".format(node.last_long / 10**7) if node.last_long else "N/A" }}</td>
|
||||
<td><a href="/nodelist?channel={{node.channel}}">{{node.channel if node.channel else "N/A"}}</a></td>
|
||||
<td>{{ node.last_update.strftime('%-I:%M:%S %p - %m-%d-%Y') if node.last_update else "N/A" }}</td>
|
||||
<td class="long_name"> <a href="/packet_list/{{ node.node_id }}">{{ node.long_name }}</a></td>
|
||||
<td class="short_name">{{ node.short_name }}</td>
|
||||
<td class="hw_model">{{ node.hw_model if node.hw_model else "N/A" }}</td>
|
||||
<td class="firmware">{{ node.firmware }}</td>
|
||||
<td class="role">{{ node.role if node.role else "N/A" }}</td>
|
||||
<td class="last_lat">{{ "{:.7f}".format(node.last_lat / 10**7) if node.last_lat else "N/A" }}</td>
|
||||
<td class="last_long">{{ "{:.7f}".format(node.last_long / 10**7) if node.last_long else "N/A" }}</td>
|
||||
<td class="channel">{{ node.channel if node.channel else "N/A" }}</td>
|
||||
<td class="last_update">{{ node.last_update.strftime('%-I:%M:%S %p - %m-%d-%Y') if node.last_update else "N/A" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -64,4 +114,56 @@ tr:nth-child(odd) {
|
||||
<p>No nodes found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
|
||||
<script>
|
||||
var nodeList;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var options = {
|
||||
valueNames: ["long_name", "short_name", "hw_model", "firmware", "role", "last_lat", "last_long", "channel", "last_update"]
|
||||
};
|
||||
nodeList = new List("node-list", options);
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
var selectedRole = document.querySelector(".filter-role").value;
|
||||
var selectedChannel = document.querySelector(".filter-channel").value;
|
||||
|
||||
nodeList.filter(function (item) {
|
||||
var matchesRole = selectedRole === "" || item.values().role === selectedRole;
|
||||
var matchesChannel = selectedChannel === "" || item.values().channel === selectedChannel;
|
||||
return matchesRole && matchesChannel;
|
||||
});
|
||||
}
|
||||
|
||||
function exportToCSV() {
|
||||
var table = document.getElementById("node-table");
|
||||
var rows = table.querySelectorAll("tr");
|
||||
var csvContent = [];
|
||||
|
||||
// Extract header row
|
||||
var headers = [];
|
||||
table.querySelectorAll("th").forEach(th => headers.push(th.innerText));
|
||||
csvContent.push(headers.join(","));
|
||||
|
||||
// Extract table rows
|
||||
rows.forEach(row => {
|
||||
var cells = row.querySelectorAll("td");
|
||||
if (cells.length > 0) {
|
||||
var rowData = [];
|
||||
cells.forEach(cell => rowData.push(cell.innerText));
|
||||
csvContent.push(rowData.join(","));
|
||||
}
|
||||
});
|
||||
|
||||
// Create CSV file and trigger download
|
||||
var csvString = csvContent.join("\n");
|
||||
var blob = new Blob([csvString], { type: "text/csv" });
|
||||
var a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = "nodes_list.csv";
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+2
-2
@@ -1477,9 +1477,9 @@ async def nodelist(request):
|
||||
role = request.query.get("role")
|
||||
#print(role)
|
||||
channel = request.query.get("channel")
|
||||
print(channel)
|
||||
#print(channel)
|
||||
hw_model = request.query.get("hw_model")
|
||||
print(hw_model)
|
||||
#print(hw_model)
|
||||
nodes= await store.get_nodes(role,channel, hw_model)
|
||||
template = env.get_template("nodelist.html")
|
||||
return web.Response(
|
||||
|
||||
Reference in New Issue
Block a user