From 8c5152fca4ec5a1250e55199a5c3072d0e6f6961 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 17 May 2026 00:07:04 +0200 Subject: [PATCH] docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard) Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 6 +- app.md | 5 + data/hn_top_stories.duckdb | Bin 0 -> 1060864 bytes data_factory.db-shm | Bin 32768 -> 32768 bytes data_factory.db-wal | Bin 90672 -> 585072 bytes data_http.cpp | 92 +++ data_http.h | 30 + main.cpp | 47 +- migrations/002_add_storage_columns.sql | 5 + operations.db | 0 tabs.cpp | 745 ++++++++++++++++++++----- tabs.h | 5 + 12 files changed, 765 insertions(+), 170 deletions(-) create mode 100644 data/hn_top_stories.duckdb create mode 100644 migrations/002_add_storage_columns.sql create mode 100644 operations.db diff --git a/CMakeLists.txt b/CMakeLists.txt index ef3b85a..6b551ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,10 +9,8 @@ add_imgui_app(data_factory ) target_include_directories(data_factory PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -# fn_table_viz: optional, kept for parity with dag_engine_ui. -if(TARGET fn_table_viz) - target_link_libraries(data_factory PRIVATE fn_table_viz) -endif() +# fn_module_data_table: required — tabs.cpp uses data_table::render (issue 0081). +target_link_libraries(data_factory PRIVATE fn_module_data_table) if(WIN32) target_link_libraries(data_factory PRIVATE ws2_32) diff --git a/app.md b/app.md index 7b21b6a..4592f0f 100644 --- a/app.md +++ b/app.md @@ -7,11 +7,16 @@ tags: [imgui, dashboard, data-pipeline, factory, http, websocket] uses_functions: - empty_state_cpp_core - badge_cpp_core + - data_table_cpp_viz uses_types: [] +uses_modules: [data_table_cpp] framework: "imgui" entry_point: "main.cpp" dir_path: "apps/data_factory" repo_url: "https://gitea.organic-machine.com/dataforge/data_factory" +icon: + phosphor: "factory" + accent: "#f97316" e2e_checks: - id: build_cmake cmd: "cmake --build cpp/build -j --target data_factory" diff --git a/data/hn_top_stories.duckdb b/data/hn_top_stories.duckdb new file mode 100644 index 0000000000000000000000000000000000000000..79434f4d75a389c6392fcd8170608cb9fe1915eb GIT binary patch literal 1060864 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
fCfu|3Ctar;l``kbJ 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 zqe3;xim+>9Uk&tQ6RXd<>Xu$N(X~mQ27VCqbQ-FWaaQE1QTO;|rqQg1REgiyI1Hkb zCr`Rdoxjr8EApYX^FROr1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL JK;VJ}{(qi-eBJ;6 literal 0 HcmV?d00001 diff --git a/data_factory.db-shm b/data_factory.db-shm index b2671e71263f00aed53a48f8c8bc21f39555089e..d6e924c6384d82b5c5ee4d4833d043fbfcbdd8fe 100644 GIT binary patch literal 32768 zcmeI**G?5t6o>H-0Vy^>RP2hqU~kw_>=k?Oy&M&L?-ljJ#8)srh;QMY@8GqrK`sm# zCmIuDhHob;GudnJIrIPJT+goi(m*n*ql%#FZCv|Dt-DoHXc_SCQ=z`C@cF~XudR)X z-#61#UH6CU<5BG+dG5ELM#nU>I&QkfwK%FIsx+#5RL`hhQN5%3L{&v4bIE;LFYOs= zb7_0i=E5T}X}76~Yu{h|Zra|o-z|&%L~3 z_e%a(o!{wi=i-qL1r$&~0RvV=wAWL9jR@0sWxfWQI>vV=w zAWL9zR@0sWxfbXir%m)?2qT%qG?viJ8a8n?*O?8czz>14I74PA^-N|uOIg8MHnW4h z9O5`I36#f)Ny8XL12br1CF^KmC;K?e3C?nn>)hcX&uGhCasw$) z5ocsoF`Us%VJ6F1#d@}|i~StoBU2J*}ztI zbAY3q;ykU~$g@M>iu-xZh{>F)UO5J(4m#z4^dykX6RN-|u zQq=>E0t1k_|B(Pxn2CX5Vq^T~BTNGJn=df&urV_-2yH&d^qYy9i9r~`U}g}3FjyEw zAq-XqF(BikAS)XKHv=z_`ICv6oq-P`$-%%6X0UQH@GuB~B_}_~XW4v+Nk#wwHL6Gm diff --git a/data_factory.db-wal b/data_factory.db-wal index f4a9b7bca0511013348e48040b8078a3d85ef32f..3ff5ed99f4c0fbd700acdc1bb81bf1ee68708589 100644 GIT binary patch literal 585072 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 out) { + std::string host; + int port; + if (!parse_url(api_url, host, port)) return false; + HttpClient cli(host, port); + auto res = cli.get("/api/datafactory/tables"); + if (!res.ok()) { + fprintf(stderr, "[df_http] list_tables failed: status=%d\n", res.status); + return false; + } + auto j = json::parse(res.body, nullptr, false); + if (!j.is_object() || !j.contains("tables") || !j["tables"].is_array()) { + return false; + } + out.clear(); + for (auto& item : j["tables"]) { + TableEntry t; + parse_table_entry(item, t); + out.push_back(std::move(t)); + } + return true; +} + bool get_function_http(const std::string& api_url, const std::string& function_id, FnInfo& out) { @@ -219,4 +254,61 @@ bool get_function_http(const std::string& api_url, return true; } +bool get_table_preview_http(const std::string& api_url, + const std::string& database_id, + const std::string& table, + int limit, int offset, + TablePreview& out) { + std::string host; + int port; + if (!parse_url(api_url, host, port)) return false; + if (database_id.empty() || table.empty()) return false; + HttpClient cli(host, port); + // Build query string manually (no URL encoding needed — IDs/table names + // are alphanumeric per the server-side validation regex). + char path[512]; + std::snprintf(path, sizeof(path), + "/api/datafactory/preview?database_id=%s&table=%s&limit=%d&offset=%d", + database_id.c_str(), table.c_str(), limit, offset); + auto res = cli.get(path); + if (!res.ok()) { + fprintf(stderr, "[df_http] get_table_preview(%s.%s) failed: status=%d body=%s\n", + database_id.c_str(), table.c_str(), res.status, res.body.c_str()); + return false; + } + auto j = json::parse(res.body, nullptr, false); + if (!j.is_object()) return false; + + out.database_id = get_str(j, "database_id"); + out.table_name = get_str(j, "table_name"); + out.total_rows = get_int64(j, "total_rows"); + out.limit = get_int64(j, "limit"); + out.offset = get_int64(j, "offset"); + + out.columns.clear(); + if (j.contains("columns") && j["columns"].is_array()) { + for (auto& c : j["columns"]) { + std::string name = get_str(c, "name"); + std::string type = get_str(c, "type"); + out.columns.emplace_back(name, type); + } + } + + out.rows.clear(); + if (j.contains("rows") && j["rows"].is_array()) { + for (auto& row : j["rows"]) { + if (!row.is_array()) continue; + std::vector r; + r.reserve(row.size()); + for (auto& cell : row) { + if (cell.is_string()) r.push_back(cell.get()); + else if (cell.is_null()) r.push_back(""); + else r.push_back(cell.dump()); + } + out.rows.push_back(std::move(r)); + } + } + return true; +} + } // namespace data_factory diff --git a/data_http.h b/data_http.h index bb469fb..f1cb600 100644 --- a/data_http.h +++ b/data_http.h @@ -34,6 +34,8 @@ struct Run { long long duration_ms = 0; std::string trigger; std::string error; + std::string storage_db_id; + std::string storage_table; }; struct DatabaseInfo { @@ -47,6 +49,25 @@ struct DatabaseInfo { std::string last_seen_at; }; +struct TableEntry { + std::string database_id; + std::string database_label; + std::string database_kind; + std::string table_name; + long long row_count = 0; + std::string error; +}; + +struct TablePreview { + std::string database_id; + std::string table_name; + std::vector> columns; // (name, type) + std::vector> rows; + long long total_rows = 0; + long long limit = 100; + long long offset = 0; +}; + // Mirrors dag_engine_ui FnInfo (response shape of GET /api/functions/{id}). struct FnInfo { std::string id; @@ -69,8 +90,17 @@ bool list_runs_http(const std::string& api_url, const std::string& node_id, bool list_databases_http(const std::string& api_url, std::vector& out); +bool list_tables_http(const std::string& api_url, + std::vector& out); + bool get_function_http(const std::string& api_url, const std::string& function_id, FnInfo& out); +bool get_table_preview_http(const std::string& api_url, + const std::string& database_id, + const std::string& table, + int limit, int offset, + TablePreview& out); + } // namespace data_factory diff --git a/main.cpp b/main.cpp index 1fc92a4..823dde1 100644 --- a/main.cpp +++ b/main.cpp @@ -26,20 +26,23 @@ static std::string g_ws_path = "/api/ws/datafactory"; static std::vector g_nodes; static std::vector g_runs_all; static std::vector g_databases; +static std::vector g_tables; static WsClient g_ws; static int g_ws_msg_count = 0; static bool g_initial_fetched = false; static bool g_refresh_pending = false; // Panel toggles. -static bool g_show_map = true; -static bool g_show_extractors = true; -static bool g_show_transformers= true; -static bool g_show_databases = true; -static bool g_show_sinks = true; -static bool g_show_health = true; -static bool g_show_detail = true; -static bool g_show_live = false; +static bool g_show_map = true; +static bool g_show_extractors = true; +static bool g_show_transformers = true; +static bool g_show_databases = true; +static bool g_show_tables = true; +static bool g_show_sinks = true; +static bool g_show_health = true; +static bool g_show_detail = true; +static bool g_show_table_preview = true; +static bool g_show_live = false; static void upsert_run(const data_factory::Run& r) { for (auto& existing : g_runs_all) { @@ -132,6 +135,7 @@ static void render() { g_refresh_pending = false; data_factory::list_nodes_http(g_api_url, "", g_nodes); data_factory::list_databases_http(g_api_url, g_databases); + data_factory::list_tables_http(g_api_url, g_tables); std::vector tmp; if (data_factory::list_runs_http(g_api_url, "", 200, tmp)) { for (auto& r : tmp) upsert_run(r); @@ -149,11 +153,14 @@ static void render() { if (g_show_extractors) data_factory_ui::draw_extractors(g_api_url, g_nodes, g_runs_all); if (g_show_transformers) data_factory_ui::draw_transformers(g_api_url, g_nodes, g_runs_all); if (g_show_databases) data_factory_ui::draw_databases(g_api_url, g_databases); + if (g_show_tables) data_factory_ui::draw_tables(g_api_url, g_tables); if (g_show_sinks) data_factory_ui::draw_sinks(g_api_url, g_nodes, g_runs_all); if (g_show_health) data_factory_ui::draw_health(g_api_url, g_runs_all); - if (g_show_detail) data_factory_ui::draw_node_detail_panel( - g_api_url, g_nodes, g_runs_all, &g_show_detail); - if (g_show_live) draw_live(); + if (g_show_detail) data_factory_ui::draw_node_detail_panel( + g_api_url, g_nodes, g_runs_all, &g_show_detail); + if (g_show_table_preview) data_factory_ui::draw_table_preview_panel( + g_api_url, &g_show_table_preview); + if (g_show_live) draw_live(); } // Self-test: blocking HTTP GET to sqlite_api /api/datafactory/nodes. No GUI. @@ -179,14 +186,16 @@ int main(int argc, char** argv) { g_ws.start(g_ws_host, g_ws_port, g_ws_path); static fn_ui::PanelToggle panels[] = { - { "Map", nullptr, &g_show_map }, - { "Extractors", nullptr, &g_show_extractors }, - { "Transformers", nullptr, &g_show_transformers }, - { "Databases", nullptr, &g_show_databases }, - { "Sinks", nullptr, &g_show_sinks }, - { "Health", nullptr, &g_show_health }, - { "Node Detail", nullptr, &g_show_detail }, - { "Live (WS)", nullptr, &g_show_live }, + { "Map", nullptr, &g_show_map }, + { "Extractors", nullptr, &g_show_extractors }, + { "Transformers", nullptr, &g_show_transformers }, + { "Databases", nullptr, &g_show_databases }, + { "Tables", nullptr, &g_show_tables }, + { "Table Preview", nullptr, &g_show_table_preview }, + { "Sinks", nullptr, &g_show_sinks }, + { "Health", nullptr, &g_show_health }, + { "Node Detail", nullptr, &g_show_detail }, + { "Live (WS)", nullptr, &g_show_live }, }; fn::AppConfig cfg; diff --git a/migrations/002_add_storage_columns.sql b/migrations/002_add_storage_columns.sql new file mode 100644 index 0000000..4f3d9b7 --- /dev/null +++ b/migrations/002_add_storage_columns.sql @@ -0,0 +1,5 @@ +-- Migration 002: track where each run's extracted data is stored. +-- Aditiva, idempotente (SQLite ALTER ADD COLUMN + "duplicate column" ignorado en app code). + +ALTER TABLE runs ADD COLUMN storage_db_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE runs ADD COLUMN storage_table TEXT NOT NULL DEFAULT ''; diff --git a/operations.db b/operations.db new file mode 100644 index 0000000..e69de29 diff --git a/tabs.cpp b/tabs.cpp index 88db5ad..39d5418 100644 --- a/tabs.cpp +++ b/tabs.cpp @@ -1,4 +1,6 @@ #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" @@ -9,9 +11,115 @@ #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 // --------------------------------------------------------------------------- @@ -64,14 +172,6 @@ static time_t parse_rfc3339(const std::string& s) { 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, @@ -89,83 +189,119 @@ static const data_factory::Run* last_run_for( // Generic kind table — used by extractors / transformers / sinks // --------------------------------------------------------------------------- -static void draw_node_table(const char* table_id, +// 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) { - int cols = show_schedule ? 6 : 5; - if (!ImGui::BeginTable(table_id, cols, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) - { - return; + // 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"; } - 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(); + // 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); - // Name (selectable -> sets selection) - ImGui::TableNextColumn(); - bool selected = (selection().node_id == n.id); - if (ImGui::Selectable(n.name.c_str(), selected, - ImGuiSelectableFlags_SpanAllColumns)) + 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; - // 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(); } // --------------------------------------------------------------------------- @@ -243,6 +379,101 @@ void draw_sinks(const std::string& /*api_url*/, 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 // --------------------------------------------------------------------------- @@ -260,35 +491,44 @@ void draw_databases(const std::string& /*api_url*/, 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(); + 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) { - 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()); + 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); } - ImGui::EndTable(); + 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(); } @@ -338,35 +578,45 @@ void draw_health(const std::string& /*api_url*/, 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(); + 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]; - ImGui::TableNextColumn(); - ImGui::Text("%s Runs (24h)", TI_ACTIVITY); - ImGui::Text("%d", runs_24h); - ImGui::TextDisabled("success: %d", success_24h); + 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); - 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); + cells_to_ptrs(g_back_kpis, g_ptrs_kpis); + tbl.cells = g_ptrs_kpis.data(); - 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(); + data_table::render("##dt_kpis", {tbl}, g_st_kpis); } ImGui::Separator(); @@ -498,6 +748,27 @@ void draw_node_detail_panel(const std::string& api_url, } } + // 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(); @@ -545,37 +816,217 @@ void draw_node_detail_panel(const std::string& api_url, // 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(); + // 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 == 0) { + + 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(); diff --git a/tabs.h b/tabs.h index ea97b34..1ae3eac 100644 --- a/tabs.h +++ b/tabs.h @@ -47,6 +47,9 @@ void draw_transformers(const std::string& api_url, void draw_databases(const std::string& api_url, const std::vector& dbs); +void draw_tables(const std::string& api_url, + const std::vector& tables); + void draw_sinks(const std::string& api_url, const std::vector& nodes, const std::vector& runs_all); @@ -59,4 +62,6 @@ void draw_node_detail_panel(const std::string& api_url, const std::vector& runs_all, bool* p_open); +void draw_table_preview_panel(const std::string& api_url, bool* p_open); + } // namespace data_factory_ui