feat(networks): add delete confirmation modal with host count warning

Show a modal before deleting a network. If the network has hosts,
display a warning with the exact count since they will be cascade-deleted.
Host count comes from the existing NetworkWithCounts data (no extra query).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 01:28:12 +02:00
parent 30dd1ad0b0
commit 55d8ed9f72
3 changed files with 92 additions and 5 deletions

View File

@@ -8,7 +8,7 @@ use crate::models::Network;
// Network row augmented with pre-computed counts.
// Defined here (not in models.rs) because it is a presentation model
// specific to the Networks page, not a pure domain entity.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct NetworkWithCounts {
pub id: i64,
pub cidr: String,

View File

@@ -16,7 +16,59 @@
use leptos::prelude::*;
use leptos::form::ActionForm;
use crate::api::networks::{CreateNetwork, DeleteNetwork, get_networks_with_counts};
use crate::api::networks::{CreateNetwork, DeleteNetwork, NetworkWithCounts, get_networks_with_counts};
// ─── Delete confirmation modal ────────────────────────────────────────────────
#[component]
fn DeleteConfirmModal(
network: NetworkWithCounts,
delete_action: ServerAction<DeleteNetwork>,
pending_delete: RwSignal<Option<NetworkWithCounts>>,
) -> impl IntoView {
let id = network.id;
let cidr = network.cidr.clone();
let host_count = network.host_count;
view! {
<div class="modal-backdrop" on:click=move |_| pending_delete.set(None)>
<div class="modal" on:click=move |e| e.stop_propagation()>
<div class="modal__header">
<h2>"Delete network"</h2>
</div>
<div class="modal__body">
<p>"Delete network " <strong>{cidr}</strong> "?"</p>
{(host_count > 0).then(|| view! {
<p class="warning">
"Warning: "
{host_count}
{if host_count == 1 { " host" } else { " hosts" }}
" belonging to this network will also be deleted."
</p>
})}
</div>
<div class="modal__actions">
<button
class="btn-secondary"
type="button"
on:click=move |_| pending_delete.set(None)
>"Cancel"</button>
<button
class="btn-danger"
type="button"
on:click=move |_| {
delete_action.dispatch(DeleteNetwork { id });
}
>"Delete"</button>
</div>
</div>
</div>
}.into_any()
}
// ─── Page ─────────────────────────────────────────────────────────────────────
#[component]
pub fn NetworksPage() -> impl IntoView {
@@ -28,6 +80,16 @@ pub fn NetworksPage() -> impl IntoView {
let create_action = ServerAction::<CreateNetwork>::new();
let delete_action = ServerAction::<DeleteNetwork>::new();
// Stores the network pending deletion; Some = modal open, None = closed.
let pending_delete: RwSignal<Option<NetworkWithCounts>> = RwSignal::new(None);
// Close the modal automatically after a successful deletion.
Effect::new(move |_| {
if let Some(Ok(_)) = delete_action.value().get() {
pending_delete.set(None);
}
});
// ── Data resource ─────────────────────────────────────────────────────────
//
// `Resource::new(source, fetcher)`:
@@ -45,6 +107,15 @@ pub fn NetworksPage() -> impl IntoView {
<div class="networks-page">
<h1>"Networks"</h1>
// ── Delete confirmation modal ──────────────────────────────────────
{move || pending_delete.get().map(|network| view! {
<DeleteConfirmModal
network=network
delete_action=delete_action
pending_delete=pending_delete
/>
})}
// ── Add form ──────────────────────────────────────────────────────
//
// `<ActionForm action=create_action>` submits the form to the server
@@ -123,7 +194,7 @@ pub fn NetworksPage() -> impl IntoView {
{list
.into_iter()
.map(|network| {
let id = network.id;
let network_clone = network.clone();
view! {
<tr>
<td>{network.cidr}</td>
@@ -131,8 +202,7 @@ pub fn NetworksPage() -> impl IntoView {
<td class="col-count">{network.application_count}</td>
<td class="col-actions">
<button on:click=move |_| {
delete_action
.dispatch(DeleteNetwork { id });
pending_delete.set(Some(network_clone.clone()));
}>
"Delete"
</button>