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'])
def get_subnets():
"""Get all subnets"""
"""Get all subnets grouped by site"""
subnets = Subnet.query.all()
return jsonify([{
'id': subnet.id,
'cidr': subnet.cidr,
'location': subnet.location
} for subnet in subnets])
# Group subnets by location (site)
sites = {}
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'])
@login_required

View file

@ -305,8 +305,8 @@ def app_edit(app_id):
# Check if name changed and already exists on the same server
existing_app = App.query.filter(App.name == name,
App.server_id == server_id,
App.id != app.id).first()
App.server_id == server_id,
App.id != app.id).first()
if existing_app:
flash('Application with this name already exists on the selected server', 'danger')
return render_template(
@ -321,10 +321,13 @@ def app_edit(app_id):
app.server_id = server_id
app.documentation = documentation
db.session.commit()
flash('Application updated successfully', 'success')
return redirect(url_for('dashboard.app_view', app_id=app.id))
try:
db.session.commit()
flash('Application updated successfully', 'success')
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(
'dashboard/app_form.html',

View file

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

View file

@ -101,6 +101,45 @@
font-style: italic;
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>
</head>
@ -128,7 +167,7 @@
</a>
<!-- IPAM with Subnet Tree -->
<div class="sidebar-item-parent">
<div class="sidebar-item-parent" id="ipam-menu">
<a href="{{ url_for('ipam.ipam_home') }}"
class="sidebar-item {{ 'active' if request.endpoint and request.endpoint.startswith('ipam.') }}">
<span class="ti ti-network me-2"></span> IPAM
@ -261,19 +300,74 @@
const parent = this.parentElement;
parent.classList.toggle('expanded');
// Load subnet data when IPAM is expanded
if (parent.classList.contains('expanded') && this.textContent.includes('IPAM')) {
loadSubnets();
// Save state in localStorage
const menuId = parent.id || parent.dataset.menu;
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() {
const subnetContainer = document.getElementById('subnet-tree-container');
const loader = document.getElementById('subnet-loader');
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
loader.classList.remove('d-none');
@ -285,52 +379,75 @@
}
return response.json();
})
.then(subnets => {
.then(sites => {
loader.classList.add('d-none');
if (subnets.length === 0) {
subnetContainer.innerHTML = '<div class="text-muted px-3 py-2">No subnets found</div>';
if (sites.length === 0) {
subnetContainer.innerHTML = '<div class="text-muted px-3 py-2">No sites or subnets found</div>';
return;
}
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 += `
<div class="subnet-item-container">
<div class="subnet-item" data-subnet-id="${subnet.id}">
<span class="ti ti-chevron-right subnet-toggle-icon me-1"></span>
<a href="/ipam/subnet/${subnet.id}" class="text-reset text-decoration-none flex-grow-1">
${subnet.cidr} (${subnet.location})
</a>
<div class="site-item-container" id="${siteId}">
<div class="site-item">
<span class="ti ${isSiteExpanded ? 'ti-chevron-down' : 'ti-chevron-right'} site-toggle-icon me-1"></span>
<span class="ti ti-building me-1"></span>
<span class="flex-grow-1">${site.name}</span>
</div>
<div class="subnet-servers d-none" id="subnet-servers-${subnet.id}">
<div class="text-muted px-3 py-2">Loading servers...</div>
</div>
</div>
`;
<div class="site-subnets ${isSiteExpanded ? '' : 'd-none'}">`;
// Add subnets for this site
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;
// Add click handlers to subnet items
document.querySelectorAll('.subnet-item').forEach(item => {
item.querySelector('.subnet-toggle-icon').addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
// Add click handlers to site items
document.querySelectorAll('.site-item').forEach(item => {
const toggleIcon = item.querySelector('.site-toggle-icon');
if (toggleIcon) {
toggleIcon.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const subnetId = item.dataset.subnetId;
const serversContainer = document.getElementById(`subnet-servers-${subnetId}`);
serversContainer.classList.toggle('d-none');
const siteContainer = item.closest('.site-item-container');
const siteId = siteContainer.id;
const subnetsContainer = item.nextElementSibling;
if (!serversContainer.classList.contains('d-none') &&
serversContainer.querySelector('.text-muted')) {
loadServersForSubnet(subnetId);
}
subnetsContainer.classList.toggle('d-none');
// Toggle icon
this.classList.toggle('ti-chevron-right');
this.classList.toggle('ti-chevron-down');
});
// Save state to localStorage
if (!subnetsContainer.classList.contains('d-none')) {
saveExpandedMenu(siteId);
} else {
removeExpandedMenu(siteId);
}
// Toggle icon
this.classList.toggle('ti-chevron-right');
this.classList.toggle('ti-chevron-down');
});
}
});
})
.catch(error => {
@ -339,41 +456,6 @@
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>
{% block scripts %}{% endblock %}

Binary file not shown.