Files
dag_engine_ui/tabs.cpp
T
egutierrez 49fc908fb4 feat: 5 puntitos de status en DAG List (R1..R5 columnas Badge)
- data_http: parsea last_runs[] del /api/dags y guarda d.last_runs_status (max 5, mas reciente primero).
- tabs.cpp DAG List: 5 columnas R1..R5 con CellRenderer::Badge + BadgeRule por status (success=verde, failed=rojo, running=amarillo, pending/cancelled=gris, "-"=tenue).
- main.cpp: g_refresh_pending. WS auto-trigger refresh /api/dags cuando ve un run con status terminal -> last_runs se actualiza sin pulsar nada.
- main + tabs: extern "C" dag_list_request_refresh() para el boton Refresh manual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:07:09 +02:00

410 lines
14 KiB
C++

#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 <imgui.h>
#include <algorithm>
#include <cstdio>
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<std::string> g_back_dag_list;
static std::vector<const char*> g_ptrs_dag_list;
static std::vector<std::string> g_back_dag_runs;
static std::vector<const char*> g_ptrs_dag_runs;
static std::vector<std::string> g_back_run_steps;
static std::vector<const char*> g_ptrs_run_steps;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static void cells_to_ptrs(const std::vector<std::string>& backing,
std::vector<const char*>& 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<dag_ui::DagRunRow>& 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
// ---------------------------------------------------------------------------
// Forward decl — main.cpp owns the cache and the refresh trigger.
extern "C" void dag_list_request_refresh();
void draw_dag_list(const std::string& api_url,
const std::vector<dag_ui::DagInfo>& dags,
const std::vector<dag_ui::DagRunRow>& live_runs)
{
if (!ImGui::Begin(TI_LIST " DAGs")) {
ImGui::End();
return;
}
if (ImGui::Button(TI_REFRESH " Refresh##dag_list")) {
dag_list_request_refresh();
}
ImGui::SameLine();
ImGui::TextDisabled("Double-click row -> inspect. R1..R5 = last 5 runs.");
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. Columnas R1..R5 muestran status de las ultimas 5 runs
// como badges coloreadas (verde/rojo/amarillo/gris) — issue 0095.
data_table::TableInput ti;
ti.name = "dags";
ti.headers = {"Name", "Schedule", "Last Status",
"R1", "R2", "R3", "R4", "R5",
"Tags", "Valid"};
ti.types = {
data_table::ColumnType::String, // Name
data_table::ColumnType::String, // Schedule
data_table::ColumnType::String, // Last Status
data_table::ColumnType::String, // R1
data_table::ColumnType::String, // R2
data_table::ColumnType::String, // R3
data_table::ColumnType::String, // R4
data_table::ColumnType::String, // R5
data_table::ColumnType::String, // Tags
data_table::ColumnType::String, // Valid
};
ti.rows = static_cast<int>(dags.size());
ti.cols = static_cast<int>(ti.headers.size());
// BadgeRule por status: misma config para R1..R5.
auto run_status_badges = [](){
std::vector<data_table::BadgeRule> rules;
rules.push_back({"success", "#22c55e", ""}); // verde
rules.push_back({"failed", "#ef4444", ""}); // rojo
rules.push_back({"running", "#eab308", ""}); // amarillo
rules.push_back({"pending", "#94a3b8", ""}); // gris azulado
rules.push_back({"cancelled", "#6b7280", ""}); // gris
rules.push_back({"-", "#1f2937", "·"}); // dot tenue cuando no hay run
return rules;
};
// ColumnSpec por columna. Solo R1..R5 (indices 3..7) son Badge.
ti.column_specs.resize(ti.cols);
for (int i = 0; i < ti.cols; i++) ti.column_specs[i].id = ti.headers[i];
for (int i = 3; i <= 7; i++) {
ti.column_specs[i].renderer = data_table::CellRenderer::Badge;
ti.column_specs[i].badges = run_status_badges();
}
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));
// R1..R5 — most recent first; "-" si menos de 5 runs.
for (int i = 0; i < 5; i++) {
if (i < static_cast<int>(d.last_runs_status.size())) {
g_back_dag_list.push_back(d.last_runs_status[i]);
} else {
g_back_dag_list.push_back("-");
}
}
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");
}
cells_to_ptrs(g_back_dag_list, g_ptrs_dag_list);
ti.cells = g_ptrs_dag_list.data();
std::vector<data_table::TableEvent> 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<int>(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<int>(runs.size());
ti.cols = static_cast<int>(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<data_table::TableEvent> 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<int>(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<int>(steps.size());
ti.cols = static_cast<int>(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