Added device types/icons and stats page
This commit is contained in:
@@ -76,7 +76,9 @@ def init_db(app=None):
|
|||||||
CREATE TABLE IF NOT EXISTS Device (
|
CREATE TABLE IF NOT EXISTS Device (
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
description TEXT
|
description TEXT,
|
||||||
|
device_type_id INTEGER DEFAULT 1,
|
||||||
|
FOREIGN KEY (device_type_id) REFERENCES DeviceType(id)
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
@@ -98,6 +100,35 @@ def init_db(app=None):
|
|||||||
FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE CASCADE
|
FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS DeviceType (
|
||||||
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
icon_class VARCHAR(255) NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
cursor.execute('SELECT COUNT(*) FROM DeviceType')
|
||||||
|
if cursor.fetchone()[0] == 0:
|
||||||
|
cursor.executemany('INSERT INTO DeviceType (name, icon_class) VALUES (%s, %s)', [
|
||||||
|
('Server', 'fa-server'),
|
||||||
|
('Virtual Machine', 'fa-boxes-stacked'),
|
||||||
|
('Switch', 'fa-network-wired'),
|
||||||
|
('Firewall', 'fa-shield-halved'),
|
||||||
|
('WiFi AP', 'fa-wifi'),
|
||||||
|
('Printer', 'fa-print'),
|
||||||
|
('Other', 'fa-question')
|
||||||
|
])
|
||||||
|
cursor.execute("SHOW COLUMNS FROM Device LIKE 'device_type_id'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
cursor.execute('ALTER TABLE Device ADD COLUMN device_type_id INTEGER DEFAULT NULL')
|
||||||
|
cursor.execute("SELECT id FROM DeviceType WHERE name='Other'")
|
||||||
|
other_id = cursor.fetchone()[0]
|
||||||
|
cursor.execute('UPDATE Device SET device_type_id = %s WHERE device_type_id IS NULL', (other_id,))
|
||||||
|
try:
|
||||||
|
cursor.execute('ALTER TABLE Device ADD CONSTRAINT fk_device_type FOREIGN KEY (device_type_id) REFERENCES DeviceType(id)')
|
||||||
|
except mysql.connector.Error as e:
|
||||||
|
if e.errno != 1061 and e.errno != 1826 and 'Duplicate' not in str(e):
|
||||||
|
raise
|
||||||
cursor.execute('SELECT COUNT(*) FROM User')
|
cursor.execute('SELECT COUNT(*) FROM User')
|
||||||
if cursor.fetchone()[0] == 0:
|
if cursor.fetchone()[0] == 0:
|
||||||
cursor.execute('''INSERT INTO User (name, email, password) VALUES (%s, %s, %s)''',
|
cursor.execute('''INSERT INTO User (name, email, password) VALUES (%s, %s, %s)''',
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ def register_routes(app):
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT id, name FROM Device')
|
cursor.execute('''SELECT Device.id, Device.name, DeviceType.icon_class FROM Device LEFT JOIN DeviceType ON Device.device_type_id = DeviceType.id''')
|
||||||
devices = cursor.fetchall()
|
devices = cursor.fetchall()
|
||||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
||||||
subnets = cursor.fetchall()
|
subnets = cursor.fetchall()
|
||||||
@@ -94,21 +94,26 @@ def register_routes(app):
|
|||||||
site = site[0] if site else 'Unassigned'
|
site = site[0] if site else 'Unassigned'
|
||||||
if site not in sites_devices:
|
if site not in sites_devices:
|
||||||
sites_devices[site] = []
|
sites_devices[site] = []
|
||||||
sites_devices[site].append({'id': device[0], 'name': device[1]})
|
sites_devices[site].append({'id': device[0], 'name': device[1], 'icon_class': device[2]})
|
||||||
return render_with_user('devices.html', sites_devices=sites_devices, device_ips=device_ips)
|
return render_with_user('devices.html', sites_devices=sites_devices, device_ips=device_ips)
|
||||||
|
|
||||||
@app.route('/add_device', methods=['GET', 'POST'])
|
@app.route('/add_device', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def add_device():
|
def add_device():
|
||||||
if request.method == 'POST':
|
|
||||||
name = request.form['device_name']
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('INSERT INTO Device (name) VALUES (%s)', (name,))
|
cursor.execute('SELECT id, name FROM DeviceType ORDER BY name')
|
||||||
|
device_types = cursor.fetchall()
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.form['device_name']
|
||||||
|
device_type_id = int(request.form['device_type'])
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('INSERT INTO Device (name, device_type_id) VALUES (%s, %s)', (name, device_type_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return redirect(url_for('devices'))
|
return redirect(url_for('devices'))
|
||||||
return render_with_user('add_device.html')
|
return render_with_user('add_device.html', device_types=device_types)
|
||||||
|
|
||||||
@app.route('/device/<int:device_id>')
|
@app.route('/device/<int:device_id>')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -116,8 +121,10 @@ def register_routes(app):
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT id, name, description FROM Device WHERE id = %s', (device_id,))
|
cursor.execute('SELECT id, name, description, device_type_id FROM Device WHERE id = %s', (device_id,))
|
||||||
device = cursor.fetchone()
|
device = cursor.fetchone()
|
||||||
|
cursor.execute('SELECT id, name FROM DeviceType ORDER BY name')
|
||||||
|
device_types = cursor.fetchall()
|
||||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
||||||
subnets = [dict(id=row[0], name=row[1], cidr=row[2], site=row[3]) for row in cursor.fetchall()]
|
subnets = [dict(id=row[0], name=row[1], cidr=row[2], site=row[3]) for row in cursor.fetchall()]
|
||||||
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
||||||
@@ -143,7 +150,19 @@ def register_routes(app):
|
|||||||
in_range = False
|
in_range = False
|
||||||
ips = filtered_ips
|
ips = filtered_ips
|
||||||
available_ips_by_subnet[subnet['id']] = ips
|
available_ips_by_subnet[subnet['id']] = ips
|
||||||
return render_with_user('device.html', device={'id': device[0], 'name': device[1], 'description': device[2]}, subnets=subnets, device_ips=device_ips, available_ips_by_subnet=available_ips_by_subnet)
|
return render_with_user('device.html', device={'id': device[0], 'name': device[1], 'description': device[2], 'device_type_id': device[3]}, subnets=subnets, device_ips=device_ips, available_ips_by_subnet=available_ips_by_subnet, device_types=device_types)
|
||||||
|
|
||||||
|
@app.route('/update_device_type', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_device_type():
|
||||||
|
device_id = request.form['device_id']
|
||||||
|
device_type_id = request.form['device_type_id']
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('UPDATE Device SET device_type_id = %s WHERE id = %s', (device_type_id, device_id))
|
||||||
|
conn.commit()
|
||||||
|
return redirect(url_for('device', device_id=device_id))
|
||||||
|
|
||||||
@app.route('/device/<int:device_id>/add_ip', methods=['POST'])
|
@app.route('/device/<int:device_id>/add_ip', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -533,6 +552,22 @@ def register_routes(app):
|
|||||||
add_audit_log(user_id, action, details, subnet_id, conn=conn)
|
add_audit_log(user_id, action, details, subnet_id, conn=conn)
|
||||||
return render_with_user('dhcp.html', subnet={'id': subnet[0], 'name': subnet[1]}, dhcp_pool=dhcp_pool, error=error)
|
return render_with_user('dhcp.html', subnet={'id': subnet[0], 'name': subnet[1]}, dhcp_pool=dhcp_pool, error=error)
|
||||||
|
|
||||||
|
@app.route('/device_type_stats')
|
||||||
|
@login_required
|
||||||
|
def device_type_stats():
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT DeviceType.name, DeviceType.icon_class, COUNT(Device.id) as count
|
||||||
|
FROM DeviceType
|
||||||
|
LEFT JOIN Device ON Device.device_type_id = DeviceType.id
|
||||||
|
GROUP BY DeviceType.id, DeviceType.name, DeviceType.icon_class
|
||||||
|
ORDER BY DeviceType.name
|
||||||
|
''')
|
||||||
|
stats = cursor.fetchall()
|
||||||
|
return render_with_user('device_type_stats.html', stats=stats)
|
||||||
|
|
||||||
def get_current_user_name():
|
def get_current_user_name():
|
||||||
user_id = session.get('user_id')
|
user_id = session.get('user_id')
|
||||||
if not user_id:
|
if not user_id:
|
||||||
@@ -569,3 +604,4 @@ def register_routes(app):
|
|||||||
app.add_url_rule('/update_device_description', 'update_device_description', update_device_description, methods=['POST'])
|
app.add_url_rule('/update_device_description', 'update_device_description', update_device_description, methods=['POST'])
|
||||||
app.add_url_rule('/subnet/<int:subnet_id>/export_csv', 'export_subnet_csv', export_subnet_csv)
|
app.add_url_rule('/subnet/<int:subnet_id>/export_csv', 'export_subnet_csv', export_subnet_csv)
|
||||||
app.add_url_rule('/subnet/<int:subnet_id>/dhcp', 'dhcp_pool', dhcp_pool, methods=['GET', 'POST'])
|
app.add_url_rule('/subnet/<int:subnet_id>/dhcp', 'dhcp_pool', dhcp_pool, methods=['GET', 'POST'])
|
||||||
|
app.add_url_rule('/device_type_stats', 'device_type_stats', device_type_stats)
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<form action="/add_device" method="POST" class="flex flex-col space-y-4">
|
<form action="/add_device" method="POST" class="flex flex-col space-y-4">
|
||||||
<input type="text" name="device_name" placeholder="Device Name" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
<input type="text" name="device_name" placeholder="Device Name" class="border p-3 rounded-lg bg-gray-800 text-gray-100 border-gray-600" required>
|
||||||
|
<label for="device_type" class="block mb-2">Device Type</label>
|
||||||
|
<select id="device_type" name="device_type" class="border p-2 rounded w-full mb-4 bg-gray-800 text-gray-100 border-gray-600" required>
|
||||||
|
{% for dtype in device_types %}
|
||||||
|
<option value="{{ dtype[0] }}">{{ dtype[1] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Add Device</button>
|
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white px-4 py-2 rounded-lg">Add Device</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,14 @@
|
|||||||
<div class="flex items-center mb-8 relative justify-between gap-4">
|
<div class="flex items-center mb-8 relative justify-between gap-4">
|
||||||
<a href="javascript:window.history.back()" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-11 h-11 shrink-0"><i class="fas fa-arrow-left"></i></a>
|
<a href="javascript:window.history.back()" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white flex items-center justify-center rounded-full w-11 h-11 shrink-0"><i class="fas fa-arrow-left"></i></a>
|
||||||
<h1 class="text-3xl font-bold text-center flex-1 min-w-0 truncate">{{ device.name }}</h1>
|
<h1 class="text-3xl font-bold text-center flex-1 min-w-0 truncate">{{ device.name }}</h1>
|
||||||
|
<form action="/update_device_type" method="POST" class="hidden md:inline ml-2">
|
||||||
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
|
<select name="device_type_id" class="border p-2 rounded bg-gray-800 text-gray-100 border-gray-600" onchange="this.form.submit()">
|
||||||
|
{% for dtype in device_types %}
|
||||||
|
<option value="{{ dtype[0] }}" {% if device.device_type_id == dtype[0] %}selected{% endif %}>{{ dtype[1] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
<div class="flex items-center shrink-0">
|
<div class="flex items-center shrink-0">
|
||||||
<form action="/rename_device" method="POST" class="inline">
|
<form action="/rename_device" method="POST" class="inline">
|
||||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Device Type Statistics</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/logo.png">
|
||||||
|
<link href="/static/css/output.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col">
|
||||||
|
{% include 'header.html' %}
|
||||||
|
<div class="flex-1 flex items-center justify-center mx-4">
|
||||||
|
<div class="container py-8 max-w-2xl pt-20">
|
||||||
|
<h1 class="text-3xl font-bold mb-8 text-center">Device Type Statistics</h1>
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-md p-6">
|
||||||
|
<table class="w-full table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-700">
|
||||||
|
<th class="px-4 py-2 text-left">Type</th>
|
||||||
|
<th class="px-4 py-2 text-center">Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for name, icon_class, count in stats %}
|
||||||
|
<tr class="border-b border-gray-700">
|
||||||
|
<td class="px-4 py-3 flex items-center gap-2"><i class="fas {{ icon_class }} text-blue-300"></i> {{ name }}</td>
|
||||||
|
<td class="px-4 py-3 text-center font-bold">{{ count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
{% for device in devices %}
|
{% for device in devices %}
|
||||||
<li class="my-2">
|
<li class="my-2">
|
||||||
<a href="/device/{{ device.id }}" class="flex items-center justify-between bg-gray-900 hover:bg-gray-700 text-gray-200 hover:text-white font-semibold rounded-lg px-4 py-2 shadow transition-colors duration-150">
|
<a href="/device/{{ device.id }}" class="flex items-center justify-between bg-gray-900 hover:bg-gray-700 text-gray-200 hover:text-white font-semibold rounded-lg px-4 py-2 shadow transition-colors duration-150">
|
||||||
<span><i class="fas fa-server mr-2"></i>{{ device.name }}</span>
|
<span><i class="fas {{ device.icon_class or 'fa-server' }} mr-2"></i>{{ device.name }}</span>
|
||||||
{% set ips = device_ips.get(device.id, []) %}
|
{% set ips = device_ips.get(device.id, []) %}
|
||||||
<span class="flex flex-row flex-wrap justify-end items-center ml-4 text-xs text-blue-300 font-normal align-middle">
|
<span class="flex flex-row flex-wrap justify-end items-center ml-4 text-xs text-blue-300 font-normal align-middle">
|
||||||
{% if ips|length > 0 %}
|
{% if ips|length > 0 %}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<a href="/devices" class="text-gray-200 hover:text-blue-400 font-medium">Devices</a>
|
<a href="/devices" class="text-gray-200 hover:text-blue-400 font-medium">Devices</a>
|
||||||
<a href="/admin" class="text-gray-200 hover:text-blue-400 font-medium">Admin</a>
|
<a href="/admin" class="text-gray-200 hover:text-blue-400 font-medium">Admin</a>
|
||||||
<a href="/audit" class="text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
|
<a href="/audit" class="text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
|
||||||
|
<a href="/device_type_stats" class="text-gray-200 hover:text-blue-400 font-medium">Stats</a>
|
||||||
{% if current_user_name %}
|
{% if current_user_name %}
|
||||||
<a href="/logout" class="text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
<a href="/logout" class="text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
<a href="/devices" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Devices</a>
|
<a href="/devices" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Devices</a>
|
||||||
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Admin</a>
|
<a href="/admin" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Admin</a>
|
||||||
<a href="/audit" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
|
<a href="/audit" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Audit Log</a>
|
||||||
|
<a href="/device_type_stats" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Stats</a>
|
||||||
{% if current_user_name %}
|
{% if current_user_name %}
|
||||||
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user