feat(applications): add application detail page
- New page /applications/:id with identity (editable name), associated ports (add/remove), linked hosts (read-only via shared ports), and delete with confirmation modal - Add get_application_detail and update_application server functions - Add ApplicationDetail and HostRef types in api/applications - Add update_application to the repository layer - Application names in the list are now clickable links Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,24 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::Application;
|
||||
|
||||
// Minimal host reference used by ApplicationDetail.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HostRef {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
// Full detail for a single application: identity, associated ports, and linked hosts.
|
||||
// Linked hosts are those that have at least one port matching an application_ports entry.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ApplicationDetail {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub ports: Vec<u16>,
|
||||
pub hosts: Vec<HostRef>,
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -65,6 +83,52 @@ pub async fn get_applications() -> Result<Vec<Application>, ServerFnError> {
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// Returns full detail for a single application: identity, ports, and linked hosts.
|
||||
///
|
||||
/// Linked hosts are hosts that have at least one open port matching one of
|
||||
/// the application's registered port numbers (via application_ports ↔ host_ports).
|
||||
#[server]
|
||||
pub async fn get_application_detail(id: i64) -> Result<ApplicationDetail, ServerFnError> {
|
||||
use sqlx::{AnyPool, Row};
|
||||
use crate::server::repository::applications as repo;
|
||||
|
||||
let pool = use_context::<AnyPool>()
|
||||
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
|
||||
|
||||
let app = repo::find_application(&pool, id)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||||
.ok_or_else(|| ServerFnError::new(format!("Application {id} not found")))?;
|
||||
|
||||
let ports = repo::list_ports_for_application(&pool, id)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT DISTINCT h.id, h.name, h.ip
|
||||
FROM hosts h
|
||||
JOIN host_ports hp ON hp.host_id = h.id
|
||||
JOIN application_ports ap ON ap.port_number = hp.port_number
|
||||
WHERE ap.application_id = $1
|
||||
ORDER BY h.name",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
let hosts = rows
|
||||
.iter()
|
||||
.map(|row| HostRef {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
ip: row.get("ip"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ApplicationDetail { id: app.id, name: app.name, ports, hosts })
|
||||
}
|
||||
|
||||
/// Returns the port numbers associated with an application.
|
||||
#[server]
|
||||
pub async fn get_ports_for_application(
|
||||
@@ -83,6 +147,24 @@ pub async fn get_ports_for_application(
|
||||
|
||||
// ─── Mutations ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Updates the name of an application and returns the updated record.
|
||||
#[server]
|
||||
pub async fn update_application(id: i64, 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"))?;
|
||||
|
||||
if name.trim().is_empty() {
|
||||
return Err(ServerFnError::new("Application name cannot be empty"));
|
||||
}
|
||||
|
||||
repo::update_application(&pool, id, name.trim())
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// Creates a new application, then associates the given port numbers.
|
||||
///
|
||||
/// `ports` is a comma-separated list of port numbers (e.g. "80,443").
|
||||
|
||||
Reference in New Issue
Block a user