Added rack stuff
This commit is contained in:
@@ -107,6 +107,25 @@ def init_db(app=None):
|
||||
FOREIGN KEY (subnet_id) REFERENCES Subnet(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS Rack (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
site VARCHAR(255) NOT NULL,
|
||||
height_u INTEGER NOT NULL
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS RackDevice (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
rack_id INTEGER NOT NULL,
|
||||
device_id INTEGER NOT NULL,
|
||||
position_u INTEGER NOT NULL,
|
||||
side ENUM('front', 'back') NOT NULL,
|
||||
FOREIGN KEY (rack_id) REFERENCES Rack(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (device_id) REFERENCES Device(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
cursor.execute('SELECT COUNT(*) FROM DeviceType')
|
||||
if cursor.fetchone()[0] == 0:
|
||||
cursor.executemany('INSERT INTO DeviceType (name, icon_class) VALUES (%s, %s)', [
|
||||
@@ -133,5 +152,8 @@ def init_db(app=None):
|
||||
if cursor.fetchone()[0] == 0:
|
||||
cursor.execute('''INSERT INTO User (name, email, password) VALUES (%s, %s, %s)''',
|
||||
('Jamie Banks', 'jamie@jdbnet.co.uk', hash_password('Drippy-Cavity-Jawline')))
|
||||
cursor.execute("SHOW COLUMNS FROM Device LIKE 'rack_only'")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute('ALTER TABLE Device ADD COLUMN rack_only BOOLEAN DEFAULT 0')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -78,7 +78,7 @@ def register_routes(app):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''SELECT Device.id, Device.name, DeviceType.icon_class FROM Device LEFT JOIN DeviceType ON Device.device_type_id = DeviceType.id''')
|
||||
cursor.execute('''SELECT Device.id, Device.name, DeviceType.icon_class FROM Device LEFT JOIN DeviceType ON Device.device_type_id = DeviceType.id WHERE Device.rack_only IS NULL OR Device.rack_only=0''')
|
||||
devices = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet')
|
||||
subnets = cursor.fetchall()
|
||||
@@ -610,6 +610,261 @@ def register_routes(app):
|
||||
site_devices[site].append({'id': device_id, 'name': name, 'description': description})
|
||||
return render_with_user('devices_by_type.html', device_type=device_type, icon_class=icon_class, site_devices=site_devices)
|
||||
|
||||
@app.route('/racks')
|
||||
@login_required
|
||||
def racks():
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM Rack')
|
||||
racks = cursor.fetchall()
|
||||
return render_with_user('racks.html', racks=racks)
|
||||
|
||||
@app.route('/rack/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_rack():
|
||||
from flask import current_app
|
||||
if request.method == 'POST':
|
||||
name = request.form['name']
|
||||
site = request.form['site']
|
||||
height_u = int(request.form['height_u'])
|
||||
user_id = session.get('user_id')
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO Rack (name, site, height_u) VALUES (%s, %s, %s)', (name, site, height_u))
|
||||
rack_id = cursor.lastrowid
|
||||
add_audit_log(user_id, 'add_rack', f"Added rack '{name}' at site '{site}' ({height_u}U)", conn=conn)
|
||||
conn.commit()
|
||||
return redirect(url_for('racks'))
|
||||
return render_with_user('add_rack.html')
|
||||
|
||||
@app.route('/rack/<int:rack_id>')
|
||||
@login_required
|
||||
def rack(rack_id):
|
||||
from flask import current_app, request
|
||||
side = request.args.get('side', 'front')
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack = cursor.fetchone()
|
||||
if not rack:
|
||||
return 'Rack not found', 404
|
||||
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
|
||||
rack_devices = cursor.fetchall()
|
||||
device_ids = [rd['device_id'] for rd in rack_devices]
|
||||
device_names = {}
|
||||
if device_ids:
|
||||
format_strings = ','.join(['%s'] * len(device_ids))
|
||||
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
|
||||
for row in cursor.fetchall():
|
||||
device_names[row['id']] = row['name']
|
||||
for rd in rack_devices:
|
||||
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
|
||||
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6)')
|
||||
all_devices = cursor.fetchall()
|
||||
assigned = set((rd['device_id'], rd['position_u'], rd['side']) for rd in rack_devices)
|
||||
site_devices = []
|
||||
for d in all_devices:
|
||||
site_devices.append(d)
|
||||
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side)
|
||||
|
||||
@app.route('/rack/<int:rack_id>/add_device', methods=['POST'])
|
||||
@login_required
|
||||
def rack_add_device(rack_id):
|
||||
device_id = int(request.form['device_id'])
|
||||
position_u = int(request.form['position_u'])
|
||||
side = request.form['side']
|
||||
user_id = session.get('user_id')
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack = cursor.fetchone()
|
||||
if not rack:
|
||||
return 'Rack not found', 404
|
||||
if position_u < 1 or position_u > rack['height_u']:
|
||||
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
|
||||
rack_devices = cursor.fetchall()
|
||||
device_ids = [rd['device_id'] for rd in rack_devices]
|
||||
device_names = {}
|
||||
if device_ids:
|
||||
format_strings = ','.join(['%s'] * len(device_ids))
|
||||
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
|
||||
for row in cursor.fetchall():
|
||||
device_names[row['id']] = row['name']
|
||||
for rd in rack_devices:
|
||||
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
|
||||
cursor.execute('SELECT id, name, device_type_id FROM Device')
|
||||
all_devices = cursor.fetchall()
|
||||
site_devices = [d for d in all_devices if d['device_type_id'] not in (2, 6)]
|
||||
error = f"Invalid U position: {position_u}. Rack is {rack['height_u']}U tall."
|
||||
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side, error=error)
|
||||
cursor.execute('SELECT COUNT(*) FROM RackDevice WHERE rack_id = %s AND position_u = %s AND side = %s', (rack_id, position_u, side))
|
||||
if cursor.fetchone()['COUNT(*)'] > 0:
|
||||
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
|
||||
rack_devices = cursor.fetchall()
|
||||
device_ids = [rd['device_id'] for rd in rack_devices]
|
||||
device_names = {}
|
||||
if device_ids:
|
||||
format_strings = ','.join(['%s'] * len(device_ids))
|
||||
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
|
||||
for row in cursor.fetchall():
|
||||
device_names[row['id']] = row['name']
|
||||
for rd in rack_devices:
|
||||
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
|
||||
cursor.execute('SELECT id, name, device_type_id FROM Device')
|
||||
all_devices = cursor.fetchall()
|
||||
site_devices = [d for d in all_devices if d['device_type_id'] not in (2, 6)]
|
||||
error = f"U{position_u} on the {side} is already occupied."
|
||||
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side, error=error)
|
||||
cursor.execute('INSERT INTO RackDevice (rack_id, device_id, position_u, side) VALUES (%s, %s, %s, %s)', (rack_id, device_id, position_u, side))
|
||||
cursor2 = conn.cursor()
|
||||
cursor2.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
||||
device_name = cursor2.fetchone()
|
||||
cursor2.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack_name = cursor2.fetchone()
|
||||
add_audit_log(user_id, 'rack_add_device', f"Assigned device '{device_name[0] if device_name else device_id}' to rack '{rack_name[0] if rack_name else rack_id}' U{position_u} ({side})", conn=conn)
|
||||
conn.commit()
|
||||
return redirect(url_for('rack', rack_id=rack_id))
|
||||
|
||||
@app.route('/rack/<int:rack_id>/add_nonnet_device', methods=['POST'])
|
||||
@login_required
|
||||
def rack_add_nonnet_device(rack_id):
|
||||
device_name = request.form['device_name']
|
||||
position_u = int(request.form['position_u'])
|
||||
side = request.form['side']
|
||||
user_id = session.get('user_id')
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack = cursor.fetchone()
|
||||
if not rack:
|
||||
return 'Rack not found', 404
|
||||
if position_u < 1 or position_u > rack['height_u']:
|
||||
error = f"Invalid U position: {position_u}. Rack is {rack['height_u']}U tall."
|
||||
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
|
||||
rack_devices = cursor.fetchall()
|
||||
device_ids = [rd['device_id'] for rd in rack_devices]
|
||||
device_names = {}
|
||||
if device_ids:
|
||||
format_strings = ','.join(['%s'] * len(device_ids))
|
||||
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
|
||||
for row in cursor.fetchall():
|
||||
device_names[row['id']] = row['name']
|
||||
for rd in rack_devices:
|
||||
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
|
||||
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6) AND (rack_only IS NULL OR rack_only=0)')
|
||||
site_devices = cursor.fetchall()
|
||||
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side, error=error)
|
||||
cursor.execute('SELECT COUNT(*) FROM RackDevice WHERE rack_id = %s AND position_u = %s AND side = %s', (rack_id, position_u, side))
|
||||
if cursor.fetchone()['COUNT(*)'] > 0:
|
||||
error = f"U{position_u} on the {side} is already occupied."
|
||||
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
|
||||
rack_devices = cursor.fetchall()
|
||||
device_ids = [rd['device_id'] for rd in rack_devices]
|
||||
device_names = {}
|
||||
if device_ids:
|
||||
format_strings = ','.join(['%s'] * len(device_ids))
|
||||
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
|
||||
for row in cursor.fetchall():
|
||||
device_names[row['id']] = row['name']
|
||||
for rd in rack_devices:
|
||||
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
|
||||
cursor.execute('SELECT id, name, device_type_id FROM Device WHERE device_type_id NOT IN (2, 6) AND (rack_only IS NULL OR rack_only=0)')
|
||||
site_devices = cursor.fetchall()
|
||||
return render_with_user('rack.html', rack=rack, rack_devices=rack_devices, site_devices=site_devices, current_side=side, error=error)
|
||||
cursor.execute('INSERT INTO Device (name, device_type_id, rack_only) VALUES (%s, %s, %s)', (device_name, 1, 1))
|
||||
device_id = cursor.lastrowid
|
||||
cursor.execute('INSERT INTO RackDevice (rack_id, device_id, position_u, side) VALUES (%s, %s, %s, %s)', (rack_id, device_id, position_u, side))
|
||||
add_audit_log(user_id, 'rack_add_nonnet_device', f"Added non-networked device '{device_name}' to rack '{rack_id}' U{position_u} ({side})", conn=conn)
|
||||
conn.commit()
|
||||
return redirect(url_for('rack', rack_id=rack_id))
|
||||
|
||||
@app.route('/rack/<int:rack_id>/remove_device', methods=['POST'])
|
||||
@login_required
|
||||
def rack_remove_device(rack_id):
|
||||
rack_device_id = int(request.form['rack_device_id'])
|
||||
user_id = session.get('user_id')
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT device_id, position_u, side FROM RackDevice WHERE id = %s', (rack_device_id,))
|
||||
rd = cursor.fetchone()
|
||||
cursor.execute('SELECT name FROM Device WHERE id = %s', (rd['device_id'],))
|
||||
device_name_row = cursor.fetchone()
|
||||
device_label = device_name_row['name'] if device_name_row and 'name' in device_name_row else rd['device_id']
|
||||
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack_name_row = cursor.fetchone()
|
||||
rack_label = rack_name_row['name'] if rack_name_row and 'name' in rack_name_row else rack_id
|
||||
add_audit_log(user_id, 'rack_remove_device', f"Removed device '{device_label}' from rack '{rack_label}' U{rd['position_u']} ({rd['side']})", conn=conn)
|
||||
cursor.execute('DELETE FROM RackDevice WHERE id = %s', (rack_device_id,))
|
||||
conn.commit()
|
||||
return redirect(url_for('rack', rack_id=rack_id))
|
||||
|
||||
@app.route('/rack/<int:rack_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_rack(rack_id):
|
||||
user_id = session.get('user_id')
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack_name = cursor.fetchone()
|
||||
cursor.execute('DELETE FROM Rack WHERE id = %s', (rack_id,))
|
||||
add_audit_log(user_id, 'delete_rack', f"Deleted rack '{rack_name[0] if rack_name else rack_id}'", conn=conn)
|
||||
conn.commit()
|
||||
return redirect(url_for('racks'))
|
||||
|
||||
@app.route('/rack/<int:rack_id>/export_csv')
|
||||
@login_required
|
||||
def export_rack_csv(rack_id):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
||||
rack = cursor.fetchone()
|
||||
if not rack:
|
||||
return 'Rack not found', 404
|
||||
cursor.execute('SELECT * FROM RackDevice WHERE rack_id = %s', (rack_id,))
|
||||
rack_devices = cursor.fetchall()
|
||||
device_ids = [rd['device_id'] for rd in rack_devices]
|
||||
device_names = {}
|
||||
if device_ids:
|
||||
format_strings = ','.join(['%s'] * len(device_ids))
|
||||
cursor.execute(f'SELECT id, name FROM Device WHERE id IN ({format_strings})', tuple(device_ids))
|
||||
for row in cursor.fetchall():
|
||||
device_names[row['id']] = row['name']
|
||||
for rd in rack_devices:
|
||||
rd['device_name'] = device_names.get(rd['device_id'], 'Unknown')
|
||||
output = StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([f"Rack: {rack['name']} ({rack['height_u']}U, {rack['site']})"])
|
||||
writer.writerow([])
|
||||
for side in ['front', 'back']:
|
||||
writer.writerow([side.capitalize()])
|
||||
writer.writerow(['U', 'Device'])
|
||||
for u in range(rack['height_u'], 0, -1):
|
||||
found = False
|
||||
for rd in rack_devices:
|
||||
if rd['position_u'] == u and rd['side'] == side:
|
||||
writer.writerow([u, rd['device_name']])
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
writer.writerow([u, ''])
|
||||
writer.writerow([])
|
||||
csv_bytes = output.getvalue().encode('utf-8')
|
||||
output_bytes = BytesIO(csv_bytes)
|
||||
output_bytes.seek(0)
|
||||
filename = f"{rack['name']}_rack.csv".replace(' ', '_')
|
||||
return send_file(
|
||||
output_bytes,
|
||||
mimetype='text/csv',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
def get_current_user_name():
|
||||
user_id = session.get('user_id')
|
||||
if not user_id:
|
||||
@@ -647,3 +902,12 @@ def register_routes(app):
|
||||
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('/device_type_stats', 'device_type_stats', device_type_stats)
|
||||
app.add_url_rule('/devices/type/<device_type>', 'devices_by_type', devices_by_type)
|
||||
app.add_url_rule('/racks', 'racks', racks)
|
||||
app.add_url_rule('/rack/add', 'add_rack', add_rack, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/rack/<int:rack_id>', 'rack', rack)
|
||||
app.add_url_rule('/rack/<int:rack_id>/add_device', 'rack_add_device', rack_add_device, methods=['POST'])
|
||||
app.add_url_rule('/rack/<int:rack_id>/add_nonnet_device', 'rack_add_nonnet_device', rack_add_nonnet_device, methods=['POST'])
|
||||
app.add_url_rule('/rack/<int:rack_id>/remove_device', 'rack_remove_device', rack_remove_device, methods=['POST'])
|
||||
app.add_url_rule('/rack/<int:rack_id>/delete', 'delete_rack', delete_rack, methods=['POST'])
|
||||
app.add_url_rule('/rack/<int:rack_id>/export_csv', 'export_rack_csv', export_rack_csv)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Add Rack</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-md pt-20">
|
||||
<h1 class="text-3xl font-bold mb-8 text-center">Add Rack</h1>
|
||||
<form action="/rack/add" method="POST" class="space-y-6 bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<div>
|
||||
<label for="name" class="block font-medium mb-1">Rack Name</label>
|
||||
<input type="text" id="name" name="name" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600 w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="site" class="block font-medium mb-1">Site</label>
|
||||
<input type="text" id="site" name="site" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600 w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="height_u" class="block font-medium mb-1">Height (U)</label>
|
||||
<input type="number" id="height_u" name="height_u" min="1" max="60" class="border p-3 rounded-lg bg-gray-900 text-gray-100 border-gray-600 w-full" required>
|
||||
</div>
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg w-full">Add Rack</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,7 @@
|
||||
<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="/device_type_stats" class="text-gray-200 hover:text-blue-400 font-medium">Stats</a>
|
||||
<a href="/racks" class="text-gray-200 hover:text-blue-400 font-medium">Racks</a>
|
||||
{% if current_user_name %}
|
||||
<a href="/logout" class="text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
||||
{% endif %}
|
||||
@@ -27,6 +28,7 @@
|
||||
<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="/device_type_stats" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Stats</a>
|
||||
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Racks</a>
|
||||
{% if current_user_name %}
|
||||
<a href="/logout" class="block px-6 py-2 text-gray-200 hover:text-blue-400 font-medium">Logout</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ rack.name }} - Rack View</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">
|
||||
<div class="flex items-center mb-6 relative min-h-[3.5rem]">
|
||||
<a href="/racks" class="hidden sm:flex absolute left-0 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white items-center justify-center rounded-full w-11 h-11"><i class="fas fa-arrow-left"></i></a>
|
||||
<h1 class="text-3xl font-bold text-center w-full flex items-center justify-center mb-0">{{ rack.name }} <span class="text-base text-gray-400 ml-2">({{ rack.height_u }}U, {{ rack.site }})</span></h1>
|
||||
<form action="/rack/{{ rack.id }}/delete" method="POST" onsubmit="return confirm('Delete this rack?');" class="hidden sm:flex absolute right-0 mr-14">
|
||||
<button type="submit" class="bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white items-center justify-center rounded-full w-11 h-11 flex" title="Delete Rack">
|
||||
<i class="fas fa-times fa-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" id="export-csv" class="hidden sm:flex absolute right-0 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-500 text-white items-center justify-center rounded-full w-11 h-11 export-csv-btn" title="Export as CSV" data-rack-id="{{ rack.id }}">
|
||||
<i class="fas fa-file-csv fa-lg"></i>
|
||||
</button>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btn = document.getElementById('export-csv');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
window.location = '/rack/' + btn.getAttribute('data-rack-id') + '/export_csv';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div class="flex justify-center gap-4 mb-6">
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4 mb-6 items-stretch">
|
||||
<div class="flex gap-4">
|
||||
<a href="?side=front" id="front-btn" class="rack-side-btn px-4 py-2 rounded-lg text-white font-semibold bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 {% if current_side == 'front' %}ring-2 ring-blue-400{% endif %}">Front</a>
|
||||
<a href="?side=back" id="back-btn" class="rack-side-btn px-4 py-2 rounded-lg text-white font-semibold bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 {% if current_side == 'back' %}ring-2 ring-blue-400{% endif %}">Back</a>
|
||||
</div>
|
||||
<button id="show-nonnet-form" type="button" class="w-full md:w-auto bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg mb-2 md:mb-0 md:ml-auto flex-shrink-0"><i class="fas fa-plus"></i> Add Non-Networked Device</button>
|
||||
</div>
|
||||
<form action="/rack/{{ rack.id }}/add_device" method="POST" class="mb-6 bg-gray-800 rounded-lg p-4">
|
||||
<div class="flex flex-col md:flex-row gap-2 mb-2">
|
||||
<select name="device_id" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:flex-1" required>
|
||||
<option value="" disabled selected>Select Device...</option>
|
||||
{% for device in site_devices %}
|
||||
{% if device.device_type_id != 2 and device.device_type_id != 6 %}
|
||||
<option value="{{ device.id }}">{{ device.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="number" name="position_u" min="1" max="{{ rack.height_u }}" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-24" placeholder="U" required>
|
||||
<select name="side" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-28" required>
|
||||
<option value="front">Front</option>
|
||||
<option value="back">Back</option>
|
||||
</select>
|
||||
<button type="submit" class="w-full md:w-auto bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">Add Device</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">To add a multi-U device, repeat for each U position.</div>
|
||||
</form>
|
||||
<form id="nonnet-form" action="/rack/{{ rack.id }}/add_nonnet_device" method="POST" class="hidden flex flex-col gap-2 mt-2 mb-6 bg-gray-800 rounded-lg p-4">
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<input type="text" name="device_name" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:flex-1" placeholder="Device Name" required>
|
||||
<input type="number" name="position_u" min="1" max="{{ rack.height_u }}" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-24" placeholder="U" required>
|
||||
<select name="side" class="border p-2 rounded bg-gray-900 text-gray-100 border-gray-600 w-full md:w-28" required>
|
||||
<option value="front">Front</option>
|
||||
<option value="back">Back</option>
|
||||
</select>
|
||||
<button type="submit" class="w-full md:w-auto bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">Add</button>
|
||||
<button id="hide-nonnet-form" type="button" class="w-full md:w-auto bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg md:ml-0.5 mt-2 md:mt-0 flex-shrink-0"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-2">Add a non-networked device.</div>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('show-nonnet-form').onclick = function() {
|
||||
document.getElementById('nonnet-form').classList.remove('hidden');
|
||||
this.classList.add('hidden');
|
||||
};
|
||||
document.getElementById('hide-nonnet-form').onclick = function() {
|
||||
document.getElementById('nonnet-form').classList.add('hidden');
|
||||
document.getElementById('show-nonnet-form').classList.remove('hidden');
|
||||
};
|
||||
</script>
|
||||
{% if error %}
|
||||
<div class="mb-4 p-3 bg-red-700 text-white rounded-lg text-center font-semibold">{{ error }}</div>
|
||||
{% endif %}
|
||||
<div id="rack-visual" class="bg-gray-800 rounded-lg shadow-md p-4">
|
||||
<h2 class="text-xl font-bold mb-2 text-blue-200" id="rack-side-label">{{ current_side|capitalize }}</h2>
|
||||
<div id="rack-units">
|
||||
{% for u in range(rack.height_u, 0, -1) %}
|
||||
<div class="flex items-center h-10 border-b border-gray-700 hover:bg-gray-700/30 transition group">
|
||||
<span class="w-16 text-right text-gray-400 text-base font-mono pr-4">U{{ u }}</span>
|
||||
<span class="flex-1 ml-4 flex items-center min-h-8">
|
||||
{% set found = false %}
|
||||
{% for rd in rack_devices %}
|
||||
{% if rd.position_u == u and rd.side == current_side %}
|
||||
<a href="/device/{{ rd.device_id }}" class="text-blue-400 hover:underline text-base">{{ rd.device_name }}</a>
|
||||
<form action="/rack/{{ rack.id }}/remove_device" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to remove this device from the rack?');">
|
||||
<input type="hidden" name="rack_device_id" value="{{ rd.id }}">
|
||||
<button type="submit" class="ml-3 text-red-400 hover:text-red-600"><i class="fas fa-times"></i></button>
|
||||
</form>
|
||||
{% set found = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Racks</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-3xl pt-20">
|
||||
<h1 class="text-3xl font-bold mb-8 text-center">Racks</h1>
|
||||
<div class="flex justify-center mb-6">
|
||||
<a href="/rack/add" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg inline-block"><i class="fas fa-plus"></i> Add Rack</a>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
{% for rack in racks %}
|
||||
<div class="bg-gray-800 rounded-lg shadow-md p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-blue-200 mb-1">{{ rack.name }}</h2>
|
||||
<div class="text-gray-400">Site: {{ rack.site }} | Height: {{ rack.height_u }}U</div>
|
||||
</div>
|
||||
<a href="/rack/{{ rack.id }}" class="bg-gradient-to-r from-gray-500 to-gray-700 hover:from-gray-600 hover:to-gray-800 text-white px-4 py-2 rounded-lg">View</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-gray-400">No racks defined yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user