206 lines
No EOL
5.8 KiB
HTML
206 lines
No EOL
5.8 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="container-xl">
|
|
<div class="page-header d-print-none">
|
|
<div class="row align-items-center">
|
|
<div class="col">
|
|
<h2 class="page-title">
|
|
IP Visualization - {{ subnet.cidr }}
|
|
</h2>
|
|
<div class="text-muted mt-1">{{ subnet.location }}</div>
|
|
</div>
|
|
<div class="col-auto ms-auto d-print-none">
|
|
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="btn btn-link">
|
|
Back to Subnet
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h3 class="card-title">IP Address Map</h3>
|
|
<div>
|
|
<span class="badge bg-success me-2">Available: {{ total_ips - used_ip_count }}</span>
|
|
<span class="badge bg-danger">Used: {{ used_ip_count }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="ip-grid">
|
|
{% set network_parts = subnet.cidr.split('/')[0].split('.') %}
|
|
{% set network_prefix = network_parts[0] + '.' + network_parts[1] + '.' + network_parts[2] + '.' %}
|
|
|
|
{% for i in range(1, 255) %}
|
|
{% set ip = network_prefix + i|string %}
|
|
{% if ip in used_ips %}
|
|
<div class="ip-cell used" title="{{ used_ips[ip] }}">
|
|
{{ ip }}
|
|
</div>
|
|
{% else %}
|
|
<div class="ip-cell available" title="Available">
|
|
{{ ip }}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
loadIpMap();
|
|
|
|
// Handle scan button response
|
|
document.body.addEventListener('htmx:afterRequest', function (event) {
|
|
if (event.detail.target.matches('button[hx-post^="/ipam/subnet/"][hx-post$="/scan"]')) {
|
|
if (event.detail.successful) {
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast align-items-center show position-fixed bottom-0 end-0 m-3';
|
|
toast.setAttribute('role', 'alert');
|
|
toast.setAttribute('aria-live', 'assertive');
|
|
toast.setAttribute('aria-atomic', 'true');
|
|
|
|
toast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
Subnet scan started. Results will be available shortly.
|
|
</div>
|
|
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// Auto-remove toast after 5 seconds
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 5000);
|
|
|
|
// Reload IP map after a delay to allow scan to complete
|
|
setTimeout(() => {
|
|
loadIpMap();
|
|
}, 3000);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
function loadIpMap() {
|
|
const ipMap = document.getElementById('ip-map');
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
|
fetch('/ipam/subnet/{{ subnet.id }}/visualization')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Hide loading indicator
|
|
loadingIndicator.style.display = 'none';
|
|
|
|
// Create IP grid
|
|
const grid = document.createElement('div');
|
|
grid.className = 'ip-grid';
|
|
|
|
// Add each IP address to the grid
|
|
data.forEach(ip => {
|
|
const cell = document.createElement('div');
|
|
cell.className = `ip-cell ${ip.status}`;
|
|
|
|
// Add IP address
|
|
const ipAddress = document.createElement('span');
|
|
ipAddress.className = 'ip-address';
|
|
ipAddress.textContent = ip.ip;
|
|
cell.appendChild(ipAddress);
|
|
|
|
// Add hostname if exists
|
|
if (ip.hostname) {
|
|
const hostname = document.createElement('span');
|
|
hostname.className = 'ip-hostname';
|
|
hostname.textContent = ip.hostname;
|
|
cell.appendChild(hostname);
|
|
}
|
|
|
|
// Add click handler for details
|
|
cell.addEventListener('click', () => {
|
|
if (ip.status === 'used') {
|
|
window.location.href = `/dashboard/server/${ip.server_id}`;
|
|
}
|
|
});
|
|
|
|
grid.appendChild(cell);
|
|
});
|
|
|
|
ipMap.appendChild(grid);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading subnet visualization:', error);
|
|
document.getElementById('ip-map').innerHTML =
|
|
'<div class="alert alert-danger">Error loading subnet visualization. Please try again later.</div>';
|
|
document.getElementById('loading-indicator').style.display = 'none';
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.ip-grid-container {
|
|
min-height: 400px;
|
|
position: relative;
|
|
}
|
|
|
|
.ip-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
|
gap: 4px;
|
|
}
|
|
|
|
.ip-cell {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
height: 50px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.ip-cell:hover {
|
|
transform: scale(1.05);
|
|
z-index: 10;
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.ip-cell.used {
|
|
background-color: rgba(32, 107, 196, 0.1);
|
|
border-color: rgba(32, 107, 196, 0.5);
|
|
}
|
|
|
|
.ip-cell.available {
|
|
background-color: rgba(5, 150, 105, 0.1);
|
|
border-color: rgba(5, 150, 105, 0.5);
|
|
}
|
|
|
|
.ip-address {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.ip-hostname {
|
|
font-size: 10px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
</style> |