wip
This commit is contained in:
parent
be6f7cfcbb
commit
9433d9d235
7 changed files with 189 additions and 84 deletions
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
BIN
instance/app.db
BIN
instance/app.db
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue