7a38fe9a41
- 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) <noreply@anthropic.com>
367 lines
12 KiB
C++
367 lines
12 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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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 (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<int>(dags.size());
|
|
ti.cols = static_cast<int>(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<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
|