Skip to main content

qlite/
ui.rs

1use askama::Template;
2use axum::{
3    extract::{Form, Path, State},
4    http::StatusCode,
5    response::{Html, Json, Redirect},
6};
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10use crate::http_server::AppState;
11
12#[derive(Template)]
13#[template(path = "dashboard.html")]
14pub struct DashboardTemplate {
15    pub queues: Vec<QueueInfo>,
16    pub messages: Vec<MessageInfo>,
17    pub total_queues: usize,
18    pub total_messages: usize,
19    pub total_available_messages: usize,
20    pub total_in_flight_messages: usize,
21}
22
23#[derive(Template)]
24#[template(path = "messages.html")]
25pub struct MessagesTemplate {
26    pub messages: Vec<MessageInfo>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct QueueInfo {
31    pub name: String,
32    pub created_at: String,
33    pub available_messages: u32,
34    pub in_flight_messages: u32,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct MessageInfo {
39    pub id: String,
40    pub body: String,
41    pub created_at: String,
42    pub visibility_timeout: String,
43    pub receive_count: u32,
44    pub attributes: String,
45    pub deduplication_id: String,
46    pub status: String,
47    pub processed_at: String,
48    pub deleted_at: String,
49}
50
51#[derive(Debug, Serialize)]
52pub struct ApiResponse {
53    pub success: bool,
54    pub message: String,
55}
56
57pub async fn dashboard(State(state): State<Arc<AppState>>) -> Result<Html<String>, String> {
58    // Get all queues
59    let queues_data = state
60        .queue_service
61        .list_queues()
62        .await
63        .map_err(|e| format!("Failed to list queues: {}", e))?;
64
65    let mut queues = Vec::new();
66    let mut total_available = 0u32;
67    let mut total_in_flight = 0u32;
68
69    // Get queue attributes for each queue
70    for (queue_name, created_at) in queues_data {
71        if let Ok(Some(attrs)) = state.queue_service.get_queue_attributes(&queue_name).await {
72            total_available += attrs.approximate_number_of_messages;
73            total_in_flight += attrs.approximate_number_of_messages_not_visible;
74
75            queues.push(QueueInfo {
76                name: queue_name,
77                created_at,
78                available_messages: attrs.approximate_number_of_messages,
79                in_flight_messages: attrs.approximate_number_of_messages_not_visible,
80            });
81        }
82    }
83
84    let template = DashboardTemplate {
85        total_queues: queues.len(),
86        total_messages: (total_available + total_in_flight) as usize,
87        total_available_messages: total_available as usize,
88        total_in_flight_messages: total_in_flight as usize,
89        queues,
90        messages: vec![], // Empty by default, populated when a queue is selected
91    };
92
93    let html = template
94        .render()
95        .map_err(|e| format!("Template render error: {}", e))?;
96
97    Ok(Html(html))
98}
99
100pub async fn queue_messages(
101    Path(queue_name): Path<String>,
102    State(state): State<Arc<AppState>>,
103) -> Result<Html<String>, String> {
104    // Get messages for the specific queue by reading directly from database
105    // Since we don't have a direct method, we'll simulate it by getting queue info
106    // and then fetching some messages (this is a simplified approach)
107
108    let messages = get_queue_messages(&state, &queue_name)
109        .await
110        .map_err(|e| format!("Failed to get messages: {}", e))?;
111
112    let template = MessagesTemplate { messages };
113
114    let html = template
115        .render()
116        .map_err(|e| format!("Template render error: {}", e))?;
117
118    Ok(Html(html))
119}
120
121async fn get_queue_messages(
122    state: &Arc<AppState>,
123    queue_name: &str,
124) -> Result<Vec<MessageInfo>, Box<dyn std::error::Error>> {
125    let messages_data = state
126        .queue_service
127        .get_all_queue_messages(queue_name)
128        .await?;
129
130    let mut messages = Vec::new();
131    for (
132        id,
133        body,
134        created_at,
135        visibility_timeout,
136        receive_count,
137        attributes,
138        deduplication_id,
139        status,
140        processed_at,
141        deleted_at,
142    ) in messages_data
143    {
144        messages.push(MessageInfo {
145            id,
146            body,
147            created_at,
148            visibility_timeout: visibility_timeout.unwrap_or_else(|| "None".to_string()),
149            receive_count,
150            attributes: attributes.unwrap_or_else(|| "None".to_string()),
151            deduplication_id: deduplication_id.unwrap_or_else(|| "None".to_string()),
152            status,
153            processed_at: processed_at.unwrap_or_else(|| "Never".to_string()),
154            deleted_at: deleted_at.unwrap_or_else(|| "Never".to_string()),
155        });
156    }
157
158    Ok(messages)
159}
160
161// Form structures for UI operations
162#[derive(Debug, Deserialize)]
163pub struct CreateQueueForm {
164    pub queue_name: String,
165    pub queue_type: Option<String>,
166    pub visibility_timeout_seconds: Option<u32>,
167    pub message_retention_period_seconds: Option<u32>,
168    pub max_receive_count: Option<u32>,
169    pub delay_seconds: Option<u32>,
170    pub receive_message_wait_time_seconds: Option<u32>,
171    pub dead_letter_target_queue: Option<String>,
172    pub content_based_deduplication: Option<String>,
173}
174
175// UI handler functions for queue and message management
176pub async fn create_queue_ui(
177    State(state): State<Arc<AppState>>,
178    Form(form): Form<CreateQueueForm>,
179) -> Result<Redirect, String> {
180    use crate::config::QueueConfig;
181
182    if form.queue_name.trim().is_empty() {
183        return Err("Queue name cannot be empty".to_string());
184    }
185
186    // Determine if it's a FIFO queue based only on explicit type selection
187    let is_fifo = form
188        .queue_type
189        .as_ref()
190        .map(|t| t == "fifo")
191        .unwrap_or(false);
192
193    // For simple queue creation (no advanced options), use the basic method
194    if form.visibility_timeout_seconds.is_none()
195        && form.message_retention_period_seconds.is_none()
196        && form.max_receive_count.is_none()
197        && form.delay_seconds.is_none()
198        && form.receive_message_wait_time_seconds.is_none()
199        && form
200            .dead_letter_target_queue
201            .as_ref()
202            .is_none_or(|s| s.trim().is_empty())
203        && !is_fifo
204    {
205        match state.queue_service.create_queue(&form.queue_name).await {
206            Ok(_) => return Ok(Redirect::to("/ui")),
207            Err(e) => return Err(format!("Failed to create queue: {}", e)),
208        }
209    }
210
211    // For advanced options or FIFO queues, use the config method
212    let mut config = QueueConfig {
213        name: form.queue_name.clone(),
214        is_fifo,
215        ..Default::default()
216    };
217
218    // Apply custom configuration values
219    if let Some(timeout) = form.visibility_timeout_seconds {
220        config.visibility_timeout_seconds = timeout;
221    }
222
223    if let Some(retention) = form.message_retention_period_seconds {
224        config.message_retention_period_seconds = retention;
225    }
226
227    if let Some(max_receive) = form.max_receive_count {
228        config.max_receive_count = Some(max_receive);
229    }
230
231    if let Some(delay) = form.delay_seconds {
232        config.delay_seconds = delay;
233    }
234
235    if let Some(wait_time) = form.receive_message_wait_time_seconds {
236        config.receive_message_wait_time_seconds = wait_time;
237    }
238
239    if let Some(dlq_queue) = form
240        .dead_letter_target_queue
241        .filter(|s| !s.trim().is_empty())
242    {
243        // Convert queue name to a simple ARN-like format for internal use
244        config.dead_letter_target_arn = Some(format!("qlite://queue/{}", dlq_queue));
245    }
246
247    // FIFO-specific options
248    if is_fifo {
249        config.content_based_deduplication = form
250            .content_based_deduplication
251            .map(|v| v == "on")
252            .unwrap_or(true); // Default to true for FIFO queues
253    }
254
255    match state.queue_service.create_queue_with_config(&config).await {
256        Ok(_) => Ok(Redirect::to("/ui")),
257        Err(e) => Err(format!("Failed to create queue: {}", e)),
258    }
259}
260
261pub async fn delete_queue_ui(
262    State(state): State<Arc<AppState>>,
263    Path(queue_name): Path<String>,
264) -> Result<Redirect, String> {
265    match state.queue_service.delete_queue(&queue_name).await {
266        Ok(_) => Ok(Redirect::to("/ui")),
267        Err(e) => Err(format!("Failed to delete queue: {}", e)),
268    }
269}
270
271pub async fn delete_message_ui(
272    State(state): State<Arc<AppState>>,
273    Path(message_id): Path<String>,
274) -> Result<Redirect, String> {
275    match state.queue_service.delete_message(&message_id).await {
276        Ok(_) => Ok(Redirect::to("/ui")),
277        Err(e) => Err(format!("Failed to delete message: {}", e)),
278    }
279}
280
281pub async fn restore_message_ui(
282    State(state): State<Arc<AppState>>,
283    Path(message_id): Path<String>,
284) -> Result<Redirect, String> {
285    match state.queue_service.restore_message(&message_id).await {
286        Ok(_) => Ok(Redirect::to("/ui")),
287        Err(e) => Err(format!("Failed to restore message: {}", e)),
288    }
289}
290
291// JSON API endpoints for AJAX calls that preserve UI state
292pub async fn delete_queue_json(
293    State(state): State<Arc<AppState>>,
294    Path(queue_name): Path<String>,
295) -> Result<Json<ApiResponse>, (StatusCode, Json<ApiResponse>)> {
296    match state.queue_service.delete_queue(&queue_name).await {
297        Ok(_) => Ok(Json(ApiResponse {
298            success: true,
299            message: format!("Queue '{}' deleted successfully", queue_name),
300        })),
301        Err(e) => Err((
302            StatusCode::INTERNAL_SERVER_ERROR,
303            Json(ApiResponse {
304                success: false,
305                message: format!("Failed to delete queue: {}", e),
306            }),
307        )),
308    }
309}
310
311pub async fn delete_message_json(
312    State(state): State<Arc<AppState>>,
313    Path(message_id): Path<String>,
314) -> Result<Json<ApiResponse>, (StatusCode, Json<ApiResponse>)> {
315    match state.queue_service.delete_message(&message_id).await {
316        Ok(_) => Ok(Json(ApiResponse {
317            success: true,
318            message: "Message deleted successfully".to_string(),
319        })),
320        Err(e) => Err((
321            StatusCode::INTERNAL_SERVER_ERROR,
322            Json(ApiResponse {
323                success: false,
324                message: format!("Failed to delete message: {}", e),
325            }),
326        )),
327    }
328}
329
330pub async fn restore_message_json(
331    State(state): State<Arc<AppState>>,
332    Path(message_id): Path<String>,
333) -> Result<Json<ApiResponse>, (StatusCode, Json<ApiResponse>)> {
334    match state.queue_service.restore_message(&message_id).await {
335        Ok(_) => Ok(Json(ApiResponse {
336            success: true,
337            message: "Message restored successfully".to_string(),
338        })),
339        Err(e) => Err((
340            StatusCode::INTERNAL_SERVER_ERROR,
341            Json(ApiResponse {
342                success: false,
343                message: format!("Failed to restore message: {}", e),
344            }),
345        )),
346    }
347}