#include "tabs.h" #include "data_table/data_table.h" #include "core/data_table_types.h" #include "core/icons_tabler.h" #include "core/empty_state.h" #include "core/badge.h" #include #include #include #include #include #include #include namespace data_factory_ui { // data_table::State persistente por panel (issue 0081 pattern). static data_table::State g_st_tables; static data_table::State g_st_nodes_extractors; static data_table::State g_st_nodes_transformers; static data_table::State g_st_nodes_sinks; static data_table::State g_st_databases; static data_table::State g_st_kpis; static data_table::State g_st_node_runs; static data_table::State g_st_preview; // Backing storage for cell strings (owns chars referenced by TableInput.cells). static std::vector g_back_extractors; static std::vector g_ptrs_extractors; static std::vector g_back_transformers; static std::vector g_ptrs_transformers; static std::vector g_back_sinks; static std::vector g_ptrs_sinks; static std::vector g_back_tables; static std::vector g_ptrs_tables; static std::vector g_back_databases; static std::vector g_ptrs_databases; static std::vector g_back_kpis; static std::vector g_ptrs_kpis; static std::vector g_back_node_runs; static std::vector g_ptrs_node_runs; static std::vector g_back_preview; static std::vector g_ptrs_preview; // --------------------------------------------------------------------------- // Table preview state (populated by double-click in draw_tables). // --------------------------------------------------------------------------- struct PreviewSelection { std::string database_id; std::string table_name; data_factory::TablePreview cache; bool loading = false; bool loaded = false; std::string error; int offset = 0; int limit = 100; }; static PreviewSelection& preview_state() { static PreviewSelection s; return s; } static void cells_to_ptrs(const std::vector& backing, std::vector& ptrs) { ptrs.resize(backing.size()); for (size_t i = 0; i < backing.size(); i++) ptrs[i] = backing[i].c_str(); } // Shared BadgeRules. static std::vector run_status_badges() { return { {"success", "#22c55e", "success"}, {"failed", "#ef4444", "failed"}, {"running", "#3b82f6", "running"}, {"pending", "#94a3b8", "pending"}, {"cancelled", "#6b7280", "cancelled"}, }; } static std::vector kind_badges() { return { {"extractor", "#0ea5e9", "extractor"}, {"transformer", "#a855f7", "transformer"}, {"sink", "#f97316", "sink"}, {"database", "#14b8a6", "database"}, {"validator", "#eab308", "validator"}, }; } static std::vector enabled_badges() { return { {"yes", "#22c55e", "yes"}, {"no", "#6b7280", "no"}, }; } // CategoricalChip helpers (dot izquierda + texto, siempre visible). static std::vector run_status_chips() { return { {"success", "#22c55e"}, {"failed", "#ef4444"}, {"running", "#3b82f6"}, {"pending", "#94a3b8"}, {"cancelled", "#6b7280"}, }; } static std::vector kind_chips() { return { {"extractor", "#0ea5e9"}, {"transformer", "#a855f7"}, {"sink", "#f97316"}, {"database", "#14b8a6"}, {"validator", "#eab308"}, {"duckdb", "#f59e0b"}, {"sqlite", "#6366f1"}, }; } // --------------------------------------------------------------------------- // Globals // --------------------------------------------------------------------------- Selection& selection() { static Selection s; return s; } FunctionCache& function_cache() { static FunctionCache fc; return fc; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static std::string format_duration(long long ms) { if (ms <= 0) return "-"; if (ms < 1000) return std::to_string(ms) + "ms"; char buf[32]; std::snprintf(buf, sizeof(buf), "%.2fs", ms / 1000.0); return buf; } static std::string format_bytes(long long bytes) { if (bytes <= 0) return "-"; const double kb = 1024.0; const double mb = kb * 1024.0; const double gb = mb * 1024.0; char buf[32]; if (bytes < (long long)kb) std::snprintf(buf, sizeof(buf), "%lld B", bytes); else if (bytes < (long long)mb) std::snprintf(buf, sizeof(buf), "%.1f KB", bytes / kb); else if (bytes < (long long)gb) std::snprintf(buf, sizeof(buf), "%.1f MB", bytes / mb); else std::snprintf(buf, sizeof(buf), "%.2f GB", bytes / gb); return buf; } static time_t parse_rfc3339(const std::string& s) { if (s.size() < 19) return 0; std::tm tm{}; tm.tm_year = std::atoi(s.substr(0, 4).c_str()) - 1900; tm.tm_mon = std::atoi(s.substr(5, 2).c_str()) - 1; tm.tm_mday = std::atoi(s.substr(8, 2).c_str()); tm.tm_hour = std::atoi(s.substr(11, 2).c_str()); tm.tm_min = std::atoi(s.substr(14, 2).c_str()); tm.tm_sec = std::atoi(s.substr(17, 2).c_str()); tm.tm_isdst = -1; return std::mktime(&tm); } // Pick most-recent run per node from runs_all. static const data_factory::Run* last_run_for( const std::string& node_id, const std::vector& runs_all) { const data_factory::Run* best = nullptr; for (auto& r : runs_all) { if (r.node_id != node_id) continue; if (!best || r.started_at > best->started_at) best = &r; } return best; } // --------------------------------------------------------------------------- // Generic kind table — used by extractors / transformers / sinks // --------------------------------------------------------------------------- // Render the node table for a given kind via data_table::render. // `kind_label` selects which static State/backing to use (one per kind so the // three calls don't clobber each other's sort/filter/breadcrumb state). static void draw_node_table(const char* /*table_id*/, const std::vector& nodes, const std::string& filter_kind, const std::vector& runs_all, bool show_schedule) { // Pick the per-kind state + backing. data_table::State* st = &g_st_nodes_extractors; std::vector* backing = &g_back_extractors; std::vector* ptrs = &g_ptrs_extractors; const char* dt_id = "##dt_extractors"; if (filter_kind == "transformer") { st = &g_st_nodes_transformers; backing = &g_back_transformers; ptrs = &g_ptrs_transformers; dt_id = "##dt_transformers"; } else if (filter_kind == "sink") { st = &g_st_nodes_sinks; backing = &g_back_sinks; ptrs = &g_ptrs_sinks; dt_id = "##dt_sinks"; } // Filter nodes for the current kind, and pre-resolve their last-run. std::vector filtered; filtered.reserve(nodes.size()); for (auto& n : nodes) if (n.kind == filter_kind) filtered.push_back(&n); data_table::TableInput tbl; tbl.name = filter_kind; if (show_schedule) { tbl.headers = {"Name", "Function", "Schedule", "Last Run", "Status", "Rows/KB", "Enabled"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; } else { tbl.headers = {"Name", "Function", "Last Run", "Status", "Rows/KB", "Enabled"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; } tbl.rows = static_cast(filtered.size()); tbl.cols = static_cast(tbl.headers.size()); tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; // Function column (Code renderer not in v1.3.x; keep Text — function_id is plain). // Status column (CategoricalChip — dot izquierda + texto, siempre visible). int status_col = show_schedule ? 4 : 3; tbl.column_specs[status_col].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[status_col].chips = run_status_chips(); // Enabled column (CategoricalChip yes/no). int enabled_col = tbl.cols - 1; tbl.column_specs[enabled_col].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[enabled_col].chips = { {"yes", "#22c55e"}, {"no", "#6b7280"}, }; backing->clear(); backing->reserve(filtered.size() * tbl.cols); for (auto* pn : filtered) { const data_factory::Node& n = *pn; const data_factory::Run* lr = last_run_for(n.id, runs_all); backing->push_back(n.name); backing->push_back(n.function_id.empty() ? "(none)" : n.function_id); if (show_schedule) backing->push_back(n.schedule_cron.empty() ? "manual" : n.schedule_cron); backing->push_back(lr ? lr->started_at : "-"); backing->push_back(lr ? lr->status : "-"); if (lr) { char buf[64]; std::snprintf(buf, sizeof(buf), "%lld / %lld", lr->rows_out, lr->kb_out); backing->push_back(buf); } else { backing->push_back("-"); } backing->push_back(n.enabled ? "yes" : "no"); } cells_to_ptrs(*backing, *ptrs); tbl.cells = ptrs->data(); std::vector events; data_table::render(dt_id, {tbl}, *st, &events); for (auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(filtered.size())) { const data_factory::Node& n = *filtered[ev.row]; selection().node_id = n.id; if (function_cache().function_id != n.function_id) { function_cache() = {}; function_cache().function_id = n.function_id; } } } } // --------------------------------------------------------------------------- // Extractors // --------------------------------------------------------------------------- void draw_extractors(const std::string& /*api_url*/, const std::vector& nodes, const std::vector& runs_all) { if (!ImGui::Begin(TI_DOWNLOAD " Extractors")) { ImGui::End(); return; } int count = 0; for (auto& n : nodes) if (n.kind == "extractor") count++; if (count == 0) { empty_state(TI_DOWNLOAD, "No extractors", "Register an extractor node via sqlite_api."); ImGui::End(); return; } ImGui::TextDisabled("%d extractor nodes. Click row -> see Node Detail.", count); draw_node_table("##df_extractors", nodes, "extractor", runs_all, true); ImGui::End(); } // --------------------------------------------------------------------------- // Transformers // --------------------------------------------------------------------------- void draw_transformers(const std::string& /*api_url*/, const std::vector& nodes, const std::vector& runs_all) { if (!ImGui::Begin(TI_REFRESH " Transformers")) { ImGui::End(); return; } int count = 0; for (auto& n : nodes) if (n.kind == "transformer") count++; if (count == 0) { empty_state(TI_REFRESH, "No transformers", "Register a transformer node via sqlite_api."); ImGui::End(); return; } ImGui::TextDisabled("%d transformer nodes.", count); draw_node_table("##df_transformers", nodes, "transformer", runs_all, false); ImGui::End(); } // --------------------------------------------------------------------------- // Sinks // --------------------------------------------------------------------------- void draw_sinks(const std::string& /*api_url*/, const std::vector& nodes, const std::vector& runs_all) { if (!ImGui::Begin(TI_UPLOAD " Sinks")) { ImGui::End(); return; } int count = 0; for (auto& n : nodes) if (n.kind == "sink") count++; if (count == 0) { empty_state(TI_UPLOAD, "No sinks", "Register a sink node via sqlite_api."); ImGui::End(); return; } ImGui::TextDisabled("%d sink nodes.", count); draw_node_table("##df_sinks", nodes, "sink", runs_all, false); ImGui::End(); } // --------------------------------------------------------------------------- // Tables // --------------------------------------------------------------------------- void draw_tables(const std::string& /*api_url*/, const std::vector& tables) { if (!ImGui::Begin(TI_TABLE " Tables")) { ImGui::End(); return; } if (tables.empty()) { empty_state(TI_TABLE, "No tables found", "Register databases in data_factory.db to see their tables here."); ImGui::End(); return; } // Count errors separately. int error_count = 0; for (auto& t : tables) if (!t.error.empty()) error_count++; if (error_count > 0) { ImGui::TextDisabled("%zu tables across all databases (%d error(s)).", tables.size(), error_count); } else { ImGui::TextDisabled("%zu tables across all databases.", tables.size()); } { data_table::TableInput tbl; tbl.name = "tables"; tbl.headers = {"Database", "Kind", "Table", "Rows"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Int, }; tbl.rows = static_cast(tables.size()); tbl.cols = static_cast(tbl.headers.size()); tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; // Kind column: CategoricalChip (duckdb = amber, sqlite = indigo). tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[1].chips = kind_chips(); // Rows column: ColorScale (0 = dim, up to 10000 = bright). tbl.column_specs[3].renderer = data_table::CellRenderer::ColorScale; tbl.column_specs[3].range_min = 0.0; tbl.column_specs[3].range_max = 10000.0; tbl.column_specs[3].range_alpha = 0.25f; g_back_tables.clear(); g_back_tables.reserve(tables.size() * tbl.cols); for (auto& t : tables) { g_back_tables.push_back(t.database_label.empty() ? t.database_id : t.database_label); g_back_tables.push_back(t.database_kind); // Show table name; if error, show the error text in the table cell. g_back_tables.push_back(t.error.empty() ? t.table_name : t.table_name + " — " + t.error); { char buf[32]; std::snprintf(buf, sizeof(buf), "%lld", t.row_count); g_back_tables.push_back(buf); } } cells_to_ptrs(g_back_tables, g_ptrs_tables); tbl.cells = g_ptrs_tables.data(); std::vector tbl_events; data_table::render("##dt_tables", {tbl}, g_st_tables, &tbl_events); for (auto& ev : tbl_events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(tables.size())) { const auto& t = tables[ev.row]; auto& ps = preview_state(); // Only reset if selection changed. if (ps.database_id != t.database_id || ps.table_name != t.table_name) { ps.database_id = t.database_id; ps.table_name = t.table_name; ps.loaded = false; ps.loading = false; ps.error.clear(); ps.offset = 0; ps.cache = data_factory::TablePreview{}; } } } } ImGui::End(); } // --------------------------------------------------------------------------- // Databases // --------------------------------------------------------------------------- void draw_databases(const std::string& /*api_url*/, const std::vector& dbs) { if (!ImGui::Begin(TI_DATABASE " Databases")) { ImGui::End(); return; } if (dbs.empty()) { empty_state(TI_DATABASE, "No databases registered", "POST /api/datafactory/databases to register a DB."); ImGui::End(); return; } { data_table::TableInput tbl; tbl.name = "databases"; tbl.headers = {"Label", "Kind", "URI", "Tables", "Size", "Last Seen"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Int, data_table::ColumnType::String, data_table::ColumnType::String, }; tbl.rows = static_cast(dbs.size()); tbl.cols = static_cast(tbl.headers.size()); tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[1].chips = kind_chips(); g_back_databases.clear(); g_back_databases.reserve(dbs.size() * tbl.cols); for (auto& d : dbs) { g_back_databases.push_back(d.label.empty() ? d.id : d.label); g_back_databases.push_back(d.kind); g_back_databases.push_back(d.uri); { char buf[32]; std::snprintf(buf, sizeof(buf), "%lld", d.table_count); g_back_databases.push_back(buf); } g_back_databases.push_back(format_bytes(d.size_bytes)); g_back_databases.push_back(d.last_seen_at.empty() ? "-" : d.last_seen_at); } cells_to_ptrs(g_back_databases, g_ptrs_databases); tbl.cells = g_ptrs_databases.data(); data_table::render("##dt_databases", {tbl}, g_st_databases); } ImGui::End(); } // --------------------------------------------------------------------------- // Health // --------------------------------------------------------------------------- void draw_health(const std::string& /*api_url*/, const std::vector& runs_all) { if (!ImGui::Begin(TI_ACTIVITY " Health")) { ImGui::End(); return; } if (runs_all.empty()) { empty_state(TI_ACTIVITY, "No runs yet", "Trigger a node to populate health metrics."); ImGui::End(); return; } const time_t now = std::time(nullptr); const time_t cutoff_24h = now - 86400; int runs_24h = 0, success_24h = 0, failed_24h = 0; int pending_total = 0; long long rows_24h = 0, kb_24h = 0; int success_all = 0, failed_all = 0, cancelled_all = 0; for (auto& r : runs_all) { if (r.status == "running" || r.status == "pending") pending_total++; if (r.status == "success") success_all++; if (r.status == "failed") failed_all++; if (r.status == "cancelled") cancelled_all++; time_t t = parse_rfc3339(r.started_at); if (t == 0 || t < cutoff_24h) continue; runs_24h++; rows_24h += r.rows_out; kb_24h += r.kb_out; if (r.status == "success") success_24h++; if (r.status == "failed") failed_24h++; } int terminal = success_all + failed_all + cancelled_all; float success_rate = (terminal > 0) ? (100.0f * (float)success_all / (float)terminal) : 0.0f; { data_table::TableInput tbl; tbl.name = "kpis"; tbl.headers = {"KPI", "Value", "Detail"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; tbl.rows = 4; tbl.cols = 3; tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; g_back_kpis.clear(); g_back_kpis.reserve(tbl.rows * tbl.cols); char buf[64]; // Runs (24h) g_back_kpis.push_back("Runs (24h)"); std::snprintf(buf, sizeof(buf), "%d", runs_24h); g_back_kpis.push_back(buf); std::snprintf(buf, sizeof(buf), "success: %d", success_24h); g_back_kpis.push_back(buf); // Success rate g_back_kpis.push_back("Success rate"); std::snprintf(buf, sizeof(buf), "%.1f%%", success_rate); g_back_kpis.push_back(buf); std::snprintf(buf, sizeof(buf), "%d / %d terminal", success_all, terminal); g_back_kpis.push_back(buf); // Failed (24h) g_back_kpis.push_back("Failed (24h)"); std::snprintf(buf, sizeof(buf), "%d", failed_24h); g_back_kpis.push_back(buf); std::snprintf(buf, sizeof(buf), "pending: %d", pending_total); g_back_kpis.push_back(buf); // Throughput (24h) g_back_kpis.push_back("Throughput (24h)"); std::snprintf(buf, sizeof(buf), "%lld rows", rows_24h); g_back_kpis.push_back(buf); std::snprintf(buf, sizeof(buf), "%lld KB", kb_24h); g_back_kpis.push_back(buf); cells_to_ptrs(g_back_kpis, g_ptrs_kpis); tbl.cells = g_ptrs_kpis.data(); data_table::render("##dt_kpis", {tbl}, g_st_kpis); } ImGui::Separator(); ImGui::TextDisabled("Computed client-side from %zu runs in cache.", runs_all.size()); ImGui::End(); } // --------------------------------------------------------------------------- // Map (placeholder: flat tree by kind) // --------------------------------------------------------------------------- void draw_map(const std::string& /*api_url*/, const std::vector& nodes) { if (!ImGui::Begin(TI_LIST " Map")) { ImGui::End(); return; } if (nodes.empty()) { empty_state(TI_LIST, "No nodes", "Register nodes via sqlite_api /api/datafactory/nodes."); ImGui::End(); return; } // Group by kind. std::map> by_kind; for (auto& n : nodes) by_kind[n.kind].push_back(&n); static const char* kind_order[] = { "extractor", "transformer", "database", "sink", "validator" }; static const char* kind_label[] = { "Extractors", "Transformers", "Databases", "Sinks", "Validators" }; const int n_kinds = sizeof(kind_order) / sizeof(kind_order[0]); for (int i = 0; i < n_kinds; i++) { auto it = by_kind.find(kind_order[i]); int count = (it != by_kind.end()) ? (int)it->second.size() : 0; char header[128]; std::snprintf(header, sizeof(header), "%s (%d)###%s_hdr", kind_label[i], count, kind_order[i]); if (!ImGui::TreeNodeEx(header, ImGuiTreeNodeFlags_DefaultOpen)) continue; if (count == 0) { ImGui::TextDisabled(" (empty)"); } else { for (auto* p : it->second) { ImGui::PushID(p->id.c_str()); ImGui::Bullet(); ImGui::SameLine(); bool sel = (selection().node_id == p->id); if (ImGui::Selectable(p->name.c_str(), sel)) { selection().node_id = p->id; if (function_cache().function_id != p->function_id) { function_cache() = {}; function_cache().function_id = p->function_id; } } ImGui::SameLine(); if (!p->function_id.empty()) { ImGui::TextDisabled(" -> %s", p->function_id.c_str()); } ImGui::PopID(); } } ImGui::TreePop(); } ImGui::End(); } // --------------------------------------------------------------------------- // Node detail panel (side) // --------------------------------------------------------------------------- void draw_node_detail_panel(const std::string& api_url, const std::vector& nodes, const std::vector& runs_all, bool* p_open) { if (!ImGui::Begin(TI_INFO_CIRCLE " Node Detail", p_open)) { ImGui::End(); return; } const std::string& nid = selection().node_id; if (nid.empty()) { empty_state(TI_INFO_CIRCLE, "Nothing selected", "Click a row in any tab to inspect the node."); ImGui::End(); return; } const data_factory::Node* node = nullptr; for (auto& n : nodes) if (n.id == nid) { node = &n; break; } if (!node) { ImGui::TextDisabled("Node not in current cache: %s", nid.c_str()); ImGui::End(); return; } // Header ImGui::Text("%s %s", TI_INFO_CIRCLE, node->name.c_str()); ImGui::SameLine(); badge(node->kind.c_str(), BadgeVariant::Info); if (!node->enabled) { ImGui::SameLine(); badge("disabled", BadgeVariant::Default); } ImGui::Separator(); if (!node->description.empty()) { ImGui::TextWrapped("%s", node->description.c_str()); ImGui::Separator(); } ImGui::Text("id: %s", node->id.c_str()); ImGui::Text("kind: %s", node->kind.c_str()); if (!node->function_id.empty()) { ImGui::Text("function: %s", node->function_id.c_str()); } if (!node->schedule_cron.empty()) { ImGui::Text("schedule: %s", node->schedule_cron.c_str()); } if (!node->tags.empty()) { ImGui::Text("tags:"); for (auto& t : node->tags) { ImGui::SameLine(); badge(t.c_str(), BadgeVariant::Default); } } // Storage info: derive from most recent run with storage populated. { const data_factory::Run* latest_with_storage = nullptr; for (auto& r : runs_all) { if (r.node_id != nid) continue; if (r.storage_db_id.empty() && r.storage_table.empty()) continue; latest_with_storage = &r; break; } if (latest_with_storage) { ImGui::Separator(); ImGui::Text("%s Storage", TI_DATABASE); ImGui::SameLine(); badge(latest_with_storage->storage_db_id.c_str(), BadgeVariant::Info); ImGui::SameLine(); ImGui::TextDisabled("table:"); ImGui::SameLine(); badge(latest_with_storage->storage_table.c_str(), BadgeVariant::Default); } } // Function metadata card (lazy load) if (!node->function_id.empty()) { ImGui::Separator(); auto& fc = function_cache(); if (fc.function_id != node->function_id) { fc = {}; fc.function_id = node->function_id; } if (!fc.loaded && fc.error.empty()) { if (data_factory::get_function_http(api_url, fc.function_id, fc.info)) { fc.loaded = true; } else { fc.error = "Failed to fetch /api/functions/" + fc.function_id; } } if (fc.loaded) { ImGui::Text("%s Registry function", TI_BOX); if (!fc.info.domain.empty()) { ImGui::SameLine(); badge(fc.info.domain.c_str(), BadgeVariant::Info); } if (!fc.info.purity.empty()) { ImGui::SameLine(); BadgeVariant v = (fc.info.purity == "pure") ? BadgeVariant::Success : BadgeVariant::Warning; badge(fc.info.purity.c_str(), v); } if (!fc.info.lang.empty()) { ImGui::SameLine(); badge(fc.info.lang.c_str(), BadgeVariant::Default); } if (!fc.info.signature.empty()) { ImGui::TextWrapped("sig: %s", fc.info.signature.c_str()); } if (!fc.info.description.empty()) { ImGui::TextWrapped("%s", fc.info.description.c_str()); } } else if (!fc.error.empty()) { ImGui::TextColored(ImVec4(0.95f, 0.4f, 0.4f, 1), "%s", fc.error.c_str()); } else { ImGui::TextDisabled("Loading function metadata..."); } } // Recent runs (top 10) ImGui::Separator(); ImGui::Text("%s Recent runs", TI_HISTORY); // Filter + cap. std::vector shown_runs; shown_runs.reserve(10); for (auto& r : runs_all) { if (r.node_id != nid) continue; shown_runs.push_back(&r); if (shown_runs.size() >= 10) break; } if (shown_runs.empty()) { ImGui::TextDisabled("(no runs for this node yet)"); } else { data_table::TableInput tbl; tbl.name = "node_runs"; tbl.headers = {"Started", "Status", "Duration (ms)", "Rows", "Trigger", "Storage DB", "Table"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Float, data_table::ColumnType::Int, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; tbl.rows = static_cast(shown_runs.size()); tbl.cols = 7; tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[1].chips = run_status_chips(); tbl.column_specs[2].renderer = data_table::CellRenderer::ColorScale; tbl.column_specs[2].range_min = 0.0; tbl.column_specs[2].range_max = 5000.0; tbl.column_specs[2].range_alpha = 0.30f; // Default 3-stop green→amber→red usado si range_stops vacio (helper interno). // Mantenemos Duration badges semanticos en el viejo path? — no, ColorScale tinta fondo. tbl.column_specs[2].duration_warn_ms = 1000.0f; tbl.column_specs[2].duration_error_ms = 5000.0f; g_back_node_runs.clear(); g_back_node_runs.reserve(shown_runs.size() * tbl.cols); for (auto* pr : shown_runs) { const data_factory::Run& r = *pr; g_back_node_runs.push_back(r.started_at); g_back_node_runs.push_back(r.status); { char buf[32]; std::snprintf(buf, sizeof(buf), "%lld", r.duration_ms); g_back_node_runs.push_back(buf); } { char buf[32]; std::snprintf(buf, sizeof(buf), "%lld", r.rows_out); g_back_node_runs.push_back(buf); } g_back_node_runs.push_back(r.trigger); g_back_node_runs.push_back(r.storage_db_id); g_back_node_runs.push_back(r.storage_table); } cells_to_ptrs(g_back_node_runs, g_ptrs_node_runs); tbl.cells = g_ptrs_node_runs.data(); std::vector events; data_table::render("##dt_node_runs", {tbl}, g_st_node_runs, &events); for (auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(shown_runs.size())) { selection().run_id = shown_runs[ev.row]->id; } } } ImGui::End(); } // --------------------------------------------------------------------------- // Table preview panel (opened by double-click in Tables tab) // --------------------------------------------------------------------------- void draw_table_preview_panel(const std::string& api_url, bool* p_open) { if (!ImGui::Begin(TI_EYE " Table Preview", p_open)) { ImGui::End(); return; } auto& ps = preview_state(); if (ps.database_id.empty()) { empty_state(TI_EYE, "No table selected", "Double-click a row in the Tables tab to preview its data."); ImGui::End(); return; } // Lazy fetch: trigger blocking HTTP fetch on the render thread. // This is KISS — data_factory previews are used interactively; the one-off // blocking call completes in <200ms over loopback. if (!ps.loaded && !ps.loading && ps.error.empty()) { ps.loading = true; bool ok = data_factory::get_table_preview_http( api_url, ps.database_id, ps.table_name, ps.limit, ps.offset, ps.cache); ps.loading = false; if (ok) { ps.loaded = true; } else { ps.error = "Failed to fetch preview for " + ps.database_id + "." + ps.table_name; } } // Header: "database_id . table_name (total_rows rows)" { char hdr[256]; long long total = ps.loaded ? ps.cache.total_rows : 0; std::snprintf(hdr, sizeof(hdr), "%s %s (%lld rows)", ps.database_id.c_str(), ps.table_name.c_str(), total); ImGui::Text("%s %s", TI_TABLE, hdr); } ImGui::Separator(); if (!ps.loaded && ps.error.empty()) { ImGui::TextDisabled("Loading..."); ImGui::End(); return; } if (!ps.error.empty()) { ImGui::TextColored(ImVec4(0.95f, 0.4f, 0.4f, 1), "%s", ps.error.c_str()); ImGui::End(); return; } // Schema summary. if (!ps.cache.columns.empty()) { std::string schema_line; for (size_t i = 0; i < ps.cache.columns.size(); i++) { if (i > 0) schema_line += " | "; schema_line += ps.cache.columns[i].first + " (" + ps.cache.columns[i].second + ")"; } ImGui::TextDisabled("%s", schema_line.c_str()); ImGui::Separator(); } // Data table via data_table::render. if (!ps.cache.columns.empty()) { int ncols = static_cast(ps.cache.columns.size()); int nrows = static_cast(ps.cache.rows.size()); data_table::TableInput tbl; tbl.name = "preview"; tbl.headers.reserve(ncols); tbl.types.reserve(ncols); tbl.column_specs.resize(ncols); for (int i = 0; i < ncols; i++) { tbl.headers.push_back(ps.cache.columns[i].first); tbl.types.push_back(data_table::ColumnType::String); tbl.column_specs[i].id = ps.cache.columns[i].first; } tbl.rows = nrows; tbl.cols = ncols; // Build flat backing array row-major. g_back_preview.clear(); g_back_preview.reserve(static_cast(nrows) * ncols); for (auto& row : ps.cache.rows) { for (int c = 0; c < ncols; c++) { g_back_preview.push_back(c < static_cast(row.size()) ? row[c] : ""); } } cells_to_ptrs(g_back_preview, g_ptrs_preview); tbl.cells = g_ptrs_preview.data(); data_table::render("##dt_preview", {tbl}, g_st_preview, nullptr, true); } // Pagination controls. ImGui::Separator(); { long long showing_from = ps.cache.offset + 1; long long showing_to = ps.cache.offset + static_cast(ps.cache.rows.size()); long long total = ps.cache.total_rows; if (ps.cache.rows.empty()) showing_from = 0; char info[128]; std::snprintf(info, sizeof(info), "showing %lld-%lld of %lld", showing_from, showing_to, total); ImGui::TextDisabled("%s", info); ImGui::SameLine(); bool can_prev = (ps.offset > 0); bool can_next = (ps.offset + ps.limit < static_cast(ps.cache.total_rows)); if (!can_prev) ImGui::BeginDisabled(); if (ImGui::SmallButton("< Prev")) { ps.offset = std::max(0, ps.offset - ps.limit); ps.loaded = false; ps.error.clear(); ps.cache = data_factory::TablePreview{}; } if (!can_prev) ImGui::EndDisabled(); ImGui::SameLine(); if (!can_next) ImGui::BeginDisabled(); if (ImGui::SmallButton("Next >")) { ps.offset += ps.limit; ps.loaded = false; ps.error.clear(); ps.cache = data_factory::TablePreview{}; } if (!can_next) ImGui::EndDisabled(); } ImGui::End(); } } // namespace data_factory_ui