feat: ✨ bulk operations
This commit is contained in:
@@ -1004,7 +1004,27 @@ def register_routes(app):
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''SELECT id, ip FROM IPAddress WHERE subnet_id = %s AND id NOT IN (SELECT ip_id FROM DeviceIPAddress) AND (hostname IS NULL OR hostname != 'DHCP')''', (subnet_id,))
|
||||
available_ips = [{'id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
||||
available_ips = cursor.fetchall()
|
||||
|
||||
# Filter out DHCP pool IPs
|
||||
cursor.execute('SELECT start_ip, end_ip, excluded_ips FROM DHCPPool WHERE subnet_id = %s', (subnet_id,))
|
||||
dhcp_row = cursor.fetchone()
|
||||
if dhcp_row:
|
||||
start_ip, end_ip, excluded_ips = dhcp_row
|
||||
excluded_list = [x for x in (excluded_ips or '').replace(' ', '').split(',') if x]
|
||||
in_range = False
|
||||
filtered_ips = []
|
||||
for ip_obj in available_ips:
|
||||
ip = ip_obj[1]
|
||||
if ip == start_ip:
|
||||
in_range = True
|
||||
if ip in excluded_list or not (in_range and ip not in excluded_list):
|
||||
filtered_ips.append(ip_obj)
|
||||
if ip == end_ip:
|
||||
in_range = False
|
||||
available_ips = filtered_ips
|
||||
|
||||
available_ips = [{'id': row[0], 'ip': row[1]} for row in available_ips]
|
||||
return {'available_ips': available_ips}
|
||||
|
||||
@app.route('/rename_device', methods=['POST'])
|
||||
@@ -2667,6 +2687,228 @@ def register_routes(app):
|
||||
kwargs['current_user_name'] = get_current_user_name()
|
||||
return render_template(*args, **kwargs)
|
||||
|
||||
# Bulk Operations
|
||||
@app.route('/bulk', methods=['GET'])
|
||||
@permission_required('view_devices')
|
||||
def bulk_operations():
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name FROM Device ORDER BY name')
|
||||
devices = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
||||
subnets = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name FROM Tag ORDER BY name')
|
||||
tags = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name FROM DeviceType ORDER BY name')
|
||||
device_types = cursor.fetchall()
|
||||
return render_with_user('bulk_operations.html',
|
||||
devices=devices,
|
||||
subnets=subnets,
|
||||
tags=tags,
|
||||
device_types=device_types,
|
||||
can_add_device_ip=has_permission('add_device_ip'),
|
||||
can_add_device=has_permission('add_device'),
|
||||
can_assign_device_tag=has_permission('assign_device_tag'),
|
||||
can_export_subnet_csv=has_permission('export_subnet_csv'))
|
||||
|
||||
@app.route('/bulk/assign_ips', methods=['POST'])
|
||||
@permission_required('add_device_ip')
|
||||
def bulk_assign_ips():
|
||||
device_id = request.form['device_id']
|
||||
ip_ids = request.form.getlist('ip_ids[]')
|
||||
user_name = get_current_user_name()
|
||||
results = {'success': [], 'failed': []}
|
||||
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
if not device:
|
||||
return jsonify({'success': [], 'failed': [{'ip_id': 'all', 'reason': 'Device not found'}]})
|
||||
|
||||
device_name = device[0]
|
||||
|
||||
for ip_id in ip_ids:
|
||||
try:
|
||||
cursor.execute('SELECT ip, subnet_id FROM IPAddress WHERE id = %s', (ip_id,))
|
||||
ip_row = cursor.fetchone()
|
||||
if not ip_row:
|
||||
results['failed'].append({'ip_id': ip_id, 'reason': 'IP not found'})
|
||||
continue
|
||||
|
||||
ip, subnet_id = ip_row[0], ip_row[1]
|
||||
|
||||
# Check if IP is already assigned
|
||||
cursor.execute('SELECT id FROM DeviceIPAddress WHERE ip_id = %s', (ip_id,))
|
||||
if cursor.fetchone():
|
||||
results['failed'].append({'ip_id': ip_id, 'ip': ip, 'reason': 'IP already assigned'})
|
||||
continue
|
||||
|
||||
# Check if IP is in DHCP pool (using exact same logic as device_add_ip)
|
||||
cursor.execute('SELECT start_ip, end_ip, excluded_ips FROM DHCPPool WHERE subnet_id = %s', (subnet_id,))
|
||||
dhcp_row = cursor.fetchone()
|
||||
if dhcp_row:
|
||||
start_ip, end_ip, excluded_ips = dhcp_row
|
||||
excluded_list = [x for x in (excluded_ips or '').replace(' ', '').split(',') if x]
|
||||
if ip not in excluded_list:
|
||||
cursor.execute('SELECT ip FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
|
||||
all_ips = [row[0] for row in cursor.fetchall()]
|
||||
in_range = False
|
||||
reserved_for_dhcp = False
|
||||
for candidate_ip in all_ips:
|
||||
if candidate_ip == start_ip:
|
||||
in_range = True
|
||||
if in_range and candidate_ip == ip:
|
||||
reserved_for_dhcp = True
|
||||
break
|
||||
if candidate_ip == end_ip:
|
||||
in_range = False
|
||||
if reserved_for_dhcp:
|
||||
results['failed'].append({'ip_id': ip_id, 'ip': ip, 'reason': 'IP is reserved for DHCP'})
|
||||
continue
|
||||
|
||||
cursor.execute('INSERT INTO DeviceIPAddress (device_id, ip_id) VALUES (%s, %s)', (device_id, ip_id))
|
||||
cursor.execute('UPDATE IPAddress SET hostname = %s WHERE id = %s', (device_name, ip_id))
|
||||
cursor.execute('SELECT name, cidr FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
subnet_name, subnet_cidr = subnet[0], subnet[1]
|
||||
add_audit_log(session['user_id'], 'add_device_ip',
|
||||
f"Assigned IP {ip} ({subnet_name} {subnet_cidr}) to device {device_name}",
|
||||
subnet_id, conn=conn)
|
||||
results['success'].append({'ip_id': ip_id, 'ip': ip})
|
||||
except Exception as e:
|
||||
results['failed'].append({'ip_id': ip_id, 'reason': str(e)})
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify(results)
|
||||
|
||||
@app.route('/bulk/create_devices', methods=['POST'])
|
||||
@permission_required('add_device')
|
||||
def bulk_create_devices():
|
||||
device_names = request.form.get('device_names', '').strip().split('\n')
|
||||
device_type_id = int(request.form.get('device_type', 1))
|
||||
user_name = get_current_user_name()
|
||||
results = {'success': [], 'failed': []}
|
||||
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
for name in device_names:
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
cursor.execute('INSERT INTO Device (name, device_type_id) VALUES (%s, %s)', (name, device_type_id))
|
||||
device_id = cursor.lastrowid
|
||||
add_audit_log(session['user_id'], 'add_device', f"Added device {name}", conn=conn)
|
||||
results['success'].append({'name': name, 'id': device_id})
|
||||
except Exception as e:
|
||||
results['failed'].append({'name': name, 'reason': str(e)})
|
||||
conn.commit()
|
||||
|
||||
logging.info(f"User {user_name} bulk created {len(results['success'])} devices.")
|
||||
return jsonify(results)
|
||||
|
||||
@app.route('/bulk/assign_tags', methods=['POST'])
|
||||
@permission_required('assign_device_tag')
|
||||
def bulk_assign_tags():
|
||||
device_ids = request.form.getlist('device_ids[]')
|
||||
tag_ids = request.form.getlist('tag_ids[]')
|
||||
user_name = get_current_user_name()
|
||||
results = {'success': [], 'failed': []}
|
||||
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
for device_id in device_ids:
|
||||
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
if not device:
|
||||
results['failed'].append({'device_id': device_id, 'reason': 'Device not found'})
|
||||
continue
|
||||
device_name = device[0]
|
||||
|
||||
for tag_id in tag_ids:
|
||||
try:
|
||||
cursor.execute('SELECT name FROM Tag WHERE id = %s', (tag_id,))
|
||||
tag = cursor.fetchone()
|
||||
if not tag:
|
||||
continue
|
||||
tag_name = tag[0]
|
||||
|
||||
cursor.execute('SELECT id FROM DeviceTag WHERE device_id = %s AND tag_id = %s', (device_id, tag_id))
|
||||
if cursor.fetchone():
|
||||
continue # Already assigned
|
||||
|
||||
cursor.execute('INSERT INTO DeviceTag (device_id, tag_id) VALUES (%s, %s)', (device_id, tag_id))
|
||||
add_audit_log(session['user_id'], 'assign_device_tag',
|
||||
f"Assigned tag '{tag_name}' to device '{device_name}'", conn=conn)
|
||||
results['success'].append({'device_id': device_id, 'device_name': device_name, 'tag_id': tag_id, 'tag_name': tag_name})
|
||||
except Exception as e:
|
||||
results['failed'].append({'device_id': device_id, 'tag_id': tag_id, 'reason': str(e)})
|
||||
conn.commit()
|
||||
|
||||
logging.info(f"User {user_name} bulk assigned tags to {len(device_ids)} devices.")
|
||||
return jsonify(results)
|
||||
|
||||
@app.route('/bulk/export_subnets', methods=['POST'])
|
||||
@permission_required('export_subnet_csv')
|
||||
def bulk_export_subnets():
|
||||
subnet_ids = request.form.getlist('subnet_ids[]')
|
||||
from flask import current_app
|
||||
output = StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
for subnet_id in subnet_ids:
|
||||
cursor.execute('SELECT id, name, cidr FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
if not subnet:
|
||||
continue
|
||||
|
||||
writer.writerow([f"Subnet: {subnet[1]} ({subnet[2]})"])
|
||||
writer.writerow(['IP Address', 'Hostname', 'Description'])
|
||||
|
||||
cursor.execute('SELECT * FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
|
||||
ip_addresses = cursor.fetchall()
|
||||
cursor.execute('SELECT id, name, description FROM Device')
|
||||
devices = cursor.fetchall()
|
||||
device_name_map = {name.lower(): (id, description) for id, name, description in devices}
|
||||
|
||||
for ip in ip_addresses:
|
||||
hostname = ip[2]
|
||||
device_description = None
|
||||
if hostname:
|
||||
match = device_name_map.get(hostname.lower())
|
||||
if match:
|
||||
device_description = match[1]
|
||||
writer.writerow([ip[1] or '', hostname or '', device_description or ''])
|
||||
|
||||
writer.writerow([]) # Empty row between subnets
|
||||
|
||||
output.seek(0)
|
||||
return send_file(BytesIO(output.getvalue().encode('utf-8')),
|
||||
mimetype='text/csv',
|
||||
as_attachment=True,
|
||||
download_name='bulk_subnet_export.csv')
|
||||
|
||||
# API key regeneration route
|
||||
@app.route('/regenerate_api_key', methods=['POST'])
|
||||
@permission_required('manage_users')
|
||||
def regenerate_api_key():
|
||||
user_id = request.form['user_id']
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
new_api_key = generate_api_key()
|
||||
cursor.execute('UPDATE User SET api_key = %s WHERE id = %s', (new_api_key, user_id))
|
||||
conn.commit()
|
||||
return redirect(url_for('users'))
|
||||
|
||||
app.add_url_rule('/login', 'login', login, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/logout', 'logout', logout)
|
||||
app.add_url_rule('/', 'index', index)
|
||||
@@ -2702,17 +2944,4 @@ def register_routes(app):
|
||||
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)
|
||||
app.add_url_rule('/help', 'help', help)
|
||||
|
||||
# API key regeneration route
|
||||
@app.route('/regenerate_api_key', methods=['POST'])
|
||||
@permission_required('manage_users')
|
||||
def regenerate_api_key():
|
||||
user_id = request.form['user_id']
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
new_api_key = generate_api_key()
|
||||
cursor.execute('UPDATE User SET api_key = %s WHERE id = %s', (new_api_key, user_id))
|
||||
conn.commit()
|
||||
return redirect(url_for('users'))
|
||||
app.add_url_rule('/help', 'help', help)
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="flex-1 flex items-center justify-center mx-4">
|
||||
<div class="container py-8 max-w-md pt-20">
|
||||
<div class="flex items-center mb-6 relative">
|
||||
<a href="javascript:window.history.back()" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 items-center justify-center rounded-full w-11 h-11"><i class="fas fa-arrow-left"></i></a>
|
||||
<a href="/devices" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 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">Add Device</h1>
|
||||
</div>
|
||||
<form action="/add_device" method="POST" class="flex flex-col space-y-4">
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bulk Operations</title>
|
||||
<link rel="icon" type="image/png" href="{{ 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-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
|
||||
{% include 'header.html' %}
|
||||
<div class="flex-1 mx-4 py-8 pt-20">
|
||||
<div class="container max-w-6xl mx-auto">
|
||||
<div class="flex items-center mb-6 relative">
|
||||
<a href="/devices" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer 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">Bulk Operations</h1>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex flex-wrap gap-2 mb-6 justify-center border-b border-gray-600">
|
||||
<button onclick="showTab('assign-ips')" id="tab-assign-ips" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer active">Bulk IP Assignment</button>
|
||||
<button onclick="showTab('create-devices')" id="tab-create-devices" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer">Bulk Device Creation</button>
|
||||
<button onclick="showTab('assign-tags')" id="tab-assign-tags" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer">Bulk Tag Assignment</button>
|
||||
<button onclick="showTab('export')" id="tab-export" class="tab-btn px-4 py-2 rounded-t-lg bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 hover:cursor-pointer">Bulk Export</button>
|
||||
</div>
|
||||
|
||||
<!-- Bulk IP Assignment -->
|
||||
<div id="panel-assign-ips" class="tab-panel bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||
{% if can_add_device_ip %}
|
||||
<h2 class="text-2xl font-bold mb-4">Bulk IP Assignment</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">Select a device and assign multiple IPs from a subnet. Hold Ctrl/Cmd to select multiple IPs.</p>
|
||||
<form id="bulk-assign-ips-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Select Device:</label>
|
||||
<select id="bulk-device-select" name="device_id" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
|
||||
<option value="">Select a device...</option>
|
||||
{% for device in devices %}
|
||||
<option value="{{ device[0] }}">{{ device[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Select Subnet:</label>
|
||||
<select id="bulk-subnet-select" name="subnet_id" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
|
||||
<option value="">Select a subnet...</option>
|
||||
{% for subnet in subnets %}
|
||||
<option value="{{ subnet[0] }}">{{ subnet[1] }} ({{ subnet[2] }}) - {{ subnet[3] or 'Unassigned' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Select IPs (hold Ctrl/Cmd to select multiple):</label>
|
||||
<select id="bulk-ip-select" name="ip_ids[]" multiple size="15" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
|
||||
<option value="" disabled>Select a subnet first...</option>
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">Selected: <span id="selected-ip-count">0</span> IPs</p>
|
||||
</div>
|
||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Assign IPs</button>
|
||||
</form>
|
||||
<div id="assign-ips-result" class="mt-4 hidden"></div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">You don't have permission to assign IPs to devices.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Bulk Device Creation -->
|
||||
<div id="panel-create-devices" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||
{% if can_add_device %}
|
||||
<h2 class="text-2xl font-bold mb-4">Bulk Device Creation</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">Create multiple devices at once. Enter one device name per line.</p>
|
||||
<form id="bulk-create-devices-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Device Names (one per line):</label>
|
||||
<textarea id="device-names" name="device_names" rows="10" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" placeholder="Device 1 Device 2 Device 3" required></textarea>
|
||||
<p class="text-sm text-gray-500 mt-1">Enter device names, one per line</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Device Type:</label>
|
||||
<select name="device_type" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
|
||||
{% for dtype in device_types %}
|
||||
<option value="{{ dtype[0] }}">{{ dtype[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Create Devices</button>
|
||||
</form>
|
||||
<div id="create-devices-result" class="mt-4 hidden"></div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">You don't have permission to create devices.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Bulk Tag Assignment -->
|
||||
<div id="panel-assign-tags" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||
{% if can_assign_device_tag %}
|
||||
<h2 class="text-2xl font-bold mb-4">Bulk Tag Assignment</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">Select multiple devices and assign one or more tags to them.</p>
|
||||
<form id="bulk-assign-tags-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Select Devices (hold Ctrl/Cmd to select multiple):</label>
|
||||
<select id="bulk-tag-device-select" name="device_ids[]" multiple size="10" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full">
|
||||
{% for device in devices %}
|
||||
<option value="{{ device[0] }}">{{ device[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">Selected: <span id="selected-tag-device-count">0</span> devices</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Select Tags (hold Ctrl/Cmd to select multiple):</label>
|
||||
<select name="tag_ids[]" multiple size="5" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
|
||||
{% for tag in tags %}
|
||||
<option value="{{ tag[0] }}">{{ tag[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Assign Tags</button>
|
||||
</form>
|
||||
<div id="assign-tags-result" class="mt-4 hidden"></div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">You don't have permission to assign tags to devices.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Bulk Export -->
|
||||
<div id="panel-export" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||
{% if can_export_subnet_csv %}
|
||||
<h2 class="text-2xl font-bold mb-4">Bulk Subnet Export</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">Select multiple subnets and export them to a single CSV file.</p>
|
||||
<form id="bulk-export-form" method="POST" action="/bulk/export_subnets" class="space-y-4">
|
||||
<div>
|
||||
<label class="block mb-2 font-medium">Select Subnets (hold Ctrl/Cmd to select multiple):</label>
|
||||
<select name="subnet_ids[]" multiple size="10" class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full" required>
|
||||
{% for subnet in subnets %}
|
||||
<option value="{{ subnet[0] }}">{{ subnet[1] }} ({{ subnet[2] }}) - {{ subnet[3] or 'Unassigned' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg">Export to CSV</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="text-gray-500">You don't have permission to export subnets.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(tabName) {
|
||||
// Hide all panels
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.add('hidden'));
|
||||
// Remove active class from all tabs
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
// Show selected panel
|
||||
document.getElementById('panel-' + tabName).classList.remove('hidden');
|
||||
// Add active class to selected tab
|
||||
document.getElementById('tab-' + tabName).classList.add('active');
|
||||
}
|
||||
|
||||
// Update selected IP count
|
||||
document.getElementById('bulk-ip-select')?.addEventListener('change', function() {
|
||||
document.getElementById('selected-ip-count').textContent = this.selectedOptions.length;
|
||||
});
|
||||
|
||||
document.getElementById('bulk-tag-device-select')?.addEventListener('change', function() {
|
||||
document.getElementById('selected-tag-device-count').textContent = this.selectedOptions.length;
|
||||
});
|
||||
|
||||
// Load available IPs when subnet changes
|
||||
document.getElementById('bulk-subnet-select')?.addEventListener('change', function() {
|
||||
const subnetId = this.value;
|
||||
const ipSelect = document.getElementById('bulk-ip-select');
|
||||
if (!subnetId) {
|
||||
ipSelect.innerHTML = '<option value="" disabled>Select a subnet first...</option>';
|
||||
document.getElementById('selected-ip-count').textContent = '0';
|
||||
return;
|
||||
}
|
||||
ipSelect.innerHTML = '<option value="" disabled>Loading...</option>';
|
||||
fetch(`/get_available_ips?subnet_id=${subnetId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
ipSelect.innerHTML = '';
|
||||
if (data.available_ips.length === 0) {
|
||||
ipSelect.innerHTML = '<option value="" disabled>No available IPs in this subnet</option>';
|
||||
} else {
|
||||
data.available_ips.forEach(ip => {
|
||||
const option = document.createElement('option');
|
||||
option.value = ip.id;
|
||||
option.textContent = ip.ip;
|
||||
ipSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
document.getElementById('selected-ip-count').textContent = '0';
|
||||
})
|
||||
.catch(() => {
|
||||
ipSelect.innerHTML = '<option value="" disabled>Error loading IPs</option>';
|
||||
});
|
||||
});
|
||||
|
||||
// Bulk IP Assignment
|
||||
document.getElementById('bulk-assign-ips-form')?.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const resultDiv = document.getElementById('assign-ips-result');
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.innerHTML = '<p class="text-blue-500">Processing...</p>';
|
||||
|
||||
fetch('/bulk/assign_ips', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let html = '<div class="space-y-2">';
|
||||
if (data.success.length > 0) {
|
||||
html += `<div class="text-green-600 dark:text-green-400"><strong>Successfully assigned ${data.success.length} IP(s):</strong><ul class="list-disc list-inside mt-2">`;
|
||||
data.success.forEach(item => {
|
||||
html += `<li>${item.ip}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (data.failed.length > 0) {
|
||||
html += `<div class="text-red-600 dark:text-red-400"><strong>Failed ${data.failed.length} assignment(s):</strong><ul class="list-disc list-inside mt-2">`;
|
||||
data.failed.forEach(item => {
|
||||
const ipDisplay = item.ip ? ` (${item.ip})` : '';
|
||||
html += `<li>IP ID ${item.ip_id}${ipDisplay}: ${item.reason}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
resultDiv.innerHTML = html;
|
||||
// Reload IP list if successful
|
||||
if (data.success.length > 0) {
|
||||
const subnetSelect = document.getElementById('bulk-subnet-select');
|
||||
if (subnetSelect.value) {
|
||||
subnetSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.innerHTML = `<p class="text-red-600">Error: ${error.message}</p>`;
|
||||
});
|
||||
});
|
||||
|
||||
// Bulk Device Creation
|
||||
document.getElementById('bulk-create-devices-form')?.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const resultDiv = document.getElementById('create-devices-result');
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.innerHTML = '<p class="text-blue-500">Processing...</p>';
|
||||
|
||||
fetch('/bulk/create_devices', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let html = '<div class="space-y-2">';
|
||||
if (data.success.length > 0) {
|
||||
html += `<div class="text-green-600 dark:text-green-400"><strong>Successfully created ${data.success.length} device(s):</strong><ul class="list-disc list-inside mt-2">`;
|
||||
data.success.forEach(item => {
|
||||
html += `<li>${item.name}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (data.failed.length > 0) {
|
||||
html += `<div class="text-red-600 dark:text-red-400"><strong>Failed ${data.failed.length} creation(s):</strong><ul class="list-disc list-inside mt-2">`;
|
||||
data.failed.forEach(item => {
|
||||
html += `<li>${item.name}: ${item.reason}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
resultDiv.innerHTML = html;
|
||||
if (data.success.length > 0) {
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.innerHTML = `<p class="text-red-600">Error: ${error.message}</p>`;
|
||||
});
|
||||
});
|
||||
|
||||
// Bulk Tag Assignment
|
||||
document.getElementById('bulk-assign-tags-form')?.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const resultDiv = document.getElementById('assign-tags-result');
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.innerHTML = '<p class="text-blue-500">Processing...</p>';
|
||||
|
||||
fetch('/bulk/assign_tags', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let html = '<div class="space-y-2">';
|
||||
if (data.success.length > 0) {
|
||||
html += `<div class="text-green-600 dark:text-green-400"><strong>Successfully assigned ${data.success.length} tag(s):</strong><ul class="list-disc list-inside mt-2">`;
|
||||
data.success.forEach(item => {
|
||||
html += `<li>${item.device_name}: ${item.tag_name}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (data.failed.length > 0) {
|
||||
html += `<div class="text-red-600 dark:text-red-400"><strong>Failed ${data.failed.length} assignment(s):</strong><ul class="list-disc list-inside mt-2">`;
|
||||
data.failed.forEach(item => {
|
||||
html += `<li>Device ID ${item.device_id}, Tag ID ${item.tag_id}: ${item.reason}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
resultDiv.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.innerHTML = `<p class="text-red-600">Error: ${error.message}</p>`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.tab-btn.active {
|
||||
background-color: rgb(156 163 175);
|
||||
color: white;
|
||||
}
|
||||
.dark .tab-btn.active {
|
||||
background-color: rgb(63 63 70);
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark: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 w-auto min-w-[20rem] max-w-2xl pt-20">
|
||||
<div class="container py-8 w-auto min-w-2xl pt-20">
|
||||
<div class="flex items-center mb-8 relative justify-between gap-4">
|
||||
<a href="/devices" class="hidden sm:flex bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 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>
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
<div class="flex-1 flex items-center justify-center mx-4">
|
||||
<div class="container py-8 max-w-4xl pt-20">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
|
||||
<div class="flex flex-row justify-center gap-4 mb-6">
|
||||
<div class="flex flex-row justify-center gap-4 mb-6 flex-wrap">
|
||||
<a href="/add_device" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add New Device</a>
|
||||
<a href="/bulk" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Bulk Operations</a>
|
||||
<a href="/device_type_stats" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">View Device Stats</a>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user