Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73a94943cf | |||
| d35873c04f | |||
| f93fa155eb | |||
| d68eefcf0c | |||
| efd44bf968 | |||
| 4f226474c2 | |||
| de123fafd4 |
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "1.1.0"
|
".": "1.2.0"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.2.0](https://github.com/JDB-NET/ipam/compare/v1.1.1...v1.2.0) (2025-11-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* :sparkles: added the ability to create/edit/remove device types ([d68eefc](https://github.com/JDB-NET/ipam/commit/d68eefcf0cc4a59cda9cedb3e126d974ee45d2ad))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* :bug: missing button classes ([f93fa15](https://github.com/JDB-NET/ipam/commit/f93fa155eb5d6c9ff4ed19f332c3ad6fff328d31))
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/JDB-NET/ipam/compare/v1.1.0...v1.1.1) (2025-11-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* :bug: image name ([de123fa](https://github.com/JDB-NET/ipam/commit/de123fafd40d97ea6e545bd8dd1d3a812e2a709f))
|
||||||
|
|
||||||
## [1.1.0](https://github.com/JDB-NET/ipam/compare/v1.0.0...v1.1.0) (2025-11-01)
|
## [1.1.0](https://github.com/JDB-NET/ipam/compare/v1.0.0...v1.1.0) (2025-11-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,6 @@ def inject_env_vars():
|
|||||||
|
|
||||||
register_routes(app)
|
register_routes(app)
|
||||||
init_db(app)
|
init_db(app)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
+1
-1
@@ -15,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: ipam
|
- name: ipam
|
||||||
image: docker.jdbnet.co.uk/public/ipam:latest
|
image: ghcr.io/jdb-net/ipam:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5000
|
- containerPort: 5000
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import os
|
|||||||
import csv
|
import csv
|
||||||
from io import StringIO, BytesIO
|
from io import StringIO, BytesIO
|
||||||
import logging
|
import logging
|
||||||
|
import mysql.connector
|
||||||
|
|
||||||
app = None
|
app = None
|
||||||
|
|
||||||
@@ -601,6 +602,70 @@ def register_routes(app):
|
|||||||
stats = cursor.fetchall()
|
stats = cursor.fetchall()
|
||||||
return render_with_user('device_type_stats.html', stats=stats)
|
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>')
|
@app.route('/devices/type/<device_type>')
|
||||||
@login_required
|
@login_required
|
||||||
def devices_by_type(device_type):
|
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>/export_csv', 'export_subnet_csv', export_subnet_csv)
|
||||||
app.add_url_rule('/subnet/<int:subnet_id>/dhcp', 'dhcp_pool', dhcp_pool, methods=['GET', 'POST'])
|
app.add_url_rule('/subnet/<int:subnet_id>/dhcp', 'dhcp_pool', dhcp_pool, methods=['GET', 'POST'])
|
||||||
app.add_url_rule('/device_type_stats', 'device_type_stats', device_type_stats)
|
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('/devices/type/<device_type>', 'devices_by_type', devices_by_type)
|
||||||
app.add_url_rule('/racks', 'racks', racks)
|
app.add_url_rule('/racks', 'racks', racks)
|
||||||
app.add_url_rule('/rack/add', 'add_rack', add_rack, methods=['GET', 'POST'])
|
app.add_url_rule('/rack/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
|
./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html,./static/js/*.js" --minify
|
||||||
|
|
||||||
echo "Starting app..."
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<option value="{{ dtype[0] }}">{{ dtype[1] }}</option>
|
<option value="{{ dtype[0] }}">{{ dtype[1] }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add Device</button>
|
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg">Add Device</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<label for="height_u" class="block font-medium mb-1">Height (U)</label>
|
<label for="height_u" class="block font-medium mb-1">Height (U)</label>
|
||||||
<input type="number" id="height_u" name="height_u" min="1" max="60" class="p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border border-gray-600 w-full" required>
|
<input type="number" id="height_u" name="height_u" min="1" max="60" class="p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border border-gray-600 w-full" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg w-full">Add Rack</button>
|
<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 w-full">Add Rack</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<input type="text" name="name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
<input type="text" name="name" placeholder="Subnet Name" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
||||||
<input type="text" name="cidr" id="cidr-input" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
<input type="text" name="cidr" id="cidr-input" placeholder="CIDR (e.g., 192.168.1.0/24)" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
||||||
<input type="text" name="site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
<input type="text" name="site" placeholder="Site/Location" class="border p-3 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600" required>
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add Subnet</button>
|
<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">Add Subnet</button>
|
||||||
<span id="cidr-error" class="text-red-500 text-sm hidden"></span>
|
<span id="cidr-error" class="text-red-500 text-sm hidden"></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<option value="{{ subnet.id }}">{{ subnet.name }} ({{ subnet.cidr }})</option>
|
<option value="{{ subnet.id }}">{{ subnet.name }} ({{ subnet.cidr }})</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="text-red-500 hover:text-red-700 rounded-full p-3" title="Delete Subnet">
|
<button type="submit" class="text-red-500 hover:text-red-700 hover:cursor-pointer rounded-full p-3" title="Delete Subnet">
|
||||||
<i class="fas fa-trash fa-lg"></i>
|
<i class="fas fa-trash fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<option value="{{ device[0] }}" {% if request.args.get('device_name') == device[0] %}selected{% endif %}>{{ device[0] }}</option>
|
<option value="{{ device[0] }}" {% if request.args.get('device_name') == device[0] %}selected{% endif %}>{{ device[0] }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg flex items-center gap-2">
|
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg flex items-center gap-2">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<span>Filter</span>
|
<span>Filter</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
<form action="/rename_device" method="POST" class="inline">
|
<form action="/rename_device" method="POST" class="inline">
|
||||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
<input type="text" name="new_name" value="{{ device.name }}" class="hidden border p-1 rounded bg-gray-200 dark:bg-zinc-800 border-gray-600 w-32 mr-2" style="vertical-align: middle;" required>
|
<input type="text" name="new_name" value="{{ device.name }}" class="hidden border p-1 rounded bg-gray-200 dark:bg-zinc-800 border-gray-600 w-32 mr-2" style="vertical-align: middle;" required>
|
||||||
<button type="button" class="text-blue-400 hover:text-blue-600 ml-2 rename-btn" title="Rename Device"><i class="fas fa-pencil-alt"></i></button>
|
<button type="button" class="text-blue-400 hover:text-blue-600 hover:cursor-pointer ml-2 rename-btn" title="Rename Device"><i class="fas fa-pencil-alt"></i></button>
|
||||||
<button type="submit" class="text-green-400 hover:text-green-600 ml-2 save-btn hidden" title="Save Name"><i class="fas fa-check"></i></button>
|
<button type="submit" class="text-green-400 hover:text-green-600 hover:cursor-pointer ml-2 save-btn hidden" title="Save Name"><i class="fas fa-check"></i></button>
|
||||||
<button type="button" class="text-gray-400 hover:text-gray-600 ml-2 cancel-btn hidden" title="Cancel"><i class="fas fa-times"></i></button>
|
<button type="button" class="text-gray-400 hover:text-gray-600 hover:cursor-pointer ml-2 cancel-btn hidden" title="Cancel"><i class="fas fa-times"></i></button>
|
||||||
</form>
|
</form>
|
||||||
<form action="/delete_device" method="POST" onsubmit="return confirm('Are you sure you want to delete this device?');">
|
<form action="/delete_device" method="POST" onsubmit="return confirm('Are you sure you want to delete this device?');">
|
||||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
<button type="submit" class="ml-4 text-red-500 hover:text-red-700" title="Delete Device">
|
<button type="submit" class="ml-4 text-red-500 hover:text-red-700 hover:cursor-pointer" title="Delete Device">
|
||||||
<i class="fas fa-trash fa-lg"></i>
|
<i class="fas fa-trash fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<select name="ip_id" id="ip-select" class="border p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full" required>
|
<select name="ip_id" id="ip-select" class="border p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full" required>
|
||||||
<option value="" disabled selected>Select IP...</option>
|
<option value="" disabled selected>Select IP...</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg w-full">Add IP</button>
|
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg w-full">Add IP</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="allocated-ips">
|
<div class="allocated-ips">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<span class="allocated-ip">{{ ip.ip }}</span>
|
<span class="allocated-ip">{{ ip.ip }}</span>
|
||||||
<form action="/device/{{ device.id }}/delete_ip" method="POST" class="inline">
|
<form action="/device/{{ device.id }}/delete_ip" method="POST" class="inline">
|
||||||
<input type="hidden" name="device_ip_id" value="{{ ip.device_ip_id }}">
|
<input type="hidden" name="device_ip_id" value="{{ ip.device_ip_id }}">
|
||||||
<button type="submit" class="text-red-500 hover:text-red-600 py-1 mr-2 text-lg"><i class="fas fa-trash"></i></button>
|
<button type="submit" class="text-red-500 hover:text-red-600 hover:cursor-pointer py-1 mr-2 text-lg"><i class="fas fa-trash"></i></button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
<label for="description" class="block mb-2 text-lg font-bold">Description</label>
|
<label for="description" class="block mb-2 text-lg font-bold">Description</label>
|
||||||
<textarea id="description" name="description" rows="3" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full resize-y" placeholder="Enter device description...">{{ device.description or '' }}</textarea>
|
<textarea id="description" name="description" rows="3" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600 w-full resize-y" placeholder="Enter device description...">{{ device.description or '' }}</textarea>
|
||||||
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg w-full mt-2">Save Description</button>
|
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg w-full mt-2">Save Description</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<div class="flex items-center mb-6 relative">
|
<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>
|
<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>
|
<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>
|
||||||
<div class="bg-gray-200 dark:bg-zinc-800 rounded-lg shadow-md p-6">
|
<div class="bg-gray-200 dark:bg-zinc-800 rounded-lg shadow-md p-6">
|
||||||
<table class="w-full table-auto">
|
<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>
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="site-group bg-gray-200 dark:bg-zinc-800 rounded-lg shadow-md">
|
<div class="site-group bg-gray-200 dark:bg-zinc-800 rounded-lg shadow-md">
|
||||||
<div class="flex flex-row items-center justify-between p-4 cursor-pointer site-header">
|
<div class="flex flex-row items-center justify-between p-4 cursor-pointer site-header">
|
||||||
<h2 class="text-xl font-bold mb-0 dark:text-white">{{ site }}</h2>
|
<h2 class="text-xl font-bold mb-0 dark:text-white">{{ site }}</h2>
|
||||||
<button type="button" class="expand-btn text-gray-400 hover:text-gray-200 ml-2 flex items-center" aria-label="Expand site">
|
<button type="button" class="expand-btn text-gray-400 hover:text-gray-200 hover:cursor-pointer ml-2 flex items-center" aria-label="Expand site">
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -27,9 +27,9 @@
|
|||||||
<label for="excluded_ips" class="font-medium">Exclude IPs (comma separated)</label>
|
<label for="excluded_ips" class="font-medium">Exclude IPs (comma separated)</label>
|
||||||
<input type="text" id="excluded_ips" name="excluded_ips" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" placeholder="e.g. 192.168.1.105,192.168.1.110" value="{{ dhcp_pool.excluded_ips if dhcp_pool and dhcp_pool.excluded_ips else '' }}">
|
<input type="text" id="excluded_ips" name="excluded_ips" class="border p-3 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600" placeholder="e.g. 192.168.1.105,192.168.1.110" value="{{ dhcp_pool.excluded_ips if dhcp_pool and dhcp_pool.excluded_ips else '' }}">
|
||||||
<div class="flex gap-4 mt-4">
|
<div class="flex gap-4 mt-4">
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Save DHCP Pool</button>
|
<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">Save DHCP Pool</button>
|
||||||
{% if dhcp_pool %}
|
{% if dhcp_pool %}
|
||||||
<button type="submit" name="remove" value="1" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Remove DHCP Pool</button>
|
<button type="submit" name="remove" value="1" 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">Remove DHCP Pool</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium">Logout</a>
|
<a href="/logout" class="text-gray-200 hover:text-gray-400 font-medium">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<button class="md:hidden flex items-center text-gray-200 focus:outline-none" id="nav-toggle" aria-label="Open navigation menu">
|
<button class="md:hidden flex items-center text-gray-200 hover:cursor-pointer focus:outline-none" id="nav-toggle" aria-label="Open navigation menu">
|
||||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<li class="site-group bg-gray-200 dark:bg-zinc-800 rounded-xl shadow-lg">
|
<li class="site-group bg-gray-200 dark:bg-zinc-800 rounded-xl shadow-lg">
|
||||||
<div class="flex flex-row items-center justify-between p-4 cursor-pointer site-header">
|
<div class="flex flex-row items-center justify-between p-4 cursor-pointer site-header">
|
||||||
<h2 class="text-xl font-bold mb-0 text-gray-900 dark:text-white">{{ site }}</h2>
|
<h2 class="text-xl font-bold mb-0 text-gray-900 dark:text-white">{{ site }}</h2>
|
||||||
<button type="button" class="expand-btn ml-2 flex items-center" aria-label="Expand site">
|
<button type="button" class="expand-btn ml-2 flex items-center hover:cursor-pointer" aria-label="Expand site">
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<p class="text-gray-900 dark:text-white text-lg font-medium">{{ subnet.name }}</p>
|
<p class="text-gray-900 dark:text-white text-lg font-medium">{{ subnet.name }}</p>
|
||||||
<p class="text-sm text-gray-800 dark:text-gray-400">{{ subnet.cidr }}</p>
|
<p class="text-sm text-gray-800 dark:text-gray-400">{{ subnet.cidr }}</p>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="export-csv-btn ml-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-600 dark:hover:bg-zinc-500 flex items-center justify-center rounded-full w-9 h-9" title="Export as CSV" data-subnet-id="{{ subnet.id }}">
|
<button type="button" class="export-csv-btn ml-2 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-600 dark:hover:bg-zinc-500 hover:cursor-pointer flex items-center justify-center rounded-full w-9 h-9" title="Export as CSV" data-subnet-id="{{ subnet.id }}">
|
||||||
<i class="fas fa-file-csv"></i>
|
<i class="fas fa-file-csv"></i>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<form action="/login" method="POST" class="flex flex-col space-y-4">
|
<form action="/login" method="POST" class="flex flex-col space-y-4">
|
||||||
<input type="email" name="email" placeholder="Email Address" class="p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border border-gray-600" required>
|
<input type="email" name="email" placeholder="Email Address" class="p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border border-gray-600" required>
|
||||||
<input type="password" name="password" placeholder="Password" class="p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border border-gray-600" required>
|
<input type="password" name="password" placeholder="Password" class="p-3 rounded-lg bg-gray-200 dark:bg-zinc-800 border border-gray-600" required>
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg flex items-center gap-2 justify-center w-full">
|
<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 flex items-center gap-2 justify-center w-full">
|
||||||
<i class="fas fa-sign-in-alt"></i>
|
<i class="fas fa-sign-in-alt"></i>
|
||||||
<span>Login</span>
|
<span>Login</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+9
-9
@@ -17,11 +17,11 @@
|
|||||||
<h1 class="text-3xl font-bold text-center w-full mb-0">{{ rack.name }}</h1>
|
<h1 class="text-3xl font-bold text-center w-full mb-0">{{ rack.name }}</h1>
|
||||||
<span class="text-base mt-1">({{ rack.height_u }}U, {{ rack.site }})</span>
|
<span class="text-base mt-1">({{ rack.height_u }}U, {{ rack.site }})</span>
|
||||||
<form action="/rack/{{ rack.id }}/delete" method="POST" onsubmit="return confirm('Delete this rack?');" class="hidden sm:flex absolute right-0 mr-14">
|
<form action="/rack/{{ rack.id }}/delete" method="POST" onsubmit="return confirm('Delete this rack?');" class="hidden sm:flex absolute right-0 mr-14">
|
||||||
<button type="submit" class="bg-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 flex" title="Delete Rack">
|
<button type="submit" class="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 flex" title="Delete Rack">
|
||||||
<i class="fas fa-times fa-lg"></i>
|
<i class="fas fa-times fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<button type="button" id="export-csv" 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-full w-11 h-11 export-csv-btn" title="Export as CSV" data-rack-id="{{ rack.id }}">
|
<button type="button" id="export-csv" class="hidden sm:flex absolute right-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 export-csv-btn" title="Export as CSV" data-rack-id="{{ rack.id }}">
|
||||||
<i class="fas fa-file-csv fa-lg"></i>
|
<i class="fas fa-file-csv fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<script>
|
<script>
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
<a href="?side=back" id="back-btn" class="rack-side-btn px-4 py-2 rounded-lg font-semibold bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 {% if current_side == 'back' %}ring-2 ring-gray-400{% endif %}">Back</a>
|
<a href="?side=back" id="back-btn" class="rack-side-btn px-4 py-2 rounded-lg font-semibold bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 {% if current_side == 'back' %}ring-2 ring-gray-400{% endif %}">Back</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-4 w-full justify-center">
|
<div class="flex flex-wrap gap-4 w-full justify-center">
|
||||||
<button id="show-add-device-form" type="button" class="flex-1 min-w-[12rem] max-w-[16rem] bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg flex-shrink-0 whitespace-nowrap"> <i class="fas fa-plus"></i> Add Device</button>
|
<button id="show-add-device-form" type="button" class="flex-1 min-w-[12rem] max-w-[16rem] bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg flex-shrink-0 whitespace-nowrap"> <i class="fas fa-plus"></i> Add Device</button>
|
||||||
<button id="show-nonnet-form" type="button" class="flex-[1.5_1.5_0%] min-w-[16rem] max-w-[28rem] bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg flex-shrink-0 whitespace-nowrap"> <i class="fas fa-plus"></i> Add Non-Networked Device</button>
|
<button id="show-nonnet-form" type="button" class="flex-[1.5_1.5_0%] min-w-[16rem] max-w-[28rem] bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg flex-shrink-0 whitespace-nowrap"> <i class="fas fa-plus"></i> Add Non-Networked Device</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form id="add-device-form" action="/rack/{{ rack.id }}/add_device" method="POST" class="hidden mb-6 bg-gray-200 dark:bg-zinc-800 rounded-lg p-4">
|
<form id="add-device-form" action="/rack/{{ rack.id }}/add_device" method="POST" class="hidden mb-6 bg-gray-200 dark:bg-zinc-800 rounded-lg p-4">
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
<option value="front">Front</option>
|
<option value="front">Front</option>
|
||||||
<option value="back">Back</option>
|
<option value="back">Back</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="w-full md:w-auto bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add Device</button>
|
<button type="submit" class="w-full md:w-auto 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">Add Device</button>
|
||||||
<button id="hide-add-device-form" type="button" class="w-full md:w-auto bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg md:ml-0.5 mt-2 md:mt-0 flex-shrink-0"><i class="fas fa-times"></i></button>
|
<button id="hide-add-device-form" type="button" class="w-full md:w-auto 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 md:ml-0.5 mt-2 md:mt-0 flex-shrink-0"><i class="fas fa-times"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs dark:text-gray-400">To add a multi-U device, repeat for each U position.</div>
|
<div class="text-xs dark:text-gray-400">To add a multi-U device, repeat for each U position.</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
<option value="front">Front</option>
|
<option value="front">Front</option>
|
||||||
<option value="back">Back</option>
|
<option value="back">Back</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="w-full md:w-auto bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add</button>
|
<button type="submit" class="w-full md:w-auto 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">Add</button>
|
||||||
<button id="hide-nonnet-form" type="button" class="w-full md:w-auto bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg md:ml-0.5 mt-2 md:mt-0 flex-shrink-0"><i class="fas fa-times"></i></button>
|
<button id="hide-nonnet-form" type="button" class="w-full md:w-auto 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 md:ml-0.5 mt-2 md:mt-0 flex-shrink-0"><i class="fas fa-times"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs dark:text-gray-400 mt-2">Add a non-networked device.</div>
|
<div class="text-xs dark:text-gray-400 mt-2">Add a non-networked device.</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="/rack/{{ rack.id }}/remove_device" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to remove this device from the rack?');">
|
<form action="/rack/{{ rack.id }}/remove_device" method="POST" style="display:inline" onsubmit="return confirm('Are you sure you want to remove this device from the rack?');">
|
||||||
<input type="hidden" name="rack_device_id" value="{{ rd.id }}">
|
<input type="hidden" name="rack_device_id" value="{{ rd.id }}">
|
||||||
<button type="submit" class="ml-3 text-red-400 hover:text-red-600"><i class="fas fa-times"></i></button>
|
<button type="submit" class="ml-3 text-red-400 hover:text-red-600 hover:cursor-pointer"><i class="fas fa-times"></i></button>
|
||||||
</form>
|
</form>
|
||||||
{% set found = true %}
|
{% set found = true %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="flex items-center mb-6 relative">
|
<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="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>
|
||||||
<h1 class="text-3xl font-bold text-center w-full">{{ subnet.name }} ({{ subnet.cidr }})</h1>
|
<h1 class="text-3xl font-bold text-center w-full">{{ subnet.name }} ({{ subnet.cidr }})</h1>
|
||||||
<button type="button" id="export-csv" 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-full w-11 h-11 export-csv-btn" title="Export as CSV" data-subnet-id="{{ subnet.id }}">
|
<button type="button" id="export-csv" class="hidden sm:flex absolute right-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 export-csv-btn" title="Export as CSV" data-subnet-id="{{ subnet.id }}">
|
||||||
<i class="fas fa-file-csv fa-lg"></i>
|
<i class="fas fa-file-csv fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<i class="fas fa-network-wired"></i> Define DHCP Pool
|
<i class="fas fa-network-wired"></i> Define DHCP Pool
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button id="toggle-desc" class="sm:hidden bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg mb-4 w-full">Show Descriptions</button>
|
<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>
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<table class="table-auto w-full mb-6">
|
<table class="table-auto w-full mb-6">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<input type="text" name="name" placeholder="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="name" placeholder="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="email" name="email" placeholder="Email Address" 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="email" name="email" placeholder="Email Address" 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="password" name="password" placeholder="Password" 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="password" name="password" placeholder="Password" 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>
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg max-w-md w-full sm:w-auto">Add User</button>
|
<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 max-w-md w-full sm:w-auto">Add User</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<h2 class="text-xl font-bold mb-4">Existing Users</h2>
|
<h2 class="text-xl font-bold mb-4">Existing Users</h2>
|
||||||
@@ -32,12 +32,12 @@
|
|||||||
<input type="text" name="name" value="{{ user[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-52">
|
<input type="text" name="name" value="{{ user[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-52">
|
||||||
<input type="email" name="email" value="{{ user[2] }}" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-80">
|
<input type="email" name="email" value="{{ user[2] }}" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-80">
|
||||||
<input type="password" name="password" placeholder="New Password (leave blank to keep)" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-80">
|
<input type="password" name="password" placeholder="New Password (leave blank to keep)" class="border p-2 rounded-lg bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 border-gray-600 w-80">
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-3 py-1 rounded-lg">Save</button>
|
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-3 py-1 rounded-lg">Save</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="/users" method="POST" onsubmit="return confirm('Are you sure you want to delete this user?');">
|
<form action="/users" method="POST" onsubmit="return confirm('Are you sure you want to delete this user?');">
|
||||||
<input type="hidden" name="action" value="delete">
|
<input type="hidden" name="action" value="delete">
|
||||||
<input type="hidden" name="user_id" value="{{ user[0] }}">
|
<input type="hidden" name="user_id" value="{{ user[0] }}">
|
||||||
<button type="submit" class="text-red-500 hover:text-red-700 mx-4" title="Delete User"><i class="fas fa-trash"></i></button>
|
<button type="submit" class="text-red-500 hover:text-red-700 hover:cursor-pointer mx-4" title="Delete User"><i class="fas fa-trash"></i></button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user