Files
rust-ipam/src/client/applications.rs

252 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// client/applications.rs — Applications list page
//
// Displays all applications with:
// - Add button : opens a modal to create an application by name
// - Filter bar : name substring filter (client-side)
// - Table : application name + number of associated hosts
// - Delete : confirmation modal before deletion
use leptos::prelude::*;
use leptos::form::ActionForm;
use crate::api::applications::{
ApplicationWithCounts, CreateApplication, DeleteApplication,
get_applications_with_counts,
};
// ─── Add application modal ────────────────────────────────────────────────────
#[component]
fn AddApplicationModal(
create_action: ServerAction<CreateApplication>,
show_modal: RwSignal<bool>,
) -> impl IntoView {
Effect::new(move |_| {
if let Some(Ok(_)) = create_action.value().get() {
show_modal.set(false);
}
});
view! {
<div class="modal-backdrop" on:click=move |_| show_modal.set(false)>
<div class="modal" on:click=move |e| e.stop_propagation()>
<div class="modal__header">
<h2>"Add an application"</h2>
<button class="modal__close" type="button" aria-label="Close"
on:click=move |_| show_modal.set(false)>
"×"
</button>
</div>
<ActionForm action=create_action>
<div class="add-form__fields">
<label>
"Name"
<input
type="text"
name="name"
placeholder="e.g. Nginx, PostgreSQL, Prometheus"
required
/>
</label>
</div>
<div class="modal__actions">
<button class="btn-secondary" type="button"
on:click=move |_| show_modal.set(false)>
"Cancel"
</button>
<button type="submit">"Add application"</button>
</div>
</ActionForm>
{move || create_action.value().get()
.and_then(|r| r.err())
.map(|e| view! { <p class="error">{e.to_string()}</p> })
}
</div>
</div>
}.into_any()
}
// ─── Delete confirmation modal ────────────────────────────────────────────────
#[component]
fn DeleteAppModal(
app: ApplicationWithCounts,
delete_action: ServerAction<DeleteApplication>,
pending_delete: RwSignal<Option<ApplicationWithCounts>>,
) -> impl IntoView {
let id = app.id;
let label = app.name.clone();
let host_count = app.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 application"</h2>
<button class="modal__close" type="button" aria-label="Close"
on:click=move |_| pending_delete.set(None)>
"×"
</button>
</div>
<div class="modal__body">
<p>"Delete application " <strong>{label}</strong> "?"</p>
{(host_count > 0).then(|| view! {
<p class="warning">
"This application is linked to "
{host_count}
{if host_count == 1 { " host" } else { " hosts" }}
" via shared ports. The port associations will be removed."
</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(DeleteApplication { id }); }>
"Delete"
</button>
</div>
</div>
</div>
}.into_any()
}
// ─── Page ─────────────────────────────────────────────────────────────────────
#[component]
pub fn ApplicationsPage() -> impl IntoView {
let create_action = ServerAction::<CreateApplication>::new();
let delete_action = ServerAction::<DeleteApplication>::new();
let show_modal = RwSignal::new(false);
// Some(app) = delete modal open for that app; None = closed.
let pending_delete: RwSignal<Option<ApplicationWithCounts>> = RwSignal::new(None);
// Name filter (client-side — list is typically small)
let name_filter = RwSignal::new(String::new());
// Close the delete modal automatically after a successful deletion.
Effect::new(move |_| {
if let Some(Ok(_)) = delete_action.value().get() {
pending_delete.set(None);
}
});
let applications = Resource::new(
move || (create_action.version().get(), delete_action.version().get()),
|_| get_applications_with_counts(),
);
view! {
<div class="applications-page">
// ── Page header ───────────────────────────────────────────────────
<div class="page-header">
<h1>"Applications"</h1>
<button class="btn-primary" on:click=move |_| show_modal.set(true)>
"+ Add application"
</button>
</div>
// ── Add modal ─────────────────────────────────────────────────────
{move || show_modal.get().then(|| view! {
<AddApplicationModal
create_action=create_action
show_modal=show_modal
/>
})}
// ── Delete modal ──────────────────────────────────────────────────
{move || pending_delete.get().map(|app| view! {
<DeleteAppModal
app=app
delete_action=delete_action
pending_delete=pending_delete
/>
})}
// ── Filter bar ────────────────────────────────────────────────────
<section class="filter-bar">
<div class="filter-bar__fields">
<label class="filter-field">
"Name"
<input
type="text"
placeholder="Search…"
on:input=move |e| name_filter.set(event_target_value(&e))
/>
</label>
</div>
</section>
// ── Application list ──────────────────────────────────────────────
<section class="list">
{move || delete_action.value().get()
.and_then(|r| r.err())
.map(|e| view! { <p class="error">"Delete failed: " {e.to_string()}</p> })
}
<Suspense fallback=|| view! { <p>"Loading applications…"</p> }>
{move || applications.get().map(|result| match result {
Err(e) => view! {
<p class="error">"Could not load applications: " {e.to_string()}</p>
}.into_any(),
Ok(list) => {
let filter = name_filter.get().to_lowercase();
let filtered: Vec<_> = list.into_iter()
.filter(|app| filter.is_empty() || app.name.to_lowercase().contains(&filter))
.collect();
if filtered.is_empty() {
view! {
<p class="empty">"No applications match the current filter."</p>
}.into_any()
} else {
view! {
<div class="table-container">
<table>
<thead>
<tr>
<th>"Name"</th>
<th class="col-count">"Hosts"</th>
<th class="col-actions">"Actions"</th>
</tr>
</thead>
<tbody>
{filtered.into_iter().map(|app| {
let app_clone = app.clone();
view! {
<tr>
<td>{app.name}</td>
<td class="col-count">{app.host_count}</td>
<td class="col-actions">
<button on:click=move |_| {
pending_delete.set(Some(app_clone.clone()));
}>
"Delete"
</button>
</td>
</tr>
}
}).collect_view()}
</tbody>
</table>
</div>
}.into_any()
}
}
})}
</Suspense>
</section>
</div>
}
}