UI: Dashboard + Ingest tabs showing full system progression
- Dashboard: live stats (datasets, rows, embeddings, HNSW, tools, cache) Architecture overview (6 capability areas) Build progression timeline (all 17 phases listed) - Ingest tab: Postgres table browser + import, file upload info, inbox watcher - System tab: existing health checks - Starts on Dashboard for immediate overview - No futures::executor in WASM — all async/await Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9992b5f135
commit
399fc81ab5
@ -162,3 +162,46 @@ th {
|
||||
}
|
||||
td { padding: 8px 14px; border-bottom: 1px solid var(--border); }
|
||||
tr:hover td { background: var(--accent-glow); }
|
||||
|
||||
/* Dashboard */
|
||||
.dashboard-hero { margin-bottom: 28px; }
|
||||
.dashboard-hero h2 { font-size: 32px; font-weight: 700; }
|
||||
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; margin-bottom: 32px; }
|
||||
.stat-card {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
||||
padding: 20px 16px; text-align: center;
|
||||
}
|
||||
.stat-card.accent { border-color: var(--accent); background: var(--accent-glow); }
|
||||
.stat-value { font-size: 28px; font-weight: 700; color: var(--text); }
|
||||
.stat-card.accent .stat-value { color: var(--accent); }
|
||||
.stat-label { font-size: 11px; color: var(--text-dim); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
|
||||
.architecture-section, .phases-section { margin-bottom: 28px; }
|
||||
.architecture-section h3, .phases-section h3 { font-size: 16px; font-weight: 600; color: var(--text-dim); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
|
||||
.arch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; }
|
||||
.arch-card {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 14px; border-left: 3px solid var(--accent);
|
||||
}
|
||||
.arch-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; color: var(--accent); }
|
||||
.arch-items { font-size: 11px; color: var(--text-dim); line-height: 1.6; }
|
||||
|
||||
.phase-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
.phase-item {
|
||||
display: grid; grid-template-columns: 40px 180px 1fr; gap: 12px; align-items: center;
|
||||
padding: 8px 12px; border-radius: 6px; font-size: 12px;
|
||||
background: var(--surface); border-left: 3px solid var(--success);
|
||||
}
|
||||
.phase-num { font-weight: 700; color: var(--accent); text-align: center; }
|
||||
.phase-name { font-weight: 600; }
|
||||
.phase-detail { color: var(--text-dim); }
|
||||
|
||||
/* Ingest */
|
||||
.table-list { margin-top: 12px; }
|
||||
.table-item {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 13px;
|
||||
}
|
||||
.table-item:hover { background: var(--accent-glow); }
|
||||
|
||||
@ -138,9 +138,11 @@ async fn get_schema_context(datasets: &[Dataset]) -> String {
|
||||
// --- Tabs ---
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum Tab {
|
||||
Dashboard,
|
||||
Ask,
|
||||
Explore,
|
||||
Sql,
|
||||
Ingest,
|
||||
Status,
|
||||
}
|
||||
|
||||
@ -148,7 +150,7 @@ enum Tab {
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let mut active_tab = use_signal(|| Tab::Ask);
|
||||
let mut active_tab = use_signal(|| Tab::Dashboard);
|
||||
let mut datasets = use_signal(Vec::<Dataset>::new);
|
||||
let mut ds_loading = use_signal(|| true);
|
||||
|
||||
@ -177,6 +179,11 @@ fn App() -> Element {
|
||||
div { class: "header",
|
||||
h1 { "LAKEHOUSE" }
|
||||
div { class: "tabs",
|
||||
button {
|
||||
class: if *active_tab.read() == Tab::Dashboard { "tab active" } else { "tab" },
|
||||
onclick: move |_| active_tab.set(Tab::Dashboard),
|
||||
"Dashboard"
|
||||
}
|
||||
button {
|
||||
class: if *active_tab.read() == Tab::Ask { "tab active" } else { "tab" },
|
||||
onclick: move |_| active_tab.set(Tab::Ask),
|
||||
@ -192,6 +199,11 @@ fn App() -> Element {
|
||||
onclick: move |_| active_tab.set(Tab::Sql),
|
||||
"SQL"
|
||||
}
|
||||
button {
|
||||
class: if *active_tab.read() == Tab::Ingest { "tab active" } else { "tab" },
|
||||
onclick: move |_| active_tab.set(Tab::Ingest),
|
||||
"Ingest"
|
||||
}
|
||||
button {
|
||||
class: if *active_tab.read() == Tab::Status { "tab active" } else { "tab" },
|
||||
onclick: move |_| active_tab.set(Tab::Status),
|
||||
@ -208,9 +220,11 @@ fn App() -> Element {
|
||||
|
||||
div { class: "content-full",
|
||||
match *active_tab.read() {
|
||||
Tab::Dashboard => rsx! { DashboardPanel {} },
|
||||
Tab::Ask => rsx! { AskPanel { datasets: datasets.read().clone() } },
|
||||
Tab::Explore => rsx! { ExplorePanel { datasets: datasets.read().clone() } },
|
||||
Tab::Sql => rsx! { SqlPanel {} },
|
||||
Tab::Ingest => rsx! { IngestPanel {} },
|
||||
Tab::Status => rsx! { StatusPanel {} },
|
||||
}
|
||||
}
|
||||
@ -532,6 +546,297 @@ fn SqlPanel() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
// === DASHBOARD — System overview ===
|
||||
|
||||
#[component]
|
||||
fn DashboardPanel() -> Element {
|
||||
let mut stats = use_signal(|| None::<serde_json::Value>);
|
||||
let mut loading = use_signal(|| false);
|
||||
|
||||
let do_load = move || {
|
||||
spawn(async move {
|
||||
loading.set(true);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let datasets = fetch_datasets().await.ok().map(|d| d.len()).unwrap_or(0);
|
||||
let row_count = run_sql("SELECT (SELECT COUNT(*) FROM candidates) + (SELECT COUNT(*) FROM clients) + (SELECT COUNT(*) FROM job_orders) + (SELECT COUNT(*) FROM placements) + (SELECT COUNT(*) FROM timesheets) + (SELECT COUNT(*) FROM call_log) + (SELECT COUNT(*) FROM email_log) as total").await.ok().and_then(|r| r.rows.as_array()?.first()?.get("total")?.as_i64()).unwrap_or(0);
|
||||
|
||||
let cache: Option<serde_json::Value> = match client.get(&format!("{}/query/cache/stats", api_base())).send().await {
|
||||
Ok(r) => r.json().await.ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let indexes: Vec<serde_json::Value> = match client.get(&format!("{}/vectors/indexes", api_base())).send().await {
|
||||
Ok(r) => r.json().await.unwrap_or_default(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
let tools: Vec<serde_json::Value> = match client.get(&format!("{}/tools", api_base())).send().await {
|
||||
Ok(r) => r.json().await.unwrap_or_default(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
let jobs: Vec<serde_json::Value> = match client.get(&format!("{}/vectors/jobs", api_base())).send().await {
|
||||
Ok(r) => r.json().await.unwrap_or_default(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
let hnsw: Vec<String> = match client.get(&format!("{}/vectors/hnsw/list", api_base())).send().await {
|
||||
Ok(r) => r.json().await.unwrap_or_default(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
let journal: Option<serde_json::Value> = match client.get(&format!("{}/journal/stats", api_base())).send().await {
|
||||
Ok(r) => r.json().await.ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
stats.set(Some(serde_json::json!({
|
||||
"datasets": datasets,
|
||||
"total_rows": row_count,
|
||||
"cache": cache,
|
||||
"vector_indexes": indexes.len(),
|
||||
"vector_total_chunks": indexes.iter().filter_map(|i| i.get("chunk_count")?.as_i64()).sum::<i64>(),
|
||||
"hnsw_loaded": hnsw.len(),
|
||||
"tools": tools.len(),
|
||||
"jobs_total": jobs.len(),
|
||||
"jobs_running": jobs.iter().filter(|j| j.get("status").and_then(|s| s.as_str()) == Some("running")).count(),
|
||||
"journal": journal,
|
||||
})));
|
||||
loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Auto-load on mount
|
||||
// Auto-load on mount
|
||||
use_effect(move || { do_load(); });
|
||||
|
||||
rsx! {
|
||||
div { class: "panel",
|
||||
div { class: "dashboard-hero",
|
||||
h2 { "Lakehouse" }
|
||||
p { class: "subtitle", "Rust-first data platform — SQL + AI + RAG over object storage" }
|
||||
}
|
||||
|
||||
if let Some(s) = stats.read().as_ref() {
|
||||
div { class: "stat-grid",
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s[\"datasets\"]}" }
|
||||
div { class: "stat-label", "Datasets" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", {format!("{:.1}M", s["total_rows"].as_i64().unwrap_or(0) as f64 / 1_000_000.0)} }
|
||||
div { class: "stat-label", "Total Rows" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", {format!("{}K", s["vector_total_chunks"].as_i64().unwrap_or(0) / 1000)} }
|
||||
div { class: "stat-label", "Embeddings" }
|
||||
}
|
||||
div { class: "stat-card accent",
|
||||
div { class: "stat-value", "{s[\"hnsw_loaded\"]}" }
|
||||
div { class: "stat-label", "HNSW Indexes (27ms)" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s[\"tools\"]}" }
|
||||
div { class: "stat-label", "Governed Tools" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", {
|
||||
s.get("cache")
|
||||
.and_then(|c| c.get("datasets"))
|
||||
.and_then(|d| d.as_i64())
|
||||
.map(|n| format!("{n}"))
|
||||
.unwrap_or("0".into())
|
||||
} }
|
||||
div { class: "stat-label", "Cached Datasets" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "architecture-section",
|
||||
h3 { "Architecture" }
|
||||
div { class: "arch-grid",
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Ingest" }
|
||||
div { class: "arch-items", "CSV, JSON, PDF, Text, PostgreSQL, File Watcher" }
|
||||
}
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Storage" }
|
||||
div { class: "arch-items", "Parquet on Object Storage, Delta Writes, Compaction" }
|
||||
}
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Query" }
|
||||
div { class: "arch-items", "DataFusion SQL, MemCache (9.8x), Hot/Cold" }
|
||||
}
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "AI" }
|
||||
div { class: "arch-items", "Ollama (local), Embed, Generate, RAG, HNSW" }
|
||||
}
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Governance" }
|
||||
div { class: "arch-items", "Event Journal, PII Detection, Tool Registry, Access Control" }
|
||||
}
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Agents" }
|
||||
div { class: "arch-items", "Workspaces, Handoff, Shortlists, Activity Logs" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "phases-section",
|
||||
h3 { "Build Progression" }
|
||||
div { class: "phase-list",
|
||||
{rsx! {
|
||||
PhaseItem { num: "0-5", name: "Foundation", detail: "Storage, Catalog, DataFusion, AI, UI, gRPC" }
|
||||
PhaseItem { num: "6", name: "Ingest Pipeline", detail: "CSV/JSON/PDF/Text auto-schema" }
|
||||
PhaseItem { num: "7", name: "Vector + RAG", detail: "Embed, Search, LLM Answers" }
|
||||
PhaseItem { num: "8", name: "Hot Cache", detail: "9.8x speedup, Delta Writes" }
|
||||
PhaseItem { num: "8.5", name: "Agent Workspaces", detail: "Per-contract, Instant Handoff" }
|
||||
PhaseItem { num: "9", name: "Event Journal", detail: "Append-only Mutation History" }
|
||||
PhaseItem { num: "10", name: "Rich Catalog", detail: "PII Detection, Lineage" }
|
||||
PhaseItem { num: "11", name: "Embedding Versioning", detail: "Model-proof Vectors" }
|
||||
PhaseItem { num: "12", name: "Tool Registry", detail: "6 Governed Actions + Audit" }
|
||||
PhaseItem { num: "13", name: "Access Control", detail: "Role-based, Field-level" }
|
||||
PhaseItem { num: "14", name: "Schema Evolution", detail: "Diff Detection, AI Migration" }
|
||||
PhaseItem { num: "15", name: "HNSW Index", detail: "100K Search in 27ms" }
|
||||
PhaseItem { num: "16", name: "File Watcher", detail: "Auto-ingest from Inbox" }
|
||||
PhaseItem { num: "17", name: "DB Connector", detail: "PostgreSQL Import" }
|
||||
}}
|
||||
}
|
||||
}
|
||||
} else if *loading.read() {
|
||||
div { class: "loading", "loading dashboard..." }
|
||||
}
|
||||
|
||||
button { class: "btn", onclick: move |_| do_load(), "Refresh" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PhaseItem(num: String, name: String, detail: String) -> Element {
|
||||
rsx! {
|
||||
div { class: "phase-item",
|
||||
span { class: "phase-num", "{num}" }
|
||||
span { class: "phase-name", "{name}" }
|
||||
span { class: "phase-detail", "{detail}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === INGEST — Data on-ramp ===
|
||||
|
||||
#[component]
|
||||
fn IngestPanel() -> Element {
|
||||
let mut pg_host = use_signal(|| "127.0.0.1".to_string());
|
||||
let mut pg_db = use_signal(|| "knowledge_base".to_string());
|
||||
let mut pg_tables = use_signal(|| None::<Vec<String>>);
|
||||
let mut pg_result = use_signal(|| None::<Result<serde_json::Value, String>>);
|
||||
let mut pg_loading = use_signal(|| false);
|
||||
|
||||
let list_tables = move |_| {
|
||||
let host = pg_host.read().clone();
|
||||
let db = pg_db.read().clone();
|
||||
spawn(async move {
|
||||
pg_loading.set(true);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client.post(&format!("{}/ingest/postgres/tables", api_base()))
|
||||
.json(&serde_json::json!({"host": host, "port": 5432, "database": db, "user": "postgres", "password": ""}))
|
||||
.send().await;
|
||||
match resp {
|
||||
Ok(r) => {
|
||||
if let Ok(tables) = r.json::<Vec<String>>().await {
|
||||
pg_tables.set(Some(tables));
|
||||
}
|
||||
}
|
||||
Err(e) => pg_tables.set(None),
|
||||
}
|
||||
pg_loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
let mut import_table = move |table: String| {
|
||||
let host = pg_host.read().clone();
|
||||
let db = pg_db.read().clone();
|
||||
spawn(async move {
|
||||
pg_result.set(None);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client.post(&format!("{}/ingest/postgres/import", api_base()))
|
||||
.json(&serde_json::json!({"host": host, "port": 5432, "database": db, "user": "postgres", "password": "", "table": table}))
|
||||
.send().await;
|
||||
match resp {
|
||||
Ok(r) => {
|
||||
match r.json::<serde_json::Value>().await {
|
||||
Ok(v) => pg_result.set(Some(Ok(v))),
|
||||
Err(e) => pg_result.set(Some(Err(e.to_string()))),
|
||||
}
|
||||
}
|
||||
Err(e) => pg_result.set(Some(Err(e.to_string()))),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div { class: "panel",
|
||||
h2 { "Data Ingest" }
|
||||
p { class: "subtitle", "Bring data in from files, databases, or the auto-watch inbox" }
|
||||
|
||||
div { class: "panel-section",
|
||||
h3 { "File Upload" }
|
||||
p { class: "hint", "POST a file to /ingest/file — or drop it in ./inbox/ for auto-ingest" }
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Supported Formats" }
|
||||
div { class: "arch-items", "CSV (auto-schema) | JSON (nested flattening) | PDF (text extraction) | Text/SMS" }
|
||||
}
|
||||
div { class: "arch-card",
|
||||
div { class: "arch-title", "Auto-Watch Inbox" }
|
||||
div { class: "arch-items", "Drop files in ./inbox/ → auto-detected → Parquet → queryable in <15s" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "panel-section",
|
||||
h3 { "PostgreSQL Import" }
|
||||
div { class: "form-row",
|
||||
label { "Host" }
|
||||
input { value: "{pg_host}", oninput: move |e| pg_host.set(e.value()) }
|
||||
}
|
||||
div { class: "form-row",
|
||||
label { "Database" }
|
||||
input { value: "{pg_db}", oninput: move |e| pg_db.set(e.value()) }
|
||||
}
|
||||
button { class: "btn", disabled: *pg_loading.read(), onclick: list_tables, "List Tables" }
|
||||
|
||||
if let Some(tables) = pg_tables.read().as_ref() {
|
||||
div { class: "table-list",
|
||||
for table in tables.iter() {
|
||||
{
|
||||
let t = table.clone();
|
||||
let t2 = table.clone();
|
||||
rsx! {
|
||||
div { class: "table-item",
|
||||
span { "{t}" }
|
||||
button { class: "btn btn-sm", onclick: move |_| import_table(t2.clone()), "Import" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(result) = pg_result.read().as_ref() {
|
||||
match result {
|
||||
Ok(v) => rsx! {
|
||||
div { class: "result-box",
|
||||
pre { {serde_json::to_string_pretty(v).unwrap_or_default()} }
|
||||
}
|
||||
},
|
||||
Err(e) => rsx! { div { class: "error", "{e}" } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === STATUS — System health ===
|
||||
|
||||
#[component]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user