#include "tabs.h" #include "core/icons_tabler.h" #include "core/empty_state.h" #include "core/badge.h" #include #include #include #include #include #include namespace data_factory_ui { // --------------------------------------------------------------------------- // 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); } static BadgeVariant variant_for_status(const std::string& st) { if (st == "success") return BadgeVariant::Success; if (st == "failed") return BadgeVariant::Error; if (st == "running") return BadgeVariant::Warning; if (st == "cancelled") return BadgeVariant::Default; return BadgeVariant::Default; } // 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 // --------------------------------------------------------------------------- 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) { int cols = show_schedule ? 6 : 5; if (!ImGui::BeginTable(table_id, cols, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { return; } ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.20f); ImGui::TableSetupColumn("Function", ImGuiTableColumnFlags_WidthStretch, 0.30f); if (show_schedule) ImGui::TableSetupColumn("Schedule", ImGuiTableColumnFlags_WidthStretch, 0.12f); ImGui::TableSetupColumn("Last Run", ImGuiTableColumnFlags_WidthStretch, 0.18f); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.10f); ImGui::TableSetupColumn("Rows/KB", ImGuiTableColumnFlags_WidthStretch, 0.10f); ImGui::TableHeadersRow(); int row_idx = 0; for (auto& n : nodes) { if (n.kind != filter_kind) continue; ImGui::PushID(row_idx++); ImGui::TableNextRow(); // Name (selectable -> sets selection) ImGui::TableNextColumn(); bool selected = (selection().node_id == n.id); if (ImGui::Selectable(n.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) { selection().node_id = n.id; // Invalidate function cache if function_id changed. if (function_cache().function_id != n.function_id) { function_cache() = {}; function_cache().function_id = n.function_id; } } if (!n.enabled) { ImGui::SameLine(); badge("disabled", BadgeVariant::Default); } // Function id ImGui::TableNextColumn(); if (n.function_id.empty()) ImGui::TextDisabled("(none)"); else ImGui::TextUnformatted(n.function_id.c_str()); // Schedule if (show_schedule) { ImGui::TableNextColumn(); if (n.schedule_cron.empty()) ImGui::TextDisabled("manual"); else ImGui::TextUnformatted(n.schedule_cron.c_str()); } // Last run const data_factory::Run* lr = last_run_for(n.id, runs_all); ImGui::TableNextColumn(); if (lr) ImGui::TextUnformatted(lr->started_at.c_str()); else ImGui::TextDisabled("-"); // Status badge ImGui::TableNextColumn(); if (lr) badge(lr->status.c_str(), variant_for_status(lr->status)); else ImGui::TextDisabled("-"); // Rows / KB ImGui::TableNextColumn(); if (lr) ImGui::Text("%lld / %lld", lr->rows_out, lr->kb_out); else ImGui::TextDisabled("-"); ImGui::PopID(); } ImGui::EndTable(); } // --------------------------------------------------------------------------- // 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(); } // --------------------------------------------------------------------------- // 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; } if (ImGui::BeginTable("##df_databases", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch, 0.18f); ImGui::TableSetupColumn("Kind", ImGuiTableColumnFlags_WidthStretch, 0.10f); ImGui::TableSetupColumn("URI", ImGuiTableColumnFlags_WidthStretch, 0.32f); ImGui::TableSetupColumn("Tables", ImGuiTableColumnFlags_WidthStretch, 0.10f); ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthStretch, 0.15f); ImGui::TableSetupColumn("Last Seen",ImGuiTableColumnFlags_WidthStretch, 0.15f); ImGui::TableHeadersRow(); for (auto& d : dbs) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(d.label.empty() ? d.id.c_str() : d.label.c_str()); ImGui::TableNextColumn(); badge(d.kind.c_str(), BadgeVariant::Info); ImGui::TableNextColumn(); ImGui::TextUnformatted(d.uri.c_str()); ImGui::TableNextColumn(); ImGui::Text("%lld", d.table_count); ImGui::TableNextColumn(); ImGui::TextUnformatted(format_bytes(d.size_bytes).c_str()); ImGui::TableNextColumn(); if (d.last_seen_at.empty()) ImGui::TextDisabled("-"); else ImGui::TextUnformatted(d.last_seen_at.c_str()); } ImGui::EndTable(); } 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; if (ImGui::BeginTable("##df_kpis", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame)) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%s Runs (24h)", TI_ACTIVITY); ImGui::Text("%d", runs_24h); ImGui::TextDisabled("success: %d", success_24h); ImGui::TableNextColumn(); ImGui::Text("%s Success rate", TI_CHECK); ImGui::TextColored(ImVec4(0.30f, 0.85f, 0.40f, 1), "%.1f%%", success_rate); ImGui::TextDisabled("%d / %d terminal", success_all, terminal); ImGui::TableNextColumn(); ImGui::Text("%s Failed (24h)", TI_ALERT_TRIANGLE); if (failed_24h > 0) ImGui::TextColored(ImVec4(0.95f, 0.35f, 0.30f, 1), "%d", failed_24h); else ImGui::Text("%d", failed_24h); ImGui::TextDisabled("pending: %d", pending_total); ImGui::TableNextColumn(); ImGui::Text("%s Throughput (24h)", TI_BOLT); ImGui::Text("%lld rows", rows_24h); ImGui::TextDisabled("%lld KB", kb_24h); ImGui::EndTable(); } 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); } } // 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); int shown = 0; if (ImGui::BeginTable("##df_node_runs", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Started", ImGuiTableColumnFlags_WidthStretch, 0.30f); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.12f); ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_WidthStretch, 0.13f); ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthStretch, 0.10f); ImGui::TableSetupColumn("Trigger", ImGuiTableColumnFlags_WidthStretch, 0.15f); ImGui::TableHeadersRow(); for (auto& r : runs_all) { if (r.node_id != nid) continue; if (shown >= 10) break; shown++; ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(r.started_at.c_str()); ImGui::TableNextColumn(); badge(r.status.c_str(), variant_for_status(r.status)); ImGui::TableNextColumn(); ImGui::TextUnformatted(format_duration(r.duration_ms).c_str()); ImGui::TableNextColumn(); ImGui::Text("%lld", r.rows_out); ImGui::TableNextColumn(); ImGui::TextUnformatted(r.trigger.c_str()); } ImGui::EndTable(); } if (shown == 0) { ImGui::TextDisabled("(no runs for this node yet)"); } ImGui::End(); } } // namespace data_factory_ui