Files
rust-ipam/src/api/applications.rs
mathieu 4d0be98160 feat(applications): add applications list page with host count and delete modal
- API: ApplicationWithCounts struct + get_applications_with_counts() — counts
  distinct hosts linked via matching ports (application_ports ↔ host_ports)
- ApplicationsPage at /applications: inline add form, table with Name and
  Hosts columns, delete confirmation modal showing affected host count
- Nav: add Applications link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 02:48:53 +02:00

153 lines
5.1 KiB
Rust

// api/applications.rs — Server functions for applications and their port associations
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use crate::models::Application;
// Application row enriched with the number of hosts that use at least one of
// its registered ports. Host count is computed via the join:
// application_ports → host_ports (matched on port_number) → hosts
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApplicationWithCounts {
pub id: i64,
pub name: String,
/// Distinct hosts that have at least one port matching this application.
pub host_count: i64,
}
// ─── Queries ──────────────────────────────────────────────────────────────────
/// Returns all applications enriched with their associated host count.
#[server]
pub async fn get_applications_with_counts() -> Result<Vec<ApplicationWithCounts>, ServerFnError> {
use sqlx::{AnyPool, Row};
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
let rows = sqlx::query(
"SELECT
a.id,
a.name,
COUNT(DISTINCT hp.host_id) AS host_count
FROM applications a
LEFT JOIN application_ports ap ON ap.application_id = a.id
LEFT JOIN host_ports hp ON hp.port_number = ap.port_number
GROUP BY a.id, a.name
ORDER BY a.name",
)
.fetch_all(&pool)
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;
Ok(rows
.into_iter()
.map(|row| ApplicationWithCounts {
id: row.get("id"),
name: row.get("name"),
host_count: row.get("host_count"),
})
.collect())
}
/// Returns all applications ordered by name.
#[server]
pub async fn get_applications() -> Result<Vec<Application>, ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::applications as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::list_applications(&pool)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
/// Returns the port numbers associated with an application.
#[server]
pub async fn get_ports_for_application(
application_id: i64,
) -> Result<Vec<u16>, ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::applications as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::list_ports_for_application(&pool, application_id)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
// ─── Mutations ────────────────────────────────────────────────────────────────
/// Creates a new application and returns the created record.
#[server]
pub async fn create_application(name: String) -> Result<Application, ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::applications as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::create_application(&pool, &name)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
/// Deletes an application and all its port associations.
///
/// Returns `true` if the application existed and was deleted.
#[server]
pub async fn delete_application(id: i64) -> Result<bool, ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::applications as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::delete_application(&pool, id)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
/// Associates a port number with an application.
///
/// If the association already exists, this is a no-op.
#[server]
pub async fn add_port_to_application(
application_id: i64,
port_number: u16,
) -> Result<(), ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::applications as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::add_port_to_application(&pool, application_id, port_number)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
/// Removes a port association from an application.
///
/// If the association does not exist, this is a no-op.
#[server]
pub async fn remove_port_from_application(
application_id: i64,
port_number: u16,
) -> Result<(), ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::applications as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::remove_port_from_application(&pool, application_id, port_number)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}