feat: ✨ added the ability to create/edit/remove device types
This commit is contained in:
@@ -33,4 +33,7 @@ def inject_env_vars():
|
||||
}
|
||||
|
||||
register_routes(app)
|
||||
init_db(app)
|
||||
init_db(app)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -6,6 +6,7 @@ import os
|
||||
import csv
|
||||
from io import StringIO, BytesIO
|
||||
import logging
|
||||
import mysql.connector
|
||||
|
||||
app = None
|
||||
|
||||
@@ -601,6 +602,70 @@ def register_routes(app):
|
||||
stats = cursor.fetchall()
|
||||
return render_with_user('device_type_stats.html', stats=stats)
|
||||
|
||||
@app.route('/device_types', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def device_types():
|
||||
from flask import current_app
|
||||
error = None
|
||||
with get_db_connection(current_app) as conn:
|
||||
cursor = conn.cursor()
|
||||
if request.method == 'POST':
|
||||
action = request.form['action']
|
||||
user_name = get_current_user_name()
|
||||
if action == 'add':
|
||||
name = request.form['name'].strip()
|
||||
icon_class = request.form['icon_class'].strip()
|
||||
if not name:
|
||||
error = 'Device type name is required.'
|
||||
elif not icon_class:
|
||||
error = 'Icon class is required.'
|
||||
else:
|
||||
try:
|
||||
cursor.execute('INSERT INTO DeviceType (name, icon_class) VALUES (%s, %s)', (name, icon_class))
|
||||
conn.commit()
|
||||
logging.info(f"User {user_name} added device type '{name}' with icon '{icon_class}'.")
|
||||
except mysql.connector.IntegrityError as e:
|
||||
if e.errno == 1062: # Duplicate entry
|
||||
error = f"Device type '{name}' already exists."
|
||||
else:
|
||||
raise
|
||||
elif action == 'edit':
|
||||
device_type_id = request.form['device_type_id']
|
||||
name = request.form['name'].strip()
|
||||
icon_class = request.form['icon_class'].strip()
|
||||
if not name:
|
||||
error = 'Device type name is required.'
|
||||
elif not icon_class:
|
||||
error = 'Icon class is required.'
|
||||
else:
|
||||
try:
|
||||
cursor.execute('UPDATE DeviceType SET name = %s, icon_class = %s WHERE id = %s', (name, icon_class, device_type_id))
|
||||
conn.commit()
|
||||
logging.info(f"User {user_name} edited device type {device_type_id} to '{name}' with icon '{icon_class}'.")
|
||||
except mysql.connector.IntegrityError as e:
|
||||
if e.errno == 1062: # Duplicate entry
|
||||
error = f"Device type '{name}' already exists."
|
||||
else:
|
||||
raise
|
||||
elif action == 'delete':
|
||||
device_type_id = request.form['device_type_id']
|
||||
# Check if any devices are using this device type
|
||||
cursor.execute('SELECT COUNT(*) FROM Device WHERE device_type_id = %s', (device_type_id,))
|
||||
device_count = cursor.fetchone()[0]
|
||||
if device_count > 0:
|
||||
cursor.execute('SELECT name FROM DeviceType WHERE id = %s', (device_type_id,))
|
||||
device_type_name = cursor.fetchone()[0]
|
||||
error = f"Cannot delete device type '{device_type_name}' because {device_count} device(s) are using it."
|
||||
else:
|
||||
cursor.execute('SELECT name FROM DeviceType WHERE id = %s', (device_type_id,))
|
||||
device_type_name = cursor.fetchone()[0]
|
||||
cursor.execute('DELETE FROM DeviceType WHERE id = %s', (device_type_id,))
|
||||
conn.commit()
|
||||
logging.info(f"User {user_name} deleted device type '{device_type_name}'.")
|
||||
cursor.execute('SELECT id, name, icon_class FROM DeviceType ORDER BY name')
|
||||
device_types = cursor.fetchall()
|
||||
return render_with_user('device_types.html', device_types=device_types, error=error)
|
||||
|
||||
@app.route('/devices/type/<device_type>')
|
||||
@login_required
|
||||
def devices_by_type(device_type):
|
||||
@@ -959,6 +1024,7 @@ 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('/device_types', 'device_types', device_types, methods=['GET', 'POST'])
|
||||
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'])
|
||||
|
||||
@@ -4,4 +4,4 @@ echo "Generating CSS..."
|
||||
./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html,./static/js/*.js" --minify
|
||||
|
||||
echo "Starting app..."
|
||||
gunicorn --bind 0.0.0.0:5000 app:app --log-level debug
|
||||
python app.py
|
||||
@@ -0,0 +1,94 @@
|
||||
/* Icon search suggestions styling */
|
||||
.icon-suggestions {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.icon-suggestion-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.icon-suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.icon-suggestion-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark .icon-suggestion-item {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dark .icon-suggestion-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.icon-suggestion-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 1.125rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dark .icon-suggestion-item i {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.icon-suggestion-item span {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .icon-suggestion-item span {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Icon preview styling */
|
||||
.icon-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for suggestions */
|
||||
.icon-suggestions::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.icon-suggestions::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .icon-suggestions::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.icon-suggestions::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .icon-suggestions::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.icon-suggestions::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .icon-suggestions::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Font Awesome icon search functionality
|
||||
// Common Font Awesome icons for device types
|
||||
const fontAwesomeIcons = [
|
||||
// Network & Server
|
||||
'fa-server', 'fa-router', 'fa-network-wired', 'fa-switch', 'fa-hub', 'fa-ethernet',
|
||||
'fa-satellite-dish', 'fa-broadcast-tower', 'fa-tower-cell', 'fa-wifi', 'fa-network',
|
||||
'fa-project-diagram', 'fa-sitemap', 'fa-diagram-project', 'fa-cloud',
|
||||
|
||||
// Security
|
||||
'fa-shield-halved', 'fa-shield', 'fa-shield-alt', 'fa-firewall', 'fa-lock', 'fa-unlock',
|
||||
'fa-key', 'fa-fingerprint', 'fa-user-shield', 'fa-user-lock',
|
||||
|
||||
// Hardware
|
||||
'fa-print', 'fa-boxes-stacked', 'fa-database', 'fa-hard-drive', 'fa-memory', 'fa-microchip',
|
||||
'fa-cpu', 'fa-usb', 'fa-fan', 'fa-battery-full', 'fa-power-off', 'fa-plug', 'fa-bolt',
|
||||
'fa-lightbulb', 'fa-monitor', 'fa-display', 'fa-tv', 'fa-camera', 'fa-video',
|
||||
|
||||
// Computing
|
||||
'fa-laptop', 'fa-desktop', 'fa-tablet', 'fa-mobile-alt', 'fa-phone', 'fa-keyboard',
|
||||
'fa-mouse', 'fa-microphone', 'fa-headphones', 'fa-speaker',
|
||||
|
||||
// Storage & Files
|
||||
'fa-box', 'fa-package', 'fa-archive', 'fa-folder', 'fa-file', 'fa-hdd', 'fa-ssd',
|
||||
'fa-floppy-disk', 'fa-disk', 'fa-save', 'fa-folder-open', 'fa-folder-plus',
|
||||
|
||||
// Data & Analytics
|
||||
'fa-chart-line', 'fa-chart-bar', 'fa-chart-pie', 'fa-graph', 'fa-analytics',
|
||||
'fa-database', 'fa-file-database', 'fa-file-chart-line', 'fa-file-chart-pie',
|
||||
|
||||
// Location & Infrastructure
|
||||
'fa-globe', 'fa-earth', 'fa-map', 'fa-location', 'fa-map-marker', 'fa-building',
|
||||
'fa-warehouse', 'fa-home', 'fa-office', 'fa-industry',
|
||||
|
||||
// Tools & Utilities
|
||||
'fa-robot', 'fa-cog', 'fa-gear', 'fa-wrench', 'fa-tools', 'fa-question',
|
||||
'fa-code', 'fa-terminal', 'fa-console', 'fa-bug', 'fa-bug-slash',
|
||||
|
||||
// Identification
|
||||
'fa-id-card', 'fa-credit-card', 'fa-qrcode', 'fa-barcode', 'fa-rfid',
|
||||
|
||||
// Transport & Logistics
|
||||
'fa-truck', 'fa-shipping-fast', 'fa-conveyor-belt', 'fa-pallet', 'fa-dolly',
|
||||
'fa-cube', 'fa-cubes', 'fa-layer-group', 'fa-stack',
|
||||
|
||||
// UI & Display
|
||||
'fa-th', 'fa-th-large', 'fa-th-list', 'fa-list', 'fa-list-ul', 'fa-list-ol',
|
||||
'fa-table', 'fa-columns', 'fa-grid', 'fa-window-maximize', 'fa-window-restore',
|
||||
'fa-window-minimize', 'fa-window-close', 'fa-expand', 'fa-compress',
|
||||
|
||||
// Actions
|
||||
'fa-sync', 'fa-sync-alt', 'fa-redo', 'fa-undo', 'fa-refresh', 'fa-download',
|
||||
'fa-upload', 'fa-exchange-alt', 'fa-share', 'fa-link', 'fa-unlink', 'fa-chain',
|
||||
'fa-chain-broken', 'fa-arrows-alt', 'fa-arrows', 'fa-move',
|
||||
|
||||
// Time & Calendar
|
||||
'fa-clock', 'fa-hourglass', 'fa-stopwatch', 'fa-timer', 'fa-calendar',
|
||||
'fa-calendar-alt', 'fa-calendar-check', 'fa-calendar-times', 'fa-history',
|
||||
|
||||
// Media
|
||||
'fa-play', 'fa-pause', 'fa-stop', 'fa-step-backward', 'fa-step-forward',
|
||||
'fa-fast-backward', 'fa-fast-forward', 'fa-eject', 'fa-record-vinyl',
|
||||
'fa-compact-disc', 'fa-cd', 'fa-dvd',
|
||||
|
||||
// Users
|
||||
'fa-user-shield', 'fa-user-lock', 'fa-user-secret', 'fa-user-cog', 'fa-user-gear',
|
||||
'fa-user-tie', 'fa-user-ninja', 'fa-users', 'fa-users-cog', 'fa-user-group',
|
||||
'fa-user-friends', 'fa-user-plus', 'fa-user-minus', 'fa-user-times', 'fa-user-check',
|
||||
'fa-user-xmark', 'fa-user-slash'
|
||||
];
|
||||
|
||||
function initIconSearch() {
|
||||
const iconInputs = document.querySelectorAll('.icon-search-input');
|
||||
|
||||
iconInputs.forEach(input => {
|
||||
const container = input.closest('.icon-search-container');
|
||||
const preview = container.querySelector('.icon-preview');
|
||||
const suggestions = container.querySelector('.icon-suggestions');
|
||||
|
||||
if (!preview || !suggestions) return;
|
||||
|
||||
// Initialize preview if input already has a value
|
||||
if (input.value && input.value.trim()) {
|
||||
const iconClass = input.value.trim().startsWith('fa-') ? input.value.trim() : `fa-${input.value.trim()}`;
|
||||
preview.innerHTML = `<i class="fas ${iconClass}"></i>`;
|
||||
preview.classList.remove('hidden');
|
||||
}
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
|
||||
// Update preview
|
||||
if (query) {
|
||||
const iconClass = query.startsWith('fa-') ? query : `fa-${query}`;
|
||||
preview.innerHTML = `<i class="fas ${iconClass}"></i>`;
|
||||
preview.classList.remove('hidden');
|
||||
} else {
|
||||
preview.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Filter and display suggestions
|
||||
if (query.length > 0) {
|
||||
const filtered = fontAwesomeIcons.filter(icon =>
|
||||
icon.includes(query) || icon.replace('fa-', '').includes(query)
|
||||
).slice(0, 10); // Show top 10 matches
|
||||
|
||||
if (filtered.length > 0) {
|
||||
suggestions.innerHTML = filtered.map(icon => `
|
||||
<div class="icon-suggestion-item" data-icon="${icon}">
|
||||
<i class="fas ${icon}"></i>
|
||||
<span>${icon}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
suggestions.classList.remove('hidden');
|
||||
|
||||
// Add click handlers
|
||||
suggestions.querySelectorAll('.icon-suggestion-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
input.value = item.dataset.icon;
|
||||
preview.innerHTML = `<i class="fas ${item.dataset.icon}"></i>`;
|
||||
preview.classList.remove('hidden');
|
||||
suggestions.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
suggestions.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
suggestions.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide suggestions when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!container.contains(e.target)) {
|
||||
suggestions.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview on blur if value exists
|
||||
input.addEventListener('blur', () => {
|
||||
const value = input.value.trim();
|
||||
if (value && preview) {
|
||||
const iconClass = value.startsWith('fa-') ? value : `fa-${value}`;
|
||||
preview.innerHTML = `<i class="fas ${iconClass}"></i>`;
|
||||
preview.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initIconSearch);
|
||||
} else {
|
||||
initIconSearch();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<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 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">Device Stats</h1>
|
||||
<a href="/device_types" class="hidden sm:flex absolute right-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 items-center justify-center rounded-lg px-4 py-2 text-sm"><i class="fas fa-cog mr-2"></i>Manage Types</a>
|
||||
</div>
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 rounded-lg shadow-md p-6">
|
||||
<table class="w-full table-auto">
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Device Type Management</title>
|
||||
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
|
||||
<link href="/static/css/output.css" rel="stylesheet">
|
||||
<link href="/static/css/device_types.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 flex items-center justify-center mx-4">
|
||||
<div class="container py-8 max-w-6xl pt-20">
|
||||
<div class="flex items-center mb-6 relative">
|
||||
<a href="/device_type_stats" 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">Device Type Management</h1>
|
||||
</div>
|
||||
{% if error %}
|
||||
<div class="mb-4 bg-red-200 dark:bg-red-800 text-red-900 dark:text-red-100 p-4 rounded-lg">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="/device_types" method="POST" class="mb-8 bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Device Type</h2>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<input type="text" name="name" placeholder="Device Type Name (e.g., Router, Load Balancer)" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 flex-1" required>
|
||||
<div class="icon-search-container relative flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="icon-preview hidden text-2xl text-gray-600 dark:text-gray-400 flex-shrink-0"></div>
|
||||
<input type="text" name="icon_class" placeholder="Icon Class (e.g., fa-server, fa-router)" class="icon-search-input border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 flex-1" required>
|
||||
</div>
|
||||
<div class="icon-suggestions hidden absolute z-10 w-full mt-1 bg-gray-300 dark:bg-zinc-900 border border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||
</div>
|
||||
</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-4 py-2 rounded-lg whitespace-nowrap">Add Device Type</button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<p><strong>Icon Class Format:</strong> Start typing to see icon suggestions. Use Font Awesome icon classes (e.g., <code>fa-server</code>, <code>fa-router</code>, <code>fa-database</code>).</p>
|
||||
<p class="mt-1">Common icons: <code>fa-server</code>, <code>fa-network-wired</code>, <code>fa-shield-halved</code>, <code>fa-wifi</code>, <code>fa-print</code>, <code>fa-boxes-stacked</code>, <code>fa-question</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h2 class="text-xl font-bold mb-4">Existing Device Types</h2>
|
||||
{% if device_types %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for device_type in device_types %}
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow">
|
||||
<form id="edit-form-{{ device_type[0] }}" action="/device_types" method="POST" class="space-y-4">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="device_type_id" value="{{ device_type[0] }}">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div class="flex items-center justify-center mb-2">
|
||||
<i class="fas {{ device_type[2] }} text-4xl text-gray-700 dark:text-gray-300"></i>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
|
||||
<input type="text" name="name" value="{{ device_type[1] }}" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-full" required>
|
||||
</div>
|
||||
<div class="icon-search-container relative">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Icon</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="icon-preview text-xl text-gray-600 dark:text-gray-400 flex-shrink-0">
|
||||
<i class="fas {{ device_type[2] }}"></i>
|
||||
</div>
|
||||
<input type="text" name="icon_class" value="{{ device_type[2] }}" class="icon-search-input border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 flex-1" required>
|
||||
</div>
|
||||
<div class="icon-suggestions hidden absolute z-10 w-full mt-1 bg-gray-300 dark:bg-zinc-900 border border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" form="edit-form-{{ device_type[0] }}" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-3 py-2 rounded-lg text-sm font-medium transition-colors">
|
||||
<i class="fas fa-save mr-1"></i> Save
|
||||
</button>
|
||||
<form action="/device_types" method="POST" onsubmit="return confirm('Are you sure you want to delete this device type?');" class="inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="device_type_id" value="{{ device_type[0] }}">
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700 hover:cursor-pointer text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors" title="Delete Device Type">
|
||||
<i class="fas fa-trash mr-1"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg text-center text-gray-600 dark:text-gray-400">
|
||||
<p>No device types found. Add your first device type above.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/device_types.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user