This commit is contained in:
pika 2025-03-30 22:01:42 +02:00
parent be6f7cfcbb
commit 9433d9d235
7 changed files with 189 additions and 84 deletions

View file

@ -10,13 +10,31 @@ bp = Blueprint('api', __name__, url_prefix='/api')
@bp.route('/subnets', methods=['GET']) @bp.route('/subnets', methods=['GET'])
def get_subnets(): def get_subnets():
"""Get all subnets""" """Get all subnets grouped by site"""
subnets = Subnet.query.all() subnets = Subnet.query.all()
return jsonify([{
'id': subnet.id, # Group subnets by location (site)
'cidr': subnet.cidr, sites = {}
'location': subnet.location for subnet in subnets:
} for subnet in subnets]) location = subnet.location
if location not in sites:
sites[location] = []
sites[location].append({
'id': subnet.id,
'cidr': subnet.cidr,
'location': location
})
# Convert to list of site objects
result = [
{
'name': site_name,
'subnets': subnets
} for site_name, subnets in sites.items()
]
return jsonify(result)
@bp.route('/subnets/<int:subnet_id>', methods=['GET']) @bp.route('/subnets/<int:subnet_id>', methods=['GET'])
@login_required @login_required

View file

@ -305,8 +305,8 @@ def app_edit(app_id):
# Check if name changed and already exists on the same server # Check if name changed and already exists on the same server
existing_app = App.query.filter(App.name == name, existing_app = App.query.filter(App.name == name,
App.server_id == server_id, App.server_id == server_id,
App.id != app.id).first() App.id != app.id).first()
if existing_app: if existing_app:
flash('Application with this name already exists on the selected server', 'danger') flash('Application with this name already exists on the selected server', 'danger')
return render_template( return render_template(
@ -321,10 +321,13 @@ def app_edit(app_id):
app.server_id = server_id app.server_id = server_id
app.documentation = documentation app.documentation = documentation
db.session.commit() try:
db.session.commit()
flash('Application updated successfully', 'success') flash('Application updated successfully', 'success')
return redirect(url_for('dashboard.app_view', app_id=app.id)) return redirect(url_for('dashboard.app_view', app_id=app.id))
except Exception as e:
db.session.rollback()
flash(f'Error updating application: {str(e)}', 'danger')
return render_template( return render_template(
'dashboard/app_form.html', 'dashboard/app_form.html',

View file

@ -54,9 +54,11 @@
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
{% if app %} {% if app %}
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="btn btn-outline-secondary ms-2">Cancel</a> <a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="btn btn-outline-secondary ms-2">Cancel</a>
{% else %} {% elif server_id %}
<a href="{% if server_id %}{{ url_for('dashboard.server_view', server_id=server_id) }}{% else %}{{ url_for('dashboard.dashboard_home') }}{% endif %}" <a href="{{ url_for('dashboard.server_view', server_id=server_id) }}"
class="btn btn-outline-secondary ms-2">Cancel</a> class="btn btn-outline-secondary ms-2">Cancel</a>
{% else %}
<a href="{{ url_for('dashboard.dashboard_home') }}" class="btn btn-outline-secondary ms-2">Cancel</a>
{% endif %} {% endif %}
</div> </div>
</form> </form>

View file

@ -101,6 +101,45 @@
font-style: italic; font-style: italic;
padding: 5px 15px; padding: 5px 15px;
} }
.site-item {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
color: var(--sidebar-color);
cursor: pointer;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 4px;
margin: 2px 0;
}
.site-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
.site-toggle-icon {
transition: transform 0.2s;
cursor: pointer;
}
.site-item-container {
margin-bottom: 0.25rem;
}
.subnet-item {
margin-left: 1rem;
}
.subnet-item a {
padding-left: 1rem;
display: block;
color: var(--sidebar-color);
}
.subnet-item a:hover {
background-color: rgba(0, 0, 0, 0.03);
border-radius: 4px;
}
</style> </style>
</head> </head>
@ -128,7 +167,7 @@
</a> </a>
<!-- IPAM with Subnet Tree --> <!-- IPAM with Subnet Tree -->
<div class="sidebar-item-parent"> <div class="sidebar-item-parent" id="ipam-menu">
<a href="{{ url_for('ipam.ipam_home') }}" <a href="{{ url_for('ipam.ipam_home') }}"
class="sidebar-item {{ 'active' if request.endpoint and request.endpoint.startswith('ipam.') }}"> class="sidebar-item {{ 'active' if request.endpoint and request.endpoint.startswith('ipam.') }}">
<span class="ti ti-network me-2"></span> IPAM <span class="ti ti-network me-2"></span> IPAM
@ -261,19 +300,74 @@
const parent = this.parentElement; const parent = this.parentElement;
parent.classList.toggle('expanded'); parent.classList.toggle('expanded');
// Load subnet data when IPAM is expanded // Save state in localStorage
if (parent.classList.contains('expanded') && this.textContent.includes('IPAM')) { const menuId = parent.id || parent.dataset.menu;
loadSubnets(); if (menuId) {
if (parent.classList.contains('expanded')) {
saveExpandedMenu(menuId);
// Load subnet data when IPAM is expanded
if (menuId === 'ipam-menu') {
loadSubnets();
}
} else {
removeExpandedMenu(menuId);
}
} }
}); });
}); });
// Check and restore expanded menus from localStorage
restoreExpandedMenus();
function restoreExpandedMenus() {
const expandedMenus = getExpandedMenus();
// Expand saved menus
expandedMenus.forEach(menuId => {
const menuElement = document.getElementById(menuId);
if (menuElement) {
menuElement.classList.add('expanded');
// Load IPAM subnets if that menu is expanded
if (menuId === 'ipam-menu') {
loadSubnets();
}
}
});
}
function getExpandedMenus() {
const saved = localStorage.getItem('expandedMenus');
return saved ? JSON.parse(saved) : [];
}
function saveExpandedMenu(menuId) {
const expandedMenus = getExpandedMenus();
if (!expandedMenus.includes(menuId)) {
expandedMenus.push(menuId);
localStorage.setItem('expandedMenus', JSON.stringify(expandedMenus));
}
}
function removeExpandedMenu(menuId) {
let expandedMenus = getExpandedMenus();
expandedMenus = expandedMenus.filter(id => id !== menuId);
localStorage.setItem('expandedMenus', JSON.stringify(expandedMenus));
}
function loadSubnets() { function loadSubnets() {
const subnetContainer = document.getElementById('subnet-tree-container'); const subnetContainer = document.getElementById('subnet-tree-container');
const loader = document.getElementById('subnet-loader'); const loader = document.getElementById('subnet-loader');
if (!loader || !subnetContainer) return; if (!loader || !subnetContainer) return;
// Check if we already have loaded subnets
if (subnetContainer.querySelector('.site-item-container') &&
!loader.classList.contains('d-none')) {
return; // Already loading or loaded
}
// Show loader // Show loader
loader.classList.remove('d-none'); loader.classList.remove('d-none');
@ -285,52 +379,75 @@
} }
return response.json(); return response.json();
}) })
.then(subnets => { .then(sites => {
loader.classList.add('d-none'); loader.classList.add('d-none');
if (subnets.length === 0) { if (sites.length === 0) {
subnetContainer.innerHTML = '<div class="text-muted px-3 py-2">No subnets found</div>'; subnetContainer.innerHTML = '<div class="text-muted px-3 py-2">No sites or subnets found</div>';
return; return;
} }
let html = ''; let html = '';
subnets.forEach(subnet => { // Loop through sites
sites.forEach(site => {
const siteId = `site-${site.name.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
const isSiteExpanded = getExpandedMenus().includes(siteId);
html += ` html += `
<div class="subnet-item-container"> <div class="site-item-container" id="${siteId}">
<div class="subnet-item" data-subnet-id="${subnet.id}"> <div class="site-item">
<span class="ti ti-chevron-right subnet-toggle-icon me-1"></span> <span class="ti ${isSiteExpanded ? 'ti-chevron-down' : 'ti-chevron-right'} site-toggle-icon me-1"></span>
<a href="/ipam/subnet/${subnet.id}" class="text-reset text-decoration-none flex-grow-1"> <span class="ti ti-building me-1"></span>
${subnet.cidr} (${subnet.location}) <span class="flex-grow-1">${site.name}</span>
</a>
</div> </div>
<div class="subnet-servers d-none" id="subnet-servers-${subnet.id}"> <div class="site-subnets ${isSiteExpanded ? '' : 'd-none'}">`;
<div class="text-muted px-3 py-2">Loading servers...</div>
</div> // Add subnets for this site
</div> if (site.subnets.length === 0) {
`; html += '<div class="text-muted px-3 py-2">No subnets in this site</div>';
} else {
site.subnets.forEach(subnet => {
html += `
<div class="subnet-item">
<a href="/ipam/subnet/${subnet.id}" class="text-reset text-decoration-none d-block ps-4 py-1">
<span class="ti ti-network me-1 small"></span>
${subnet.cidr}
</a>
</div>`;
});
}
html += `</div></div>`;
}); });
subnetContainer.innerHTML = html; subnetContainer.innerHTML = html;
// Add click handlers to subnet items // Add click handlers to site items
document.querySelectorAll('.subnet-item').forEach(item => { document.querySelectorAll('.site-item').forEach(item => {
item.querySelector('.subnet-toggle-icon').addEventListener('click', function (e) { const toggleIcon = item.querySelector('.site-toggle-icon');
e.preventDefault(); if (toggleIcon) {
e.stopPropagation(); toggleIcon.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const subnetId = item.dataset.subnetId; const siteContainer = item.closest('.site-item-container');
const serversContainer = document.getElementById(`subnet-servers-${subnetId}`); const siteId = siteContainer.id;
serversContainer.classList.toggle('d-none'); const subnetsContainer = item.nextElementSibling;
if (!serversContainer.classList.contains('d-none') && subnetsContainer.classList.toggle('d-none');
serversContainer.querySelector('.text-muted')) {
loadServersForSubnet(subnetId);
}
// Toggle icon // Save state to localStorage
this.classList.toggle('ti-chevron-right'); if (!subnetsContainer.classList.contains('d-none')) {
this.classList.toggle('ti-chevron-down'); saveExpandedMenu(siteId);
}); } else {
removeExpandedMenu(siteId);
}
// Toggle icon
this.classList.toggle('ti-chevron-right');
this.classList.toggle('ti-chevron-down');
});
}
}); });
}) })
.catch(error => { .catch(error => {
@ -339,41 +456,6 @@
subnetContainer.innerHTML = '<div class="text-danger px-3 py-2">Error loading subnets</div>'; subnetContainer.innerHTML = '<div class="text-danger px-3 py-2">Error loading subnets</div>';
}); });
} }
function loadServersForSubnet(subnetId) {
const serversContainer = document.getElementById(`subnet-servers-${subnetId}`);
// Fetch servers for this subnet
fetch(`/api/subnets/${subnetId}/servers`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(servers => {
if (servers.length === 0) {
serversContainer.innerHTML = '<div class="text-muted px-3 py-2">No servers in this subnet</div>';
return;
}
let html = '';
servers.forEach(server => {
html += `
<a href="/dashboard/server/${server.id}" class="server-item d-block ps-4 py-1">
<span class="ti ti-server me-1 small"></span>
${server.hostname} (${server.ip_address})
</a>
`;
});
serversContainer.innerHTML = html;
})
.catch(error => {
console.error('Error loading servers:', error);
serversContainer.innerHTML = '<div class="text-danger px-3 py-2">Error loading servers</div>';
});
}
}); });
</script> </script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}

Binary file not shown.