Add complete project structure and all files
This commit is contained in:
232
backend/app/templates/ipam_networks.html
Normal file
232
backend/app/templates/ipam_networks.html
Normal file
@@ -0,0 +1,232 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}IPAM - {{ t('network_label') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h1 style="font-size: 1.75rem; font-weight: 600; margin: 0;"><i class="fa-solid fa-globe"></i> {{ t('ipam_networks') }}</h1>
|
||||
<p style="color: var(--text-secondary); margin-top: 0.25rem;">{{ t('ip_networks_management') }}</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<a href="/ipam" class="btn" style="background: var(--bg-tertiary); color: var(--text-primary);">← {{ t('back') }}</a>
|
||||
<button onclick="showAddNetworkModal()" class="btn btn-success" data-permission="ipam.manage"><i class="fa-solid fa-plus"></i> {{ t('add_network') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Networks List -->
|
||||
<div class="card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="margin: 0;"><i class="fa-solid fa-list"></i> {{ t('all_networks') }}</h3>
|
||||
<button onclick="loadNetworks()" class="btn btn-sm" style="background: var(--bg-tertiary); color: var(--text-primary);"><i class="fa-solid fa-rotate"></i> {{ t('refresh') }}</button>
|
||||
</div>
|
||||
|
||||
<div id="networksContainer">
|
||||
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
||||
<i class="fa-solid fa-spinner fa-spin"></i> {{ t('loading') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Network Modal -->
|
||||
<div id="addNetworkModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); z-index: 2000; align-items: center; justify-content: center; padding: 1rem;">
|
||||
<div class="card" style="max-width: 600px; width: 95%; max-height: 90vh; overflow-y: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h3 style="margin: 0;"><i class="fa-solid fa-plus"></i> {{ t('add_network') }}</h3>
|
||||
<button onclick="closeAddNetworkModal()" style="background: none; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer;">×</button>
|
||||
</div>
|
||||
|
||||
<form id="addNetworkForm" onsubmit="createNetwork(event)">
|
||||
<div style="display: grid; gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('title') }} {{ t('required') }}</label>
|
||||
<input type="text" id="networkName" required placeholder="Production Network" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('network_cidr') }} {{ t('required') }}</label>
|
||||
<input type="text" id="networkCIDR" required placeholder="10.10.10.0/24" pattern="^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('gateway') }}</label>
|
||||
<input type="text" id="networkGateway" placeholder="10.10.10.1" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('vlan_id') }}</label>
|
||||
<input type="number" id="networkVLAN" min="1" max="4094" placeholder="100" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('proxmox_bridge') }}</label>
|
||||
<input type="text" id="networkBridge" placeholder="vmbr0" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary);">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">{{ t('description') }}</label>
|
||||
<textarea id="networkDescription" rows="2" placeholder="{{ t('description_placeholder') }}" style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); resize: vertical;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color);">
|
||||
<button type="button" onclick="closeAddNetworkModal()" class="btn" style="background: var(--bg-tertiary); color: var(--text-primary);">{{ t('cancel') }}</button>
|
||||
<button type="submit" class="btn btn-success"><i class="fa-solid fa-floppy-disk"></i> {{ t('create') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadNetworks() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/ipam/api/networks');
|
||||
const container = document.getElementById('networksContainer');
|
||||
|
||||
if (response.ok) {
|
||||
const networks = await response.json();
|
||||
|
||||
if (networks.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 3rem;">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;"><i class="fa-solid fa-globe"></i></div>
|
||||
<h3 style="color: var(--text-primary); margin-bottom: 0.5rem;">${t('networks_not_found')}</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">${t('add_first_network')}</p>
|
||||
<button onclick="showAddNetworkModal()" class="btn btn-success"><i class="fa-solid fa-plus"></i> ${t('add_network')}</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<th style="text-align: left; padding: 0.75rem; color: var(--text-secondary); font-weight: 500;">${t('network_label')}</th>
|
||||
<th style="text-align: left; padding: 0.75rem; color: var(--text-secondary); font-weight: 500;">${t('cidr')}</th>
|
||||
<th style="text-align: left; padding: 0.75rem; color: var(--text-secondary); font-weight: 500;">${t('gateway')}</th>
|
||||
<th style="text-align: left; padding: 0.75rem; color: var(--text-secondary); font-weight: 500;">${t('vlan')}</th>
|
||||
<th style="text-align: left; padding: 0.75rem; color: var(--text-secondary); font-weight: 500;">${t('usage')}</th>
|
||||
<th style="text-align: right; padding: 0.75rem; color: var(--text-secondary); font-weight: 500;">${t('actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${networks.map(net => `
|
||||
<tr style="border-bottom: 1px solid var(--border-color);" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
||||
<td style="padding: 0.75rem;">
|
||||
<a href="/ipam/network/${net.id}" style="color: var(--accent); text-decoration: none; font-weight: 500;">
|
||||
${net.name}
|
||||
</a>
|
||||
${net.description ? `<div style="font-size: 0.75rem; color: var(--text-muted);">${net.description}</div>` : ''}
|
||||
</td>
|
||||
<td style="padding: 0.75rem; font-family: monospace; color: var(--text-primary);">${net.network}</td>
|
||||
<td style="padding: 0.75rem; font-family: monospace; color: var(--text-secondary);">${net.gateway || '-'}</td>
|
||||
<td style="padding: 0.75rem; color: var(--text-primary);">${net.vlan_id || '-'}</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<div style="flex: 1; height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; max-width: 100px;">
|
||||
<div style="height: 100%; background: ${getUtilizationColor(net.utilization_percent)}; width: ${net.utilization_percent || 0}%;"></div>
|
||||
</div>
|
||||
<span style="font-size: 0.875rem; color: var(--text-secondary);">${net.used_ips || 0}/${net.total_ips || 0}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding: 0.75rem; text-align: right;">
|
||||
<a href="/ipam/network/${net.id}" class="btn btn-sm" style="background: var(--bg-tertiary); color: var(--text-primary);"><i class="fa-solid fa-eye"></i> ${t('open')}</a>
|
||||
<button onclick="deleteNetwork(${net.id}, '${net.name}')" class="btn btn-sm btn-danger" style="margin-left: 0.25rem;"><i class="fa-solid fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `<div style="text-align: center; padding: 2rem; color: var(--danger);"><i class="fa-solid fa-circle-xmark"></i> ${t('error_loading_networks')}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading networks:', error);
|
||||
document.getElementById('networksContainer').innerHTML = `<div style="text-align: center; padding: 2rem; color: var(--danger);"><i class="fa-solid fa-circle-xmark"></i> ${t('error_loading')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getUtilizationColor(percent) {
|
||||
if (percent >= 90) return 'var(--danger)';
|
||||
if (percent >= 70) return 'var(--warning)';
|
||||
if (percent >= 50) return 'var(--warning)';
|
||||
return 'var(--success)';
|
||||
}
|
||||
|
||||
function showAddNetworkModal() {
|
||||
document.getElementById('addNetworkModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAddNetworkModal() {
|
||||
document.getElementById('addNetworkModal').style.display = 'none';
|
||||
document.getElementById('addNetworkForm').reset();
|
||||
}
|
||||
|
||||
async function createNetwork(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const data = {
|
||||
name: document.getElementById('networkName').value,
|
||||
network: document.getElementById('networkCIDR').value,
|
||||
gateway: document.getElementById('networkGateway').value || null,
|
||||
vlan_id: document.getElementById('networkVLAN').value ? parseInt(document.getElementById('networkVLAN').value) : null,
|
||||
proxmox_bridge: document.getElementById('networkBridge').value || null,
|
||||
description: document.getElementById('networkDescription').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/ipam/api/networks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
closeAddNetworkModal();
|
||||
loadNetworks();
|
||||
showToast('success', t('success'), t('network_created_successfully') || 'Network created successfully');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast('error', t('error'), error.detail || t('error_creating_network'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating network:', error);
|
||||
showToast('error', t('error'), t('error_creating_network'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNetwork(id, name) {
|
||||
const confirmed = await showDeleteConfirm(
|
||||
t('delete_network'),
|
||||
`${t('delete_network_confirm')} "${name}"? ${t('all_related_data_lost')}`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/ipam/api/networks/${id}?force=true`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadNetworks();
|
||||
showToast('success', t('success'), t('network_deleted_successfully') || 'Network deleted successfully');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast('error', t('error'), error.detail || t('error_deleting_network'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting network:', error);
|
||||
showToast('error', t('error'), t('error_deleting_network'));
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load after DOM ready
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(loadNetworks, 100);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user