feat: ✨ vlan management
This commit is contained in:
@@ -337,6 +337,19 @@ def init_db(app=None):
|
||||
if not cursor.fetchone():
|
||||
cursor.execute('ALTER TABLE IPAddress ADD COLUMN notes TEXT DEFAULT NULL')
|
||||
|
||||
# Add VLAN columns to Subnet table if they don't exist
|
||||
cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'vlan_id'")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_id INTEGER DEFAULT NULL')
|
||||
|
||||
cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'vlan_description'")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_description VARCHAR(255) DEFAULT NULL')
|
||||
|
||||
cursor.execute("SHOW COLUMNS FROM Subnet LIKE 'vlan_notes'")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_notes TEXT DEFAULT NULL')
|
||||
|
||||
# Define all permissions with categories
|
||||
permissions = [
|
||||
# View permissions
|
||||
|
||||
@@ -421,6 +421,22 @@ def parse_custom_field_value(field_type, raw_value):
|
||||
# text, textarea, ip_address, email, url, select
|
||||
return str(raw_value)
|
||||
|
||||
def validate_vlan_id(vlan_id_str):
|
||||
"""
|
||||
Validate VLAN ID. Must be integer between 1-4094 (standard VLAN range).
|
||||
Returns (is_valid, error_message, vlan_id_int)
|
||||
"""
|
||||
if vlan_id_str is None or vlan_id_str == '':
|
||||
return True, None, None
|
||||
|
||||
try:
|
||||
vlan_id = int(vlan_id_str)
|
||||
if vlan_id < 1 or vlan_id > 4094:
|
||||
return False, "VLAN ID must be between 1 and 4094", None
|
||||
return True, None, vlan_id
|
||||
except ValueError:
|
||||
return False, "VLAN ID must be a valid integer", None
|
||||
|
||||
def get_custom_fields_for_entity(entity_type, entity_id, conn=None):
|
||||
"""
|
||||
Retrieve custom field definitions with their values for an entity.
|
||||
@@ -1422,7 +1438,17 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
custom_fields = get_custom_fields_for_entity('subnet', subnet_id, conn=conn)
|
||||
return render_with_user('subnet.html', subnet=cached_result['subnet'],
|
||||
# Ensure VLAN fields are in cached subnet dict
|
||||
subnet_dict = cached_result['subnet']
|
||||
if 'vlan_id' not in subnet_dict:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT vlan_id, vlan_description, vlan_notes FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
vlan_row = cursor.fetchone()
|
||||
if vlan_row:
|
||||
subnet_dict['vlan_id'] = vlan_row[0]
|
||||
subnet_dict['vlan_description'] = vlan_row[1]
|
||||
subnet_dict['vlan_notes'] = vlan_row[2]
|
||||
return render_with_user('subnet.html', subnet=subnet_dict,
|
||||
ip_addresses=cached_result['ip_addresses'],
|
||||
utilization=cached_result['utilization'],
|
||||
custom_fields=custom_fields,
|
||||
@@ -1431,7 +1457,7 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name, cidr FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
cursor.execute('SELECT id, name, cidr, vlan_id, vlan_description, vlan_notes FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
cursor.execute('SELECT id, ip, hostname, notes FROM IPAddress WHERE subnet_id = %s', (subnet_id,))
|
||||
ip_addresses = cursor.fetchall()
|
||||
@@ -1484,7 +1510,14 @@ def register_routes(app, limiter=None):
|
||||
device_id, device_description = match
|
||||
ip_addresses_with_device.append((ip_id, ip_address, hostname, device_id, device_description, ip_notes))
|
||||
|
||||
subnet_dict = {'id': subnet[0], 'name': subnet[1], 'cidr': subnet[2]}
|
||||
subnet_dict = {
|
||||
'id': subnet[0],
|
||||
'name': subnet[1],
|
||||
'cidr': subnet[2],
|
||||
'vlan_id': subnet[3] if len(subnet) > 3 else None,
|
||||
'vlan_description': subnet[4] if len(subnet) > 4 else None,
|
||||
'vlan_notes': subnet[5] if len(subnet) > 5 else None
|
||||
}
|
||||
result = {
|
||||
'subnet': subnet_dict,
|
||||
'ip_addresses': ip_addresses_with_device,
|
||||
@@ -1504,7 +1537,19 @@ def register_routes(app, limiter=None):
|
||||
name = request.form['name']
|
||||
cidr = request.form['cidr']
|
||||
site = request.form['site']
|
||||
vlan_id_str = request.form.get('vlan_id', '').strip()
|
||||
vlan_description = request.form.get('vlan_description', '').strip()
|
||||
vlan_notes = request.form.get('vlan_notes', '').strip()
|
||||
user_name = get_current_user_name()
|
||||
|
||||
# Validate VLAN ID if provided
|
||||
if vlan_id_str:
|
||||
is_valid, error_msg, vlan_id = validate_vlan_id(vlan_id_str)
|
||||
if not is_valid:
|
||||
return render_with_user('admin.html', subnets=[], error=error_msg)
|
||||
else:
|
||||
vlan_id = None
|
||||
|
||||
try:
|
||||
network = ip_network(cidr, strict=False)
|
||||
if network.prefixlen < 24:
|
||||
@@ -1514,11 +1559,13 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO Subnet (name, cidr, site) VALUES (%s, %s, %s)', (name, cidr, site))
|
||||
cursor.execute('INSERT INTO Subnet (name, cidr, site, vlan_id, vlan_description, vlan_notes) VALUES (%s, %s, %s, %s, %s, %s)',
|
||||
(name, cidr, site, vlan_id, vlan_description if vlan_description else None, vlan_notes if vlan_notes else None))
|
||||
subnet_id = cursor.lastrowid
|
||||
ip_rows = [(str(ip), subnet_id) for ip in network.hosts()]
|
||||
cursor.executemany('INSERT INTO IPAddress (ip, subnet_id) VALUES (%s, %s)', ip_rows)
|
||||
add_audit_log(session['user_id'], 'add_subnet', f"Added subnet {name} ({cidr})", subnet_id, conn=conn)
|
||||
vlan_info = f" (VLAN {vlan_id})" if vlan_id else ""
|
||||
add_audit_log(session['user_id'], 'add_subnet', f"Added subnet {name} ({cidr}){vlan_info}", subnet_id, conn=conn)
|
||||
conn.commit()
|
||||
# Invalidate cache
|
||||
cache.clear('index')
|
||||
@@ -1535,7 +1582,19 @@ def register_routes(app, limiter=None):
|
||||
name = request.form['name']
|
||||
cidr = request.form['cidr']
|
||||
site = request.form['site']
|
||||
vlan_id_str = request.form.get('vlan_id', '').strip()
|
||||
vlan_description = request.form.get('vlan_description', '').strip()
|
||||
vlan_notes = request.form.get('vlan_notes', '').strip()
|
||||
user_name = get_current_user_name()
|
||||
|
||||
# Validate VLAN ID if provided
|
||||
if vlan_id_str:
|
||||
is_valid, error_msg, vlan_id = validate_vlan_id(vlan_id_str)
|
||||
if not is_valid:
|
||||
return render_with_user('admin.html', subnets=[], error=error_msg)
|
||||
else:
|
||||
vlan_id = None
|
||||
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -1543,8 +1602,10 @@ def register_routes(app, limiter=None):
|
||||
old_subnet = cursor.fetchone()
|
||||
if old_subnet:
|
||||
old_name, old_cidr = old_subnet
|
||||
cursor.execute('UPDATE Subnet SET name = %s, cidr = %s, site = %s WHERE id = %s', (name, cidr, site, subnet_id))
|
||||
add_audit_log(session['user_id'], 'edit_subnet', f"Edited subnet from {old_name} ({old_cidr}) to {name} ({cidr}) at site {site}", subnet_id, conn=conn)
|
||||
cursor.execute('UPDATE Subnet SET name = %s, cidr = %s, site = %s, vlan_id = %s, vlan_description = %s, vlan_notes = %s WHERE id = %s',
|
||||
(name, cidr, site, vlan_id, vlan_description if vlan_description else None, vlan_notes if vlan_notes else None, subnet_id))
|
||||
vlan_info = f" (VLAN {vlan_id})" if vlan_id else ""
|
||||
add_audit_log(session['user_id'], 'edit_subnet', f"Edited subnet from {old_name} ({old_cidr}) to {name} ({cidr}) at site {site}{vlan_info}", subnet_id, conn=conn)
|
||||
conn.commit()
|
||||
# Invalidate cache
|
||||
invalidate_cache_for_subnet(subnet_id)
|
||||
@@ -1581,13 +1642,24 @@ def register_routes(app, limiter=None):
|
||||
def admin():
|
||||
cache_key = 'admin'
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
# Check if cached data has VLAN fields (for backward compatibility)
|
||||
if cached_result is not None:
|
||||
# Verify cached subnets have VLAN fields, if not, refresh cache
|
||||
if cached_result.get('subnets') and len(cached_result['subnets']) > 0:
|
||||
sample_subnet = cached_result['subnets'][0]
|
||||
if 'vlan_id' not in sample_subnet:
|
||||
# Cache is stale, clear it and regenerate
|
||||
cache.clear(cache_key)
|
||||
cached_result = None
|
||||
|
||||
if cached_result is not None:
|
||||
return render_with_user('admin.html', **cached_result)
|
||||
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
||||
cursor.execute('SELECT id, name, cidr, site, vlan_id, vlan_description, vlan_notes FROM Subnet ORDER BY site, name')
|
||||
subnet_rows = cursor.fetchall()
|
||||
subnets = []
|
||||
for row in subnet_rows:
|
||||
@@ -1619,6 +1691,9 @@ def register_routes(app, limiter=None):
|
||||
'name': row[1],
|
||||
'cidr': row[2],
|
||||
'site': row[3] or 'Unassigned',
|
||||
'vlan_id': row[4] if len(row) > 4 and row[4] is not None else None,
|
||||
'vlan_description': row[5] if len(row) > 5 and row[5] is not None else None,
|
||||
'vlan_notes': row[6] if len(row) > 6 and row[6] is not None else None,
|
||||
'utilization': {
|
||||
'percent': round(utilization_percent, 1),
|
||||
'assigned': assigned_ips,
|
||||
@@ -3942,7 +4017,7 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
||||
cursor.execute('SELECT id, name, cidr, site, vlan_id, vlan_description, vlan_notes FROM Subnet ORDER BY site, name')
|
||||
subnets = cursor.fetchall()
|
||||
for subnet in subnets:
|
||||
cursor.execute('SELECT COUNT(*) as total, COUNT(CASE WHEN hostname IS NOT NULL THEN 1 END) as used FROM IPAddress WHERE subnet_id = %s', (subnet['id'],))
|
||||
@@ -3970,7 +4045,7 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
cursor.execute('SELECT id, name, cidr, site, vlan_id, vlan_description, vlan_notes FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
subnet = cursor.fetchone()
|
||||
if not subnet:
|
||||
return jsonify({'error': 'Subnet not found'}), 404
|
||||
@@ -4035,6 +4110,17 @@ def register_routes(app, limiter=None):
|
||||
name = data['name']
|
||||
cidr = data['cidr']
|
||||
site = data.get('site', '')
|
||||
vlan_id_str = str(data.get('vlan_id', '')).strip() if data.get('vlan_id') else ''
|
||||
vlan_description = data.get('vlan_description', '').strip() if data.get('vlan_description') else ''
|
||||
vlan_notes = data.get('vlan_notes', '').strip() if data.get('vlan_notes') else ''
|
||||
|
||||
# Validate VLAN ID if provided
|
||||
if vlan_id_str:
|
||||
is_valid, error_msg, vlan_id = validate_vlan_id(vlan_id_str)
|
||||
if not is_valid:
|
||||
return jsonify({'error': error_msg}), 400
|
||||
else:
|
||||
vlan_id = None
|
||||
|
||||
try:
|
||||
network = ip_network(cidr, strict=False)
|
||||
@@ -4046,13 +4132,23 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO Subnet (name, cidr, site) VALUES (%s, %s, %s)', (name, cidr, site))
|
||||
cursor.execute('INSERT INTO Subnet (name, cidr, site, vlan_id, vlan_description, vlan_notes) VALUES (%s, %s, %s, %s, %s, %s)',
|
||||
(name, cidr, site, vlan_id, vlan_description if vlan_description else None, vlan_notes if vlan_notes else None))
|
||||
subnet_id = cursor.lastrowid
|
||||
ip_rows = [(str(ip), subnet_id) for ip in network.hosts()]
|
||||
cursor.executemany('INSERT INTO IPAddress (ip, subnet_id) VALUES (%s, %s)', ip_rows)
|
||||
add_audit_log(request.api_user['id'], 'add_subnet', f"Added subnet {name} ({cidr})", subnet_id, conn=conn)
|
||||
vlan_info = f" (VLAN {vlan_id})" if vlan_id else ""
|
||||
add_audit_log(request.api_user['id'], 'add_subnet', f"Added subnet {name} ({cidr}){vlan_info}", subnet_id, conn=conn)
|
||||
conn.commit()
|
||||
return jsonify({'id': subnet_id, 'name': name, 'cidr': cidr, 'site': site}), 201
|
||||
return jsonify({
|
||||
'id': subnet_id,
|
||||
'name': name,
|
||||
'cidr': cidr,
|
||||
'site': site,
|
||||
'vlan_id': vlan_id,
|
||||
'vlan_description': vlan_description if vlan_description else None,
|
||||
'vlan_notes': vlan_notes if vlan_notes else None
|
||||
}), 201
|
||||
|
||||
@app.route('/api/v1/subnets/<int:subnet_id>', methods=['PUT'])
|
||||
@rate_limit("50 per minute")
|
||||
@@ -4066,16 +4162,37 @@ def register_routes(app, limiter=None):
|
||||
from flask import current_app
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name, cidr, site FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
cursor.execute('SELECT name, cidr, site, vlan_id, vlan_description, vlan_notes FROM Subnet WHERE id = %s', (subnet_id,))
|
||||
old_subnet = cursor.fetchone()
|
||||
if not old_subnet:
|
||||
return jsonify({'error': 'Subnet not found'}), 404
|
||||
old_name, old_cidr, old_site = old_subnet
|
||||
old_name, old_cidr, old_site, old_vlan_id, old_vlan_desc, old_vlan_notes = old_subnet
|
||||
|
||||
new_name = data.get('name', old_name)
|
||||
new_cidr = data.get('cidr', old_cidr)
|
||||
new_site = data.get('site', old_site)
|
||||
|
||||
# Handle VLAN fields
|
||||
vlan_id_str = str(data.get('vlan_id', '')).strip() if data.get('vlan_id') is not None else ''
|
||||
new_vlan_description = data.get('vlan_description', '').strip() if data.get('vlan_description') else ''
|
||||
new_vlan_notes = data.get('vlan_notes', '').strip() if data.get('vlan_notes') else ''
|
||||
|
||||
# Validate VLAN ID if provided
|
||||
if vlan_id_str:
|
||||
is_valid, error_msg, new_vlan_id = validate_vlan_id(vlan_id_str)
|
||||
if not is_valid:
|
||||
return jsonify({'error': error_msg}), 400
|
||||
elif 'vlan_id' in data and data['vlan_id'] is None:
|
||||
new_vlan_id = None
|
||||
else:
|
||||
new_vlan_id = old_vlan_id
|
||||
|
||||
# Use old values if not provided in request
|
||||
if 'vlan_description' not in data:
|
||||
new_vlan_description = old_vlan_desc if old_vlan_desc else ''
|
||||
if 'vlan_notes' not in data:
|
||||
new_vlan_notes = old_vlan_notes if old_vlan_notes else ''
|
||||
|
||||
updates = []
|
||||
values = []
|
||||
if new_name != old_name:
|
||||
@@ -4087,21 +4204,42 @@ def register_routes(app, limiter=None):
|
||||
if new_site != old_site:
|
||||
updates.append('site = %s')
|
||||
values.append(new_site)
|
||||
if new_vlan_id != old_vlan_id:
|
||||
updates.append('vlan_id = %s')
|
||||
values.append(new_vlan_id)
|
||||
if new_vlan_description != (old_vlan_desc or ''):
|
||||
updates.append('vlan_description = %s')
|
||||
values.append(new_vlan_description if new_vlan_description else None)
|
||||
if new_vlan_notes != (old_vlan_notes or ''):
|
||||
updates.append('vlan_notes = %s')
|
||||
values.append(new_vlan_notes if new_vlan_notes else None)
|
||||
|
||||
if not updates:
|
||||
return jsonify({'error': 'No changes to apply'}), 400
|
||||
|
||||
values.append(subnet_id)
|
||||
cursor.execute(f'UPDATE Subnet SET {", ".join(updates)} WHERE id = %s', values)
|
||||
vlan_info = f" (VLAN {new_vlan_id})" if new_vlan_id else ""
|
||||
add_audit_log(
|
||||
request.api_user['id'],
|
||||
'edit_subnet',
|
||||
f"Edited subnet from {old_name} ({old_cidr}) to {new_name} ({new_cidr}) at site {new_site or 'Unassigned'}",
|
||||
f"Edited subnet from {old_name} ({old_cidr}) to {new_name} ({new_cidr}) at site {new_site or 'Unassigned'}{vlan_info}",
|
||||
subnet_id,
|
||||
conn=conn
|
||||
)
|
||||
conn.commit()
|
||||
return jsonify({'message': 'Subnet updated successfully', 'subnet': {'id': subnet_id, 'name': new_name, 'cidr': new_cidr, 'site': new_site}})
|
||||
return jsonify({
|
||||
'message': 'Subnet updated successfully',
|
||||
'subnet': {
|
||||
'id': subnet_id,
|
||||
'name': new_name,
|
||||
'cidr': new_cidr,
|
||||
'site': new_site,
|
||||
'vlan_id': new_vlan_id,
|
||||
'vlan_description': new_vlan_description if new_vlan_description else None,
|
||||
'vlan_notes': new_vlan_notes if new_vlan_notes else None
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/v1/subnets/<int:subnet_id>', methods=['DELETE'])
|
||||
@rate_limit("50 per minute")
|
||||
|
||||
+83
-1
@@ -3,24 +3,99 @@ function showAddSubnetModal() {
|
||||
document.getElementById('add-subnet-name').value = '';
|
||||
document.getElementById('add-subnet-cidr').value = '';
|
||||
document.getElementById('add-subnet-site').value = '';
|
||||
document.getElementById('add-subnet-vlan-id').value = '';
|
||||
document.getElementById('add-subnet-vlan-description').value = '';
|
||||
document.getElementById('add-subnet-vlan-notes').value = '';
|
||||
document.getElementById('vlan-id-error').classList.add('hidden');
|
||||
}
|
||||
|
||||
function closeAddSubnetModal() {
|
||||
document.getElementById('add-subnet-modal').classList.add('hidden');
|
||||
document.getElementById('cidr-error').classList.add('hidden');
|
||||
document.getElementById('vlan-id-error').classList.add('hidden');
|
||||
}
|
||||
|
||||
function editSubnet(subnetId, name, cidr, site) {
|
||||
function editSubnet(subnetId, name, cidr, site, vlanId, vlanDescription, vlanNotes) {
|
||||
document.getElementById('edit-subnet-id').value = subnetId;
|
||||
document.getElementById('edit-subnet-name').value = name;
|
||||
document.getElementById('edit-subnet-cidr').value = cidr;
|
||||
document.getElementById('edit-subnet-site').value = site;
|
||||
document.getElementById('edit-subnet-vlan-id').value = vlanId || '';
|
||||
document.getElementById('edit-subnet-vlan-description').value = vlanDescription || '';
|
||||
document.getElementById('edit-subnet-vlan-notes').value = vlanNotes || '';
|
||||
document.getElementById('edit-subnet-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeEditSubnetModal() {
|
||||
document.getElementById('edit-subnet-modal').classList.add('hidden');
|
||||
document.getElementById('edit-cidr-error').classList.add('hidden');
|
||||
document.getElementById('edit-vlan-id-error').classList.add('hidden');
|
||||
}
|
||||
|
||||
function validateVlanId(vlanIdValue, errorElementId) {
|
||||
if (!vlanIdValue || vlanIdValue.trim() === '') {
|
||||
return true; // VLAN ID is optional
|
||||
}
|
||||
|
||||
const vlanId = parseInt(vlanIdValue.trim());
|
||||
if (isNaN(vlanId)) {
|
||||
const errorElement = document.getElementById(errorElementId);
|
||||
if (errorElement) {
|
||||
errorElement.textContent = 'VLAN ID must be a valid integer';
|
||||
errorElement.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vlanId < 1 || vlanId > 4094) {
|
||||
const errorElement = document.getElementById(errorElementId);
|
||||
if (errorElement) {
|
||||
errorElement.textContent = 'VLAN ID must be between 1 and 4094';
|
||||
errorElement.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const errorElement = document.getElementById(errorElementId);
|
||||
if (errorElement) {
|
||||
errorElement.classList.add('hidden');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateSubnetForm() {
|
||||
const cidrInput = document.getElementById('add-subnet-cidr');
|
||||
const cidrError = document.getElementById('cidr-error');
|
||||
const cidr = cidrInput.value.trim();
|
||||
|
||||
// Basic CIDR validation
|
||||
const cidrPattern = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
|
||||
if (!cidrPattern.test(cidr)) {
|
||||
cidrError.textContent = 'Invalid CIDR format. Use format like 192.168.1.0/24';
|
||||
cidrError.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check prefix length
|
||||
const parts = cidr.split('/');
|
||||
if (parts.length === 2) {
|
||||
const prefixLen = parseInt(parts[1]);
|
||||
if (prefixLen < 24 || prefixLen > 32) {
|
||||
cidrError.textContent = 'Subnet must be /24 or smaller (e.g., /24, /25, ... /32)';
|
||||
cidrError.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cidrError.classList.add('hidden');
|
||||
|
||||
// Validate VLAN ID
|
||||
const vlanIdInput = document.getElementById('add-subnet-vlan-id');
|
||||
if (!validateVlanId(vlanIdInput.value, 'vlan-id-error')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateEditSubnetForm() {
|
||||
@@ -48,6 +123,13 @@ function validateEditSubnetForm() {
|
||||
}
|
||||
|
||||
cidrError.classList.add('hidden');
|
||||
|
||||
// Validate VLAN ID
|
||||
const vlanIdInput = document.getElementById('edit-subnet-vlan-id');
|
||||
if (!validateVlanId(vlanIdInput.value, 'edit-vlan-id-error')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
function showAddSubnetModal(){document.getElementById("add-subnet-modal").classList.remove("hidden"),document.getElementById("add-subnet-name").value="",document.getElementById("add-subnet-cidr").value="",document.getElementById("add-subnet-site").value=""}function closeAddSubnetModal(){document.getElementById("add-subnet-modal").classList.add("hidden"),document.getElementById("cidr-error").classList.add("hidden")}function editSubnet(e,t,d,n){document.getElementById("edit-subnet-id").value=e,document.getElementById("edit-subnet-name").value=t,document.getElementById("edit-subnet-cidr").value=d,document.getElementById("edit-subnet-site").value=n,document.getElementById("edit-subnet-modal").classList.remove("hidden")}function closeEditSubnetModal(){document.getElementById("edit-subnet-modal").classList.add("hidden"),document.getElementById("edit-cidr-error").classList.add("hidden")}function validateEditSubnetForm(){let e=document.getElementById("edit-subnet-cidr"),t=document.getElementById("edit-cidr-error"),d=e.value.trim();if(!/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(d))return t.textContent="Invalid CIDR format. Use format like 192.168.1.0/24",t.classList.remove("hidden"),!1;let n=d.split("/");if(2===n.length){let l=parseInt(n[1]);if(l<24||l>32)return t.textContent="Subnet must be /24 or smaller (e.g., /24, /25, ... /32)",t.classList.remove("hidden"),!1}return t.classList.add("hidden"),!0}window.onclick=function(e){let t=document.getElementById("add-subnet-modal"),d=document.getElementById("edit-subnet-modal");e.target===t&&closeAddSubnetModal(),e.target===d&&closeEditSubnetModal()};
|
||||
function showAddSubnetModal(){document.getElementById("add-subnet-modal").classList.remove("hidden"),document.getElementById("add-subnet-name").value="",document.getElementById("add-subnet-cidr").value="",document.getElementById("add-subnet-site").value="",document.getElementById("add-subnet-vlan-id").value="",document.getElementById("add-subnet-vlan-description").value="",document.getElementById("add-subnet-vlan-notes").value="",document.getElementById("vlan-id-error").classList.add("hidden")}function closeAddSubnetModal(){document.getElementById("add-subnet-modal").classList.add("hidden"),document.getElementById("cidr-error").classList.add("hidden"),document.getElementById("vlan-id-error").classList.add("hidden")}function editSubnet(e,t,d,n,l,i,a){document.getElementById("edit-subnet-id").value=e,document.getElementById("edit-subnet-name").value=t,document.getElementById("edit-subnet-cidr").value=d,document.getElementById("edit-subnet-site").value=n,document.getElementById("edit-subnet-vlan-id").value=l||"",document.getElementById("edit-subnet-vlan-description").value=i||"",document.getElementById("edit-subnet-vlan-notes").value=a||"",document.getElementById("edit-subnet-modal").classList.remove("hidden")}function closeEditSubnetModal(){document.getElementById("edit-subnet-modal").classList.add("hidden"),document.getElementById("edit-cidr-error").classList.add("hidden"),document.getElementById("edit-vlan-id-error").classList.add("hidden")}function validateVlanId(e,t){if(!e||""===e.trim())return!0;let d=parseInt(e.trim());if(isNaN(d)){let n=document.getElementById(t);return n&&(n.textContent="VLAN ID must be a valid integer",n.classList.remove("hidden")),!1}if(d<1||d>4094){let l=document.getElementById(t);return l&&(l.textContent="VLAN ID must be between 1 and 4094",l.classList.remove("hidden")),!1}let i=document.getElementById(t);return i&&i.classList.add("hidden"),!0}function validateSubnetForm(){let e=document.getElementById("add-subnet-cidr"),t=document.getElementById("cidr-error"),d=e.value.trim();if(!/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(d))return t.textContent="Invalid CIDR format. Use format like 192.168.1.0/24",t.classList.remove("hidden"),!1;let n=d.split("/");if(2===n.length){let l=parseInt(n[1]);if(l<24||l>32)return t.textContent="Subnet must be /24 or smaller (e.g., /24, /25, ... /32)",t.classList.remove("hidden"),!1}t.classList.add("hidden");let i=document.getElementById("add-subnet-vlan-id");return!!validateVlanId(i.value,"vlan-id-error")}function validateEditSubnetForm(){let e=document.getElementById("edit-subnet-cidr"),t=document.getElementById("edit-cidr-error"),d=e.value.trim();if(!/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(d))return t.textContent="Invalid CIDR format. Use format like 192.168.1.0/24",t.classList.remove("hidden"),!1;let n=d.split("/");if(2===n.length){let l=parseInt(n[1]);if(l<24||l>32)return t.textContent="Subnet must be /24 or smaller (e.g., /24, /25, ... /32)",t.classList.remove("hidden"),!1}t.classList.add("hidden");let i=document.getElementById("edit-subnet-vlan-id");return!!validateVlanId(i.value,"edit-vlan-id-error")}window.onclick=function(e){let t=document.getElementById("add-subnet-modal"),d=document.getElementById("edit-subnet-modal");e.target===t&&closeAddSubnetModal(),e.target===d&&closeEditSubnetModal()};
|
||||
+17
-1
@@ -107,6 +107,7 @@
|
||||
<th class="text-center p-3">Name</th>
|
||||
<th class="text-center p-3">CIDR</th>
|
||||
<th class="text-center p-3">Site</th>
|
||||
<th class="text-center p-3">VLAN ID</th>
|
||||
<th class="text-center p-3">Utilisation</th>
|
||||
<th class="text-center p-3">Actions</th>
|
||||
</tr>
|
||||
@@ -119,6 +120,13 @@
|
||||
<td class="p-3 text-center">
|
||||
<span class="px-2 py-1 bg-gray-300 dark:bg-zinc-700 rounded text-sm">{{ subnet.site }}</span>
|
||||
</td>
|
||||
<td class="p-3 text-center">
|
||||
{% if subnet.vlan_id %}
|
||||
<span class="px-2 py-1 bg-gray-300 dark:bg-zinc-700 rounded text-sm font-mono">{{ subnet.vlan_id }}</span>
|
||||
{% else %}
|
||||
<span class="text-sm text-gray-500">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="p-3 text-center">
|
||||
{% if subnet.utilization %}
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ subnet.utilization.percent }}%</span>
|
||||
@@ -133,7 +141,7 @@
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if can_edit_subnet %}
|
||||
<button onclick="editSubnet({{ subnet.id }}, '{{ subnet.name|replace("'", "\\'") }}', '{{ subnet.cidr|replace("'", "\\'") }}', '{{ subnet.site|replace("'", "\\'") }}')" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer" title="Edit Subnet">
|
||||
<button onclick="editSubnet({{ subnet.id }}, '{{ subnet.name|replace("'", "\\'") }}', '{{ subnet.cidr|replace("'", "\\'") }}', '{{ subnet.site|replace("'", "\\'") }}', {{ subnet.vlan_id if subnet.vlan_id else 'null' }}, '{{ subnet.vlan_description|replace("'", "\\'") if subnet.vlan_description else "" }}', '{{ subnet.vlan_notes|replace("'", "\\'")|replace("\n", "\\n") if subnet.vlan_notes else "" }}')" class="text-gray-900 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-300 hover:cursor-pointer" title="Edit Subnet">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -176,7 +184,11 @@
|
||||
<input type="text" name="name" id="add-subnet-name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
<input type="text" name="cidr" id="add-subnet-cidr" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
<input type="text" name="site" id="add-subnet-site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
<input type="number" name="vlan_id" id="add-subnet-vlan-id" placeholder="VLAN ID (1-4094, optional)" min="1" max="4094" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
|
||||
<input type="text" name="vlan_description" id="add-subnet-vlan-description" placeholder="VLAN Description (optional)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
|
||||
<textarea name="vlan_notes" id="add-subnet-vlan-notes" placeholder="VLAN Notes (optional)" rows="3" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full resize-y"></textarea>
|
||||
<span id="cidr-error" class="text-red-500 text-sm hidden"></span>
|
||||
<span id="vlan-id-error" class="text-red-500 text-sm hidden"></span>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2 mt-6">
|
||||
<button type="button" onclick="closeAddSubnetModal()" class="px-4 py-2 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
|
||||
@@ -201,7 +213,11 @@
|
||||
<input type="text" name="name" id="edit-subnet-name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
<input type="text" name="cidr" id="edit-subnet-cidr" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
<input type="text" name="site" id="edit-subnet-site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
<input type="number" name="vlan_id" id="edit-subnet-vlan-id" placeholder="VLAN ID (1-4094, optional)" min="1" max="4094" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
|
||||
<input type="text" name="vlan_description" id="edit-subnet-vlan-description" placeholder="VLAN Description (optional)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full">
|
||||
<textarea name="vlan_notes" id="edit-subnet-vlan-notes" placeholder="VLAN Notes (optional)" rows="3" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full resize-y"></textarea>
|
||||
<span id="edit-cidr-error" class="text-red-500 text-sm hidden"></span>
|
||||
<span id="edit-vlan-id-error" class="text-red-500 text-sm hidden"></span>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2 mt-6">
|
||||
<button type="button" onclick="closeEditSubnetModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer rounded-lg">Cancel</button>
|
||||
|
||||
+190
-135
@@ -19,152 +19,207 @@
|
||||
<i class="fas fa-file-csv fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% if utilization %}
|
||||
<div class="hidden sm:flex justify-center mb-4">
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 px-4 py-2 rounded-lg text-sm">
|
||||
<span class="font-medium">{{ utilization.percent }}% used</span>
|
||||
<span class="text-gray-600 dark:text-gray-400 mx-2">•</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ utilization.assigned }} assigned</span>
|
||||
<span class="text-gray-600 dark:text-gray-400 mx-2">•</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ utilization.dhcp }} DHCP</span>
|
||||
<span class="text-gray-600 dark:text-gray-400 mx-2">•</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ utilization.available }} available</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex justify-center mb-4">
|
||||
<a href="/subnet/{{ subnet.id }}/dhcp" class="hidden sm:flex bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg shadow items-center gap-2">
|
||||
<i class="fas fa-network-wired"></i> Define DHCP Pool
|
||||
</a>
|
||||
</div>
|
||||
<button id="toggle-desc" class="sm:hidden bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg mb-4 w-full">Show Descriptions</button>
|
||||
|
||||
<!-- Custom Fields Section -->
|
||||
{% if custom_fields %}
|
||||
<div class="custom-fields-section mb-6 max-w-md mx-auto">
|
||||
{% if can_edit_subnet %}
|
||||
<form action="/subnet/{{ subnet.id }}/update_custom_fields" method="POST" id="custom-fields-form">
|
||||
<div class="space-y-4">
|
||||
<!-- Info Grid: 3 columns on desktop, 1 on mobile -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<!-- Utilisation Stats Column -->
|
||||
{% if utilization %}
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-bold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
Utilisation
|
||||
</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Used:</span>
|
||||
<span class="font-medium">{{ utilization.percent }}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Assigned:</span>
|
||||
<span>{{ utilization.assigned }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">DHCP:</span>
|
||||
<span>{{ utilization.dhcp }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Available:</span>
|
||||
<span>{{ utilization.available }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-t border-gray-600 pt-2 mt-2">
|
||||
<span class="text-gray-600 dark:text-gray-400">Total:</span>
|
||||
<span class="font-medium">{{ utilization.total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- VLAN Information Column -->
|
||||
{% if subnet.vlan_id or subnet.vlan_description or subnet.vlan_notes %}
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-bold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
VLAN
|
||||
</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
{% if subnet.vlan_id %}
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400">ID:</span>
|
||||
<span class="ml-2 px-2 py-1 bg-gray-300 dark:bg-zinc-700 rounded text-sm font-mono">{{ subnet.vlan_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if subnet.vlan_description %}
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400 block mb-1">Description:</span>
|
||||
<span>{{ subnet.vlan_description }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if subnet.vlan_notes %}
|
||||
<div>
|
||||
<span class="text-gray-600 dark:text-gray-400 block mb-1">Notes:</span>
|
||||
<div class="whitespace-pre-wrap text-xs">{{ subnet.vlan_notes }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom Fields Column -->
|
||||
{% if custom_fields %}
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-bold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-list-ul"></i>
|
||||
Custom Fields
|
||||
</h3>
|
||||
{% if can_edit_subnet %}
|
||||
<form action="/subnet/{{ subnet.id }}/update_custom_fields" method="POST" id="custom-fields-form">
|
||||
<div class="space-y-3">
|
||||
{% for field in custom_fields %}
|
||||
<div class="custom-field-item">
|
||||
<label for="custom_field_{{ field.field_key }}" class="block mb-1 text-xs font-medium">
|
||||
{{ field.name }}
|
||||
{% if field.required %}<span class="text-red-500">*</span>{% endif %}
|
||||
</label>
|
||||
{% if field.field_type == 'textarea' %}
|
||||
<textarea name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full resize-y text-sm"
|
||||
{% if field.required %}required{% endif %}
|
||||
placeholder="{{ field.help_text or '' }}">{{ field.current_value or field.default_value or '' }}</textarea>
|
||||
{% elif field.field_type == 'boolean' %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
value="true"
|
||||
{% if field.current_value or (not field.current_value and field.default_value == 'true') %}checked{% endif %}
|
||||
class="w-4 h-4">
|
||||
<label for="custom_field_{{ field.field_key }}" class="text-sm">Yes</label>
|
||||
</div>
|
||||
{% elif field.field_type == 'select' %}
|
||||
{% set options = [] %}
|
||||
{% if field.validation_rules and field.validation_rules.select_options %}
|
||||
{% set options = field.validation_rules.select_options %}
|
||||
{% endif %}
|
||||
<select name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% if not field.required %}
|
||||
<option value="">-- None --</option>
|
||||
{% endif %}
|
||||
{% for option in options %}
|
||||
<option value="{{ option }}" {% if field.current_value == option or (not field.current_value and field.default_value == option) %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif field.field_type == 'date' %}
|
||||
<input type="date" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'datetime' %}
|
||||
<input type="datetime-local" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'number' %}
|
||||
<input type="number" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.min_value %}min="{{ field.validation_rules.min_value }}"{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.max_value %}max="{{ field.validation_rules.max_value }}"{% endif %}>
|
||||
{% elif field.field_type == 'decimal' %}
|
||||
<input type="number" step="any" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.min_value %}min="{{ field.validation_rules.min_value }}"{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.max_value %}max="{{ field.validation_rules.max_value }}"{% endif %}>
|
||||
{% elif field.field_type == 'ip_address' %}
|
||||
<input type="text" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full font-mono text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="192.168.1.1"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'email' %}
|
||||
<input type="email" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="user@example.com"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'url' %}
|
||||
<input type="url" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="https://example.com"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% else %}
|
||||
<input type="text" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="{{ field.help_text or '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.min_length %}minlength="{{ field.validation_rules.min_length }}"{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.max_length %}maxlength="{{ field.validation_rules.max_length }}"{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="space-y-3">
|
||||
{% for field in custom_fields %}
|
||||
<div class="custom-field-item">
|
||||
<label for="custom_field_{{ field.field_key }}" class="block mb-1 text-sm font-medium">
|
||||
{{ field.name }}
|
||||
{% if field.required %}<span class="text-red-500">*</span>{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2" title="{{ field.help_text }}">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% if field.field_type == 'textarea' %}
|
||||
<textarea name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full resize-y"
|
||||
{% if field.required %}required{% endif %}
|
||||
placeholder="{{ field.help_text or '' }}">{{ field.current_value or field.default_value or '' }}</textarea>
|
||||
{% elif field.field_type == 'boolean' %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
value="true"
|
||||
{% if field.current_value or (not field.current_value and field.default_value == 'true') %}checked{% endif %}
|
||||
class="w-4 h-4">
|
||||
<label for="custom_field_{{ field.field_key }}" class="text-sm">Yes</label>
|
||||
<label class="block mb-1 text-xs font-medium">{{ field.name }}</label>
|
||||
<div class="border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600 w-full text-sm">
|
||||
{{ field.current_value or field.default_value or '-' }}
|
||||
</div>
|
||||
{% elif field.field_type == 'select' %}
|
||||
{% set options = [] %}
|
||||
{% if field.validation_rules and field.validation_rules.select_options %}
|
||||
{% set options = field.validation_rules.select_options %}
|
||||
{% endif %}
|
||||
<select name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% if not field.required %}
|
||||
<option value="">-- None --</option>
|
||||
{% endif %}
|
||||
{% for option in options %}
|
||||
<option value="{{ option }}" {% if field.current_value == option or (not field.current_value and field.default_value == option) %}selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif field.field_type == 'date' %}
|
||||
<input type="date" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'datetime' %}
|
||||
<input type="datetime-local" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'number' %}
|
||||
<input type="number" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.min_value %}min="{{ field.validation_rules.min_value }}"{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.max_value %}max="{{ field.validation_rules.max_value }}"{% endif %}>
|
||||
{% elif field.field_type == 'decimal' %}
|
||||
<input type="number" step="any" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.min_value %}min="{{ field.validation_rules.min_value }}"{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.max_value %}max="{{ field.validation_rules.max_value }}"{% endif %}>
|
||||
{% elif field.field_type == 'ip_address' %}
|
||||
<input type="text" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full font-mono"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="192.168.1.1"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'email' %}
|
||||
<input type="email" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="user@example.com"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% elif field.field_type == 'url' %}
|
||||
<input type="url" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="https://example.com"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% else %}
|
||||
<input type="text" name="custom_field_{{ field.field_key }}"
|
||||
id="custom_field_{{ field.field_key }}"
|
||||
class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full"
|
||||
value="{{ field.current_value or field.default_value or '' }}"
|
||||
placeholder="{{ field.help_text or '' }}"
|
||||
{% if field.required %}required{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.min_length %}minlength="{{ field.validation_rules.min_length }}"{% endif %}
|
||||
{% if field.validation_rules and field.validation_rules.max_length %}maxlength="{{ field.validation_rules.max_length }}"{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="space-y-4">
|
||||
{% for field in custom_fields %}
|
||||
<div class="custom-field-item">
|
||||
<label class="block mb-1 text-sm font-medium">{{ field.name }}</label>
|
||||
<div class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full">
|
||||
{{ field.current_value or field.default_value or '-' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Actions Row -->
|
||||
<div class="flex justify-center mb-4 gap-2">
|
||||
<a href="/subnet/{{ subnet.id }}/dhcp" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg shadow items-center gap-2 flex">
|
||||
<i class="fas fa-network-wired"></i> <span class="hidden sm:inline">Define </span>DHCP Pool
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button id="toggle-desc" class="sm:hidden bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg mb-4 w-full">Show Descriptions</button>
|
||||
|
||||
<!-- IP Address Table -->
|
||||
<form action="" method="POST">
|
||||
<table class="table-auto w-full mb-6">
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user