//! HTTP handlers for the charts feature.
use actix_web::{HttpRequest, get, web};
use maud::{Markup, html};
use crate::{ConnectionStore, db};
use super::infer::infer_chart;
use super::query::{ChartQuery, DbQuery, fetch_chart_data};
use super::render::{error_svg, render_svg};
use super::types::ChartType;
use super::ui::{chart_area, chart_panel, no_connection_html, option_style, select_class};
/// Main charts page – renders the database selector.
#[get("/charts")]
pub async fn charts_page(
req: HttpRequest,
store: web::Data<ConnectionStore>,
) -> actix_web::Result<Markup> {
let Some(ch) = store.active_client() else {
return Ok(no_connection_html(&req));
};
let databases = db::all_databases(ch).await;
let content = html! {
div class="w-100 flex flex-column" style="height:100%;" {
// ── Top selector bar ─────────────────────────────────────────
div class="bg-black-70 pa3 bb b--white-10" style="flex-shrink:0;" {
div class="flex items-end" style="gap:0.5rem;" {
div {
label class="db f7 white-50 mb1" { "Database" }
select
id="chart-db-select"
name="database"
class=(select_class())
style="color:white;"
hx-get="/charts/tables"
hx-include="[name='database']"
hx-target="#chart-table-container"
hx-swap="innerHTML"
hx-trigger="change" {
option value="" selected disabled style="background-color:#1a1a1a;color:#ccc;" {
"Database…"
}
@for db in &databases {
option value=(db.name) style=(option_style()) { (db.name) }
}
}
}
div id="chart-table-container" {}
}
}
// ── Content area ─────────────────────────────────────────────
div id="chart-content"
class="w-100 flex-auto flex flex-column"
style="overflow:hidden;" {
div class="pa4 tc" {
p class="white-50 f5 i" { "Select a database and table to generate charts" }
}
}
}
};
if req.headers().get("HX-Request").is_some() {
Ok(content)
} else {
Ok(crate::view::render_layout(&content))
}
}
/// Returns the table `<select>` for the chosen database (HTMX partial).
#[get("/charts/tables")]
pub async fn charts_get_tables(
query: web::Query<DbQuery>,
store: web::Data<ConnectionStore>,
) -> actix_web::Result<Markup> {
let db_name = match query.database.as_deref() {
Some(s) if !s.is_empty() => s,
_ => return Ok(html! {}),
};
let Some(ch) = store.active_client() else {
return Ok(html! { p class="white-70 f6" { "No active connection." } });
};
let tables = db::all_tables(ch, db_name).await;
Ok(html! {
div {
label class="db f7 white-50 mb1" { "Table" }
select
id="chart-table-select"
name="table"
class=(select_class())
style="color:white;"
hx-get="/charts/view"
hx-include="[name='database'],[name='table']"
hx-target="#chart-content"
hx-swap="innerHTML"
hx-trigger="change" {
option value="" selected disabled style="background-color:#1a1a1a;color:#ccc;" {
"Table…"
}
@for t in &tables {
option value=(t.name) style=(option_style()) { (t.name) }
}
}
}
})
}
/// Renders the full chart view (controls + initial chart) for the chosen table.
#[get("/charts/view")]
pub async fn charts_view(
query: web::Query<ChartQuery>,
store: web::Data<ConnectionStore>,
) -> actix_web::Result<Markup> {
let Some(conn) = store.active() else {
return Ok(html! { p class="white-70 f6 pa3" { "No active connection." } });
};
let cols = db::describe_table(&conn, &query.database, &query.table).await;
if cols.is_empty() {
return Ok(html! {
p class="white-70 f6 pa3" {
"Could not describe table \"" (query.table) "\". Check permissions."
}
});
}
let (inferred_type, inferred_x, inferred_y) = infer_chart(&cols);
let chart_type = query.chart_type.clone().unwrap_or(inferred_type);
let x_col = query
.x_col
.clone()
.filter(|s| !s.is_empty())
.unwrap_or(inferred_x);
let y_col = query
.y_col
.clone()
.filter(|s| !s.is_empty())
.unwrap_or(inferred_y);
let limit = query.limit;
let (svg, data_len, err_msg) = match fetch_chart_data(
&conn,
&query.database,
&query.table,
&chart_type,
&x_col,
&y_col,
limit,
)
.await
{
Ok(data) => {
let len = data.len();
let svg = render_svg(&chart_type, &data, &x_col, &y_col);
(svg, len, None)
}
Err(e) => {
log::error!("Chart data fetch error: {e}");
let svg = error_svg(&format!("Query error: {e}"));
(svg, 0, Some(e))
}
};
Ok(chart_panel(
&cols,
&chart_type,
&x_col,
&y_col,
limit,
&query.database,
&query.table,
&svg,
data_len,
err_msg.as_deref(),
))
}
/// Re-renders just the `#chart-area` when the user adjusts controls.
#[get("/charts/render")]
pub async fn charts_render(
query: web::Query<ChartQuery>,
store: web::Data<ConnectionStore>,
) -> actix_web::Result<Markup> {
let Some(conn) = store.active() else {
return Ok(html! {
div id="chart-area" { p class="white-70 f6 pa3" { "No active connection." } }
});
};
let chart_type = query.chart_type.clone().unwrap_or(ChartType::Bar);
let x_col = query.x_col.clone().unwrap_or_default();
let y_col = query.y_col.clone().unwrap_or_default();
let limit = query.limit;
if x_col.is_empty() {
return Ok(html! {
div id="chart-area" { p class="white-70 f6 pa3" { "Select an X column." } }
});
}
let (svg, data_len, err_msg) = match fetch_chart_data(
&conn,
&query.database,
&query.table,
&chart_type,
&x_col,
&y_col,
limit,
)
.await
{
Ok(data) => {
let len = data.len();
let svg = render_svg(&chart_type, &data, &x_col, &y_col);
(svg, len, None)
}
Err(e) => {
log::error!("Chart render error: {e}");
let svg = error_svg(&format!("Query error: {e}"));
(svg, 0, Some(e))
}
};
Ok(chart_area(&svg, data_len, err_msg.as_deref()))
}