From 7a38fe9a4192898c1d797362640282c644f6c64f Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 15 May 2026 16:57:55 +0200 Subject: [PATCH] feat: tabs DAG List / Detail / Run Detail via data_table_cpp_viz (issue 0095 step 5) - tabs.{h,cpp}: 3 paneles que renderizan TableInput con data_table::render() + RowDoubleClick events para drill-down (DAG -> Detail -> Run Detail). - main.cpp: arranca con auto-fetch DAGs y los 3 tabs visibles por defecto. Panel Main diagnostico apagado. - CMakeLists.txt: linka empty_state.cpp del registry. - app.md: uses_functions completo (data_table_cpp_viz + stack TQL + empty_state). Tags: [imgui, dashboard, dag, scheduler, http, websocket]. Funcionalidades: - DAG List: tabla con Name/Schedule/Last Status/Tags/Valid/File. Status combina last_run (REST) + live_runs (WS). Double-click selecciona DAG. - DAG Detail: header + Run Now (POST /api/dags/{name}/run) + tabla recent runs. Double-click run abre Run Detail. - Run Detail: header del run + tabla steps (name/status/exit/duration/started) + CollapsingHeader por step con stdout/stderr. Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 2 + app.md | 28 ++-- main.cpp | 31 ++++- tabs.cpp | 366 +++++++++++++++++++++++++++++++++++++++++++++++++ tabs.h | 45 ++++++ 5 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 tabs.cpp create mode 100644 tabs.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fd3cfb..ef071dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,8 @@ add_imgui_app(dag_engine_ui http_client.cpp data_http.cpp ws_client.cpp + tabs.cpp + ${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp ) target_include_directories(dag_engine_ui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/app.md b/app.md index 4aa9d0e..18499db 100644 --- a/app.md +++ b/app.md @@ -3,21 +3,21 @@ name: dag_engine_ui lang: cpp domain: tools description: "Frontend ImGui para dag_engine. Lista, lanza e inspecciona DAGs con live updates via WS." -tags: [imgui] +tags: [imgui, dashboard, dag, scheduler, http, websocket] uses_functions: - # Uncomment when using data_table::render() — provided via fn_table_viz: - # - data_table_cpp_viz - # - viz_render_cpp_viz - # - compute_stage_cpp_core - # - compute_pipeline_cpp_core - # - compute_column_stats_cpp_core - # - auto_detect_type_cpp_core - # - tql_emit_cpp_core - # - tql_apply_cpp_core - # - lua_engine_cpp_core - # - join_tables_cpp_core - # - tql_to_sql_cpp_core - # - llm_anthropic_cpp_core + - data_table_cpp_viz + - viz_render_cpp_viz + - compute_stage_cpp_core + - compute_pipeline_cpp_core + - compute_column_stats_cpp_core + - auto_detect_type_cpp_core + - tql_emit_cpp_core + - tql_apply_cpp_core + - lua_engine_cpp_core + - join_tables_cpp_core + - tql_to_sql_cpp_core + - llm_anthropic_cpp_core + - empty_state_cpp_core uses_types: [] framework: "imgui" entry_point: "main.cpp" diff --git a/main.cpp b/main.cpp index 5929e6b..fe55807 100644 --- a/main.cpp +++ b/main.cpp @@ -5,6 +5,7 @@ #include "core/logger.h" #include "data_http.h" #include "ws_client.h" +#include "tabs.h" #include "vendor/nlohmann/json.hpp" #include @@ -29,8 +30,14 @@ static std::string g_last_error; static WsClient g_ws; // Toggles de paneles (visibles desde el menu View del menubar canonico) -static bool g_show_main = true; -static bool g_show_live = true; +static bool g_show_main = false; // diagnostico, off por defecto +static bool g_show_live = true; +static bool g_show_dag_list = true; +static bool g_show_dag_detail = true; +static bool g_show_run_detail = true; + +// Auto-fetch DAG list una vez al arrancar. +static bool g_initial_fetched = false; // Upsert por id en g_live_runs. static void upsert_live_run(const dag_ui::DagRunRow& r) { @@ -115,6 +122,12 @@ static void draw_live() { } static void render() { + // Auto-fetch DAGs on first frame. + if (!g_initial_fetched) { + g_initial_fetched = true; + dag_ui::list_dags_http(g_api_url, g_dags); + } + // Drain WS messages this frame (cheap, max 64). { std::vector msgs; @@ -122,8 +135,11 @@ static void render() { for (auto& m : msgs) parse_ws_payload(m); } - if (g_show_main) draw_main(); - if (g_show_live) draw_live(); + if (g_show_dag_list) dag_ui_tabs::draw_dag_list(g_api_url, g_dags, g_live_runs); + if (g_show_dag_detail) dag_ui_tabs::draw_dag_detail(g_api_url); + if (g_show_run_detail) dag_ui_tabs::draw_run_detail(g_api_url); + if (g_show_main) draw_main(); + if (g_show_live) draw_live(); } int main(int /*argc*/, char** /*argv*/) { @@ -131,8 +147,11 @@ int main(int /*argc*/, char** /*argv*/) { g_ws.start(g_ws_host, g_ws_port, g_ws_path); static fn_ui::PanelToggle panels[] = { - { "Main", nullptr, &g_show_main }, - { "Live (WS)", nullptr, &g_show_live }, + { "DAGs", nullptr, &g_show_dag_list }, + { "DAG Detail", nullptr, &g_show_dag_detail }, + { "Run Detail", nullptr, &g_show_run_detail }, + { "Live (WS)", nullptr, &g_show_live }, + { "Main (diag)", nullptr, &g_show_main }, }; fn::AppConfig cfg; diff --git a/tabs.cpp b/tabs.cpp new file mode 100644 index 0000000..fcb8edf --- /dev/null +++ b/tabs.cpp @@ -0,0 +1,366 @@ +#include "tabs.h" +#include "viz/data_table.h" +#include "core/data_table_types.h" +#include "core/icons_tabler.h" +#include "core/empty_state.h" + +#include +#include +#include + +namespace dag_ui_tabs { + +// --------------------------------------------------------------------------- +// Globals +// --------------------------------------------------------------------------- + +Selection& selection() { + static Selection s; + return s; +} + +Caches& caches() { + static Caches c; + return c; +} + +// data_table::State persistente por panel (issue 0081-J pattern). +static data_table::State g_st_dag_list; +static data_table::State g_st_dag_runs; +static data_table::State g_st_run_steps; + +// Backing storage para cells de cada tabla. Owner del char* const* en TableInput. +static std::vector g_back_dag_list; +static std::vector g_ptrs_dag_list; + +static std::vector g_back_dag_runs; +static std::vector g_ptrs_dag_runs; + +static std::vector g_back_run_steps; +static std::vector g_ptrs_run_steps; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +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(); +} + +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 status_for_dag(const std::string& dag_name, + const dag_ui::DagInfo& info, + const std::vector& live_runs) { + // Find most recent live run that matches this DAG. + const dag_ui::DagRunRow* best = nullptr; + for (auto& r : live_runs) { + if (r.dag_name != dag_name) continue; + if (!best || r.started_at > best->started_at) best = &r; + } + if (best) return best->status; + if (info.has_last_run) return info.last_run_status; + return "-"; +} + +// --------------------------------------------------------------------------- +// DAG List +// --------------------------------------------------------------------------- + +void draw_dag_list(const std::string& api_url, + const std::vector& dags, + const std::vector& live_runs) +{ + if (!ImGui::Begin(TI_LIST " DAGs")) { + ImGui::End(); + return; + } + + if (dags.empty()) { + empty_state("( no DAGs )", "Empty registry", + "Place a YAML in apps/dag_engine/dags_migrated/ and reload the server."); + ImGui::End(); + return; + } + + // Build TableInput + data_table::TableInput ti; + ti.name = "dags"; + ti.headers = {"Name", "Schedule", "Last Status", "Tags", "Valid", "File"}; + ti.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, + }; + ti.rows = static_cast(dags.size()); + ti.cols = static_cast(ti.headers.size()); + + g_back_dag_list.clear(); + g_back_dag_list.reserve(dags.size() * ti.cols); + for (auto& d : dags) { + g_back_dag_list.push_back(d.name); + g_back_dag_list.push_back(d.schedule.empty() ? "-" : d.schedule[0]); + g_back_dag_list.push_back(status_for_dag(d.name, d, live_runs)); + std::string tags_csv; + for (size_t i = 0; i < d.tags.size(); i++) { + if (i) tags_csv += ","; + tags_csv += d.tags[i]; + } + g_back_dag_list.push_back(tags_csv); + g_back_dag_list.push_back(d.valid ? "yes" : "no"); + g_back_dag_list.push_back(d.file_path); + } + cells_to_ptrs(g_back_dag_list, g_ptrs_dag_list); + ti.cells = g_ptrs_dag_list.data(); + + std::vector events; + ImGui::BeginChild("##dag_list_wrap", ImVec2(-1, -1)); + data_table::render("##dt_dag_list", {ti}, g_st_dag_list, &events); + ImGui::EndChild(); + + // Handle row events -> select DAG. + for (auto& ev : events) { + if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && + ev.row < static_cast(dags.size())) { + selection().dag_name = dags[ev.row].name; + caches().dag_detail_loaded = false; // force reload + } + } + + ImGui::End(); +} + +// --------------------------------------------------------------------------- +// DAG Detail +// --------------------------------------------------------------------------- + +static void load_dag_detail(const std::string& api_url) { + auto& sel = selection(); + auto& c = caches(); + c.dag_detail = {}; + if (sel.dag_name.empty()) { + c.dag_detail_loaded = false; + return; + } + if (dag_ui::get_dag_http(api_url, sel.dag_name, c.dag_detail)) { + c.dag_detail_loaded = true; + } +} + +void draw_dag_detail(const std::string& api_url) { + if (!ImGui::Begin(TI_INFO_CIRCLE " DAG Detail")) { + ImGui::End(); + return; + } + auto& sel = selection(); + auto& c = caches(); + + if (sel.dag_name.empty()) { + empty_state("( nothing selected )", "Pick a DAG", + "Double-click a row in the DAG list to inspect it here."); + ImGui::End(); + return; + } + + if (!c.dag_detail_loaded) load_dag_detail(api_url); + + auto& info = c.dag_detail.info; + ImGui::Text("%s %s", TI_HASH, info.name.empty() ? sel.dag_name.c_str() : info.name.c_str()); + if (!info.description.empty()) ImGui::TextWrapped("%s", info.description.c_str()); + if (!info.schedule.empty()) ImGui::Text("Schedule: %s", info.schedule[0].c_str()); + + ImGui::Separator(); + + if (ImGui::Button(TI_PLAYER_PLAY " Run Now")) { + std::string run_id, err; + if (dag_ui::trigger_dag_http(api_url, sel.dag_name, run_id, err)) { + sel.run_id = run_id; + c.run_detail_loaded = false; + } else { + // Surface error via console; UI banner could be added later. + fprintf(stderr, "[dag_detail] trigger failed: %s\n", err.c_str()); + } + } + ImGui::SameLine(); + if (ImGui::Button(TI_REFRESH " Refresh")) { + c.dag_detail_loaded = false; + } + + ImGui::Separator(); + ImGui::TextUnformatted("Recent runs:"); + + auto& runs = c.dag_detail.recent_runs; + if (runs.empty()) { + ImGui::TextDisabled("( no runs yet )"); + ImGui::End(); + return; + } + + data_table::TableInput ti; + ti.name = "runs"; + ti.headers = {"Run ID", "Status", "Trigger", "Started", "Finished", "Error"}; + ti.types = { + data_table::ColumnType::String, + data_table::ColumnType::String, + data_table::ColumnType::String, + data_table::ColumnType::Date, + data_table::ColumnType::Date, + data_table::ColumnType::String, + }; + ti.rows = static_cast(runs.size()); + ti.cols = static_cast(ti.headers.size()); + + g_back_dag_runs.clear(); + g_back_dag_runs.reserve(runs.size() * ti.cols); + for (auto& r : runs) { + g_back_dag_runs.push_back(r.id); + g_back_dag_runs.push_back(r.status); + g_back_dag_runs.push_back(r.trigger); + g_back_dag_runs.push_back(r.started_at); + g_back_dag_runs.push_back(r.finished_at); + g_back_dag_runs.push_back(r.error); + } + cells_to_ptrs(g_back_dag_runs, g_ptrs_dag_runs); + ti.cells = g_ptrs_dag_runs.data(); + + std::vector events; + ImGui::BeginChild("##dag_runs_wrap", ImVec2(-1, -1)); + data_table::render("##dt_dag_runs", {ti}, g_st_dag_runs, &events); + ImGui::EndChild(); + + for (auto& ev : events) { + if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && + ev.row < static_cast(runs.size())) { + sel.run_id = runs[ev.row].id; + caches().run_detail_loaded = false; + } + } + ImGui::End(); +} + +// --------------------------------------------------------------------------- +// Run Detail +// --------------------------------------------------------------------------- + +static void load_run_detail(const std::string& api_url) { + auto& sel = selection(); + auto& c = caches(); + c.run_detail = {}; + if (sel.run_id.empty()) { + c.run_detail_loaded = false; + return; + } + if (dag_ui::get_run_http(api_url, sel.run_id, c.run_detail)) { + c.run_detail_loaded = true; + } +} + +void draw_run_detail(const std::string& api_url) { + if (!ImGui::Begin(TI_CLIPBOARD_LIST " Run Detail")) { + ImGui::End(); + return; + } + auto& sel = selection(); + auto& c = caches(); + + if (sel.run_id.empty()) { + empty_state("( nothing selected )", "Pick a run", + "Double-click a row in DAG Detail recent runs."); + ImGui::End(); + return; + } + + if (!c.run_detail_loaded) load_run_detail(api_url); + + auto& run = c.run_detail.run; + ImGui::Text("%s %s", TI_HASH, run.id.empty() ? sel.run_id.c_str() : run.id.c_str()); + ImGui::Text("Status: %s | Trigger: %s", run.status.c_str(), run.trigger.c_str()); + ImGui::Text("Started: %s | Finished: %s", + run.started_at.c_str(), run.finished_at.empty() ? "-" : run.finished_at.c_str()); + if (!run.error.empty()) { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Error: %s", run.error.c_str()); + } + ImGui::SameLine(); + if (ImGui::Button(TI_REFRESH " Refresh##run")) { + c.run_detail_loaded = false; + } + + ImGui::Separator(); + auto& steps = c.run_detail.steps; + if (steps.empty()) { + ImGui::TextDisabled("( no steps yet )"); + ImGui::End(); + return; + } + + data_table::TableInput ti; + ti.name = "steps"; + ti.headers = {"Step", "Status", "Exit", "Duration", "Started"}; + ti.types = { + data_table::ColumnType::String, + data_table::ColumnType::String, + data_table::ColumnType::Int, + data_table::ColumnType::String, + data_table::ColumnType::Date, + }; + ti.rows = static_cast(steps.size()); + ti.cols = static_cast(ti.headers.size()); + + g_back_run_steps.clear(); + g_back_run_steps.reserve(steps.size() * ti.cols); + for (auto& s : steps) { + g_back_run_steps.push_back(s.step_name); + g_back_run_steps.push_back(s.status); + g_back_run_steps.push_back(std::to_string(s.exit_code)); + g_back_run_steps.push_back(format_duration(s.duration_ms)); + g_back_run_steps.push_back(s.started_at); + } + cells_to_ptrs(g_back_run_steps, g_ptrs_run_steps); + ti.cells = g_ptrs_run_steps.data(); + + ImGui::BeginChild("##run_steps_wrap", ImVec2(-1, ImGui::GetContentRegionAvail().y * 0.5f)); + data_table::render("##dt_run_steps", {ti}, g_st_run_steps); + ImGui::EndChild(); + + // stdout/stderr expandible por step. + ImGui::Separator(); + ImGui::TextUnformatted("Step output:"); + for (size_t i = 0; i < steps.size(); i++) { + char hdr[256]; + std::snprintf(hdr, sizeof(hdr), "%s##step_%zu", steps[i].step_name.c_str(), i); + if (ImGui::CollapsingHeader(hdr)) { + if (!steps[i].stdout_text.empty()) { + ImGui::TextUnformatted("stdout:"); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.08f, 0.08f, 0.08f, 1)); + ImGui::BeginChild((std::string("##stdout_") + std::to_string(i)).c_str(), + ImVec2(-1, 80), false); + ImGui::TextUnformatted(steps[i].stdout_text.c_str()); + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + if (!steps[i].stderr_text.empty()) { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "stderr:"); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.06f, 0.06f, 1)); + ImGui::BeginChild((std::string("##stderr_") + std::to_string(i)).c_str(), + ImVec2(-1, 80), false); + ImGui::TextUnformatted(steps[i].stderr_text.c_str()); + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + } + } + ImGui::End(); +} + +} // namespace dag_ui_tabs diff --git a/tabs.h b/tabs.h new file mode 100644 index 0000000..2acf34e --- /dev/null +++ b/tabs.h @@ -0,0 +1,45 @@ +#pragma once + +// Tabs / panels para dag_engine_ui: +// - DAG List (lista todos los DAGs, double-click -> selecciona) +// - DAG Detail (header + Run Now + recent runs como data_table) +// - Run Detail (steps con duracion/status + stdout/stderr expandible) +// +// Todos usan data_table_cpp_viz (issue 0081). State persistente por tab. + +#include "data_http.h" +#include +#include + +namespace dag_ui_tabs { + +// Estado global cross-tab: que DAG/run esta seleccionado actualmente. +struct Selection { + std::string dag_name; // "" = nada seleccionado + std::string run_id; // "" = nada seleccionado +}; + +Selection& selection(); + +// Cache: detalle actual del DAG seleccionado + del run seleccionado. +// Se rellena bajo demanda al cambiar la seleccion (o boton Refresh). +struct Caches { + dag_ui::DagDetail dag_detail; + dag_ui::DagRunDetail run_detail; + bool dag_detail_loaded = false; + bool run_detail_loaded = false; +}; + +Caches& caches(); + +// Render cada tab. api_url es el endpoint dag_engine. +// `live_runs` es el cache global mantenido por WS (sirve para DAG List status). +void draw_dag_list(const std::string& api_url, + const std::vector& dags, + const std::vector& live_runs); + +void draw_dag_detail(const std::string& api_url); + +void draw_run_detail(const std::string& api_url); + +} // namespace dag_ui_tabs