chore: auto-commit (8 archivos)
- CMakeLists.txt - app.md - data_http.cpp - data_http.h - main.cpp - tabs.cpp - tabs.h - appicon.ico Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ add_imgui_app(dag_engine_ui
|
||||
ws_client.cpp
|
||||
tabs.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
|
||||
)
|
||||
target_include_directories(dag_engine_ui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
|
||||
@@ -21,8 +21,26 @@ uses_functions:
|
||||
uses_types: []
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
dir_path: "cpp/apps/dag_engine_ui"
|
||||
dir_path: "apps/dag_engine_ui"
|
||||
repo_url: "https://gitea.organic-machine.com/dataforge/dag_engine_ui"
|
||||
e2e_checks:
|
||||
- id: build_cmake
|
||||
cmd: "cmake --build cpp/build -j --target dag_engine_ui"
|
||||
timeout_s: 300
|
||||
severity: critical
|
||||
- id: binary_exists
|
||||
cmd: "test -x cpp/build/linux/apps/dag_engine_ui/dag_engine_ui || test -x cpp/build/apps/dag_engine_ui/dag_engine_ui"
|
||||
timeout_s: 5
|
||||
severity: critical
|
||||
- id: self_test
|
||||
cmd: "(cpp/build/linux/apps/dag_engine_ui/dag_engine_ui --self-test 2>&1 || cpp/build/apps/dag_engine_ui/dag_engine_ui --self-test 2>&1) | head -20"
|
||||
timeout_s: 10
|
||||
expect_stdout_contains: "self-test"
|
||||
severity: warning
|
||||
- id: cpp_apps_conformance
|
||||
cmd: "./fn doctor cpp-apps 2>&1 | grep -A1 dag_engine_ui || echo 'no issues'"
|
||||
expect_stdout_contains: "no issues"
|
||||
severity: critical
|
||||
---
|
||||
|
||||
# dag_engine_ui
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -73,6 +73,7 @@ static void parse_step(const json& j, DagStepRow& s) {
|
||||
s.finished_at = get_str(j, "finished_at");
|
||||
s.duration_ms = get_int64(j, "duration_ms");
|
||||
s.error = get_str(j, "error");
|
||||
s.function_id = get_str(j, "function_id");
|
||||
}
|
||||
|
||||
static void parse_dag_info(const json& j, DagInfo& d) {
|
||||
@@ -245,4 +246,33 @@ bool trigger_dag_http(const std::string& api_url, const std::string& name,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_function_http(const std::string& api_url,
|
||||
const std::string& function_id,
|
||||
FnInfo& out) {
|
||||
std::string host;
|
||||
int port;
|
||||
if (!parse_url(api_url, host, port)) return false;
|
||||
if (function_id.empty()) return false;
|
||||
HttpClient cli(host, port);
|
||||
auto res = cli.get("/api/functions/" + function_id);
|
||||
if (!res.ok()) {
|
||||
fprintf(stderr, "[dag_http] get_function(%s) failed: status=%d\n",
|
||||
function_id.c_str(), res.status);
|
||||
return false;
|
||||
}
|
||||
auto j = json::parse(res.body, nullptr, false);
|
||||
if (!j.is_object()) return false;
|
||||
|
||||
out.id = get_str(j, "id");
|
||||
out.name = get_str(j, "name");
|
||||
out.description = get_str(j, "description");
|
||||
out.signature = get_str(j, "signature");
|
||||
out.purity = get_str(j, "purity");
|
||||
out.domain = get_str(j, "domain");
|
||||
out.lang = get_str(j, "lang");
|
||||
out.uses_functions = get_str_array(j, "uses_functions");
|
||||
out.uses_types = get_str_array(j, "uses_types");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dag_ui
|
||||
|
||||
+20
@@ -54,6 +54,20 @@ struct DagStepRow {
|
||||
std::string finished_at;
|
||||
long long duration_ms = 0;
|
||||
std::string error;
|
||||
std::string function_id; // "" if not a function step
|
||||
};
|
||||
|
||||
// Metadata de una funcion del registry (response shape de GET /api/functions/{id}).
|
||||
struct FnInfo {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string signature;
|
||||
std::string purity; // "pure" | "impure"
|
||||
std::string domain;
|
||||
std::string lang;
|
||||
std::vector<std::string> uses_functions;
|
||||
std::vector<std::string> uses_types;
|
||||
};
|
||||
|
||||
struct DagRunDetail {
|
||||
@@ -86,4 +100,10 @@ bool get_run_http(const std::string& api_url, const std::string& run_id,
|
||||
bool trigger_dag_http(const std::string& api_url, const std::string& name,
|
||||
std::string& out_run_id, std::string& out_error);
|
||||
|
||||
// GET /api/functions/{function_id} -> rellena out con metadata del registry.
|
||||
// Devuelve false si red falla, status != 2xx, o JSON no parseable.
|
||||
bool get_function_http(const std::string& api_url,
|
||||
const std::string& function_id,
|
||||
FnInfo& out);
|
||||
|
||||
} // namespace dag_ui
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
#include "core/icons_tabler.h"
|
||||
#include "core/logger.h"
|
||||
#include "data_http.h"
|
||||
#include "http_client.h"
|
||||
#include "ws_client.h"
|
||||
#include "tabs.h"
|
||||
#include "vendor/nlohmann/json.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -48,6 +51,9 @@ static bool g_show_dag_list = true;
|
||||
static bool g_show_dag_detail = true;
|
||||
static bool g_show_run_detail = true;
|
||||
static bool g_show_timeline = true;
|
||||
static bool g_show_all_runs = true;
|
||||
static bool g_show_health = true;
|
||||
static bool g_show_function_panel = true;
|
||||
|
||||
// Auto-fetch DAG list una vez al arrancar.
|
||||
static bool g_initial_fetched = false;
|
||||
@@ -171,11 +177,36 @@ static void render() {
|
||||
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_timeline) dag_ui_tabs::draw_timeline(g_api_url, g_runs_all);
|
||||
if (g_show_all_runs) dag_ui_tabs::draw_all_runs(g_api_url, g_runs_all);
|
||||
if (g_show_health) dag_ui_tabs::draw_health(g_api_url, g_runs_all);
|
||||
if (g_show_main) draw_main();
|
||||
if (g_show_live) draw_live();
|
||||
if (g_show_function_panel) dag_ui_tabs::draw_function_panel(g_api_url, &g_show_function_panel);
|
||||
}
|
||||
|
||||
int main(int /*argc*/, char** /*argv*/) {
|
||||
// Self-test: blocking HTTP GET to the dag_engine backend, no GUI.
|
||||
// Returns 0 if reachable (any 2xx), 1 otherwise.
|
||||
static int run_self_test() {
|
||||
HttpClient client(g_ws_host, g_ws_port);
|
||||
// Probe /api/dags as a sucedaneo de /health (no dedicated /health helper).
|
||||
HttpResponse resp = client.get("/api/dags");
|
||||
if (resp.ok()) {
|
||||
std::printf("self-test ok: dag_engine reachable at %s\n", g_api_url.c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("self-test fail: dag_engine unreachable at %s (status=%d)\n",
|
||||
g_api_url.c_str(), resp.status);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// CLI flag --self-test: probe backend and exit without opening GUI.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (argv[i] && std::strcmp(argv[i], "--self-test") == 0) {
|
||||
return run_self_test();
|
||||
}
|
||||
}
|
||||
|
||||
// Conecta WS al backend dag_engine. Reconnect con backoff lo gestiona WsClient.
|
||||
g_ws.start(g_ws_host, g_ws_port, g_ws_path);
|
||||
|
||||
@@ -184,6 +215,9 @@ int main(int /*argc*/, char** /*argv*/) {
|
||||
{ "DAG Detail", nullptr, &g_show_dag_detail },
|
||||
{ "Run Detail", nullptr, &g_show_run_detail },
|
||||
{ "Timeline", nullptr, &g_show_timeline },
|
||||
{ "All Runs", nullptr, &g_show_all_runs },
|
||||
{ "Health", nullptr, &g_show_health },
|
||||
{ "Function", nullptr, &g_show_function_panel },
|
||||
{ "Live (WS)", nullptr, &g_show_live },
|
||||
{ "Main (diag)", nullptr, &g_show_main },
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core/data_table_types.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include "core/empty_state.h"
|
||||
#include "core/badge.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
@@ -28,6 +29,11 @@ Caches& caches() {
|
||||
return c;
|
||||
}
|
||||
|
||||
FunctionPanelState& function_panel() {
|
||||
static FunctionPanelState fps;
|
||||
return fps;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -355,33 +361,84 @@ void draw_run_detail(const std::string& api_url) {
|
||||
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();
|
||||
|
||||
// Steps table — render nativo (ImGui::BeginTable) en vez de data_table::render
|
||||
// para soportar la columna "Function" clickable (badge -> abre Function panel).
|
||||
// Status sigue mostrando badge coloreado por tipo.
|
||||
ImGui::BeginChild("##run_steps_wrap", ImVec2(-1, ImGui::GetContentRegionAvail().y * 0.5f));
|
||||
data_table::render("##dt_run_steps", {ti}, g_st_run_steps);
|
||||
const ImGuiTableFlags steps_flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp |
|
||||
ImGuiTableFlags_ScrollY;
|
||||
if (ImGui::BeginTable("##dt_run_steps", 6, steps_flags)) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("Step", ImGuiTableColumnFlags_WidthStretch, 1.6f);
|
||||
ImGui::TableSetupColumn("Function", ImGuiTableColumnFlags_WidthStretch, 2.2f);
|
||||
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.8f);
|
||||
ImGui::TableSetupColumn("Exit", ImGuiTableColumnFlags_WidthStretch, 0.4f);
|
||||
ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_WidthStretch, 0.7f);
|
||||
ImGui::TableSetupColumn("Started", ImGuiTableColumnFlags_WidthStretch, 1.2f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (size_t i = 0; i < steps.size(); i++) {
|
||||
auto& s = steps[i];
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// Step name
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::TextUnformatted(s.step_name.c_str());
|
||||
|
||||
// Function — badge clickable o "(shell)"
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
if (!s.function_id.empty()) {
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
// Small button styled like a badge (registry green).
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.55f, 0.30f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.65f, 0.38f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.10f, 0.45f, 0.25f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6, 1));
|
||||
char btn[512];
|
||||
std::snprintf(btn, sizeof(btn), "%s %s", TI_FUNCTION, s.function_id.c_str());
|
||||
if (ImGui::SmallButton(btn)) {
|
||||
auto& fp = function_panel();
|
||||
if (!fp.selected_id.empty() && fp.selected_id != s.function_id) {
|
||||
fp.breadcrumb.push_back(fp.selected_id);
|
||||
}
|
||||
fp.selected_id = s.function_id;
|
||||
fp.loaded = false;
|
||||
fp.load_error.clear();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopID();
|
||||
} else {
|
||||
ImGui::TextDisabled("(shell)");
|
||||
}
|
||||
|
||||
// Status badge
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
BadgeVariant v = BadgeVariant::Default;
|
||||
if (s.status == "success") v = BadgeVariant::Success;
|
||||
else if (s.status == "failed") v = BadgeVariant::Error;
|
||||
else if (s.status == "running") v = BadgeVariant::Warning;
|
||||
else if (s.status == "cancelled") v = BadgeVariant::Default;
|
||||
else if (s.status == "pending") v = BadgeVariant::Info;
|
||||
badge(s.status.c_str(), v);
|
||||
|
||||
// Exit
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::Text("%d", s.exit_code);
|
||||
|
||||
// Duration
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::TextUnformatted(format_duration(s.duration_ms).c_str());
|
||||
|
||||
// Started
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
ImGui::TextUnformatted(s.started_at.c_str());
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// stdout/stderr expandible por step.
|
||||
@@ -607,4 +664,375 @@ void draw_timeline(const std::string& api_url,
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void draw_health(const std::string& /*api_url*/,
|
||||
const std::vector<dag_ui::DagRunRow>& runs_all)
|
||||
{
|
||||
if (!ImGui::Begin(TI_ACTIVITY " Health")) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const time_t now = std::time(nullptr);
|
||||
const time_t cutoff_24h = now - 86400;
|
||||
|
||||
int runs_24h = 0;
|
||||
int success_24h = 0;
|
||||
int failed_24h = 0;
|
||||
int cancelled_24h = 0;
|
||||
int pending_total = 0;
|
||||
int success_all = 0;
|
||||
int failed_all = 0;
|
||||
int cancelled_all = 0;
|
||||
|
||||
for (auto& r : runs_all) {
|
||||
if (r.status == "pending" || r.status == "running") pending_total++;
|
||||
|
||||
// success_rate computed across success+failed+cancelled (terminal states).
|
||||
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) continue;
|
||||
if (t < cutoff_24h) continue;
|
||||
runs_24h++;
|
||||
if (r.status == "success") success_24h++;
|
||||
if (r.status == "failed") failed_24h++;
|
||||
if (r.status == "cancelled") cancelled_24h++;
|
||||
}
|
||||
|
||||
int terminal_all = success_all + failed_all + cancelled_all;
|
||||
float success_rate = (terminal_all > 0)
|
||||
? (100.0f * static_cast<float>(success_all) / static_cast<float>(terminal_all))
|
||||
: 0.0f;
|
||||
|
||||
if (runs_all.empty()) {
|
||||
empty_state(TI_ACTIVITY, "No runs yet",
|
||||
"Trigger a DAG to populate health metrics.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTable("##health_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.0f), "%.1f%%", success_rate);
|
||||
ImGui::TextDisabled("%d / %d terminal", success_all, terminal_all);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%s Failed (24h)", TI_ALERT_TRIANGLE);
|
||||
if (failed_24h > 0) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.35f, 0.30f, 1.0f), "%d", failed_24h);
|
||||
} else {
|
||||
ImGui::Text("%d", failed_24h);
|
||||
}
|
||||
ImGui::TextDisabled("cancelled: %d", cancelled_24h);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%s Pending/Running", TI_LOADER);
|
||||
if (pending_total > 0) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.80f, 0.20f, 1.0f), "%d", pending_total);
|
||||
} else {
|
||||
ImGui::Text("%d", pending_total);
|
||||
}
|
||||
ImGui::TextDisabled("active now");
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Computed client-side from %zu runs in cache.", runs_all.size());
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Function panel — sidebar con metadata del function_id seleccionado.
|
||||
// Lazy-load on click. Cada uses_functions[] es navegable (TreeNode click ->
|
||||
// recursive load). Boton Back consume el breadcrumb.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static BadgeVariant variant_for_purity(const std::string& p) {
|
||||
if (p == "pure") return BadgeVariant::Success;
|
||||
if (p == "impure") return BadgeVariant::Warning;
|
||||
return BadgeVariant::Default;
|
||||
}
|
||||
|
||||
void draw_function_panel(const std::string& api_url, bool* p_open) {
|
||||
auto& fp = function_panel();
|
||||
if (fp.selected_id.empty()) return; // panel oculto si no hay seleccion
|
||||
|
||||
char title[512];
|
||||
std::snprintf(title, sizeof(title),
|
||||
TI_FUNCTION " Function — %s###function_panel",
|
||||
fp.selected_id.c_str());
|
||||
|
||||
if (!ImGui::Begin(title, p_open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy load.
|
||||
if (!fp.loaded && fp.load_error.empty()) {
|
||||
fp.cached = {};
|
||||
if (dag_ui::get_function_http(api_url, fp.selected_id, fp.cached)) {
|
||||
fp.loaded = true;
|
||||
} else {
|
||||
fp.load_error = "Failed to fetch /api/functions/" + fp.selected_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Toolbar: Back + Close (clear) + Refresh
|
||||
bool has_history = !fp.breadcrumb.empty();
|
||||
if (!has_history) ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton(TI_ARROW_LEFT " Back")) {
|
||||
std::string prev = fp.breadcrumb.back();
|
||||
fp.breadcrumb.pop_back();
|
||||
fp.selected_id = prev;
|
||||
fp.loaded = false;
|
||||
fp.load_error.clear();
|
||||
}
|
||||
if (!has_history) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TI_REFRESH " Reload##fn_panel")) {
|
||||
fp.loaded = false;
|
||||
fp.load_error.clear();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TI_X " Close##fn_panel")) {
|
||||
fp.selected_id.clear();
|
||||
fp.breadcrumb.clear();
|
||||
fp.loaded = false;
|
||||
fp.load_error.clear();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
if (!fp.load_error.empty()) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", fp.load_error.c_str());
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (!fp.loaded) {
|
||||
ImGui::TextDisabled("loading...");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& fn = fp.cached;
|
||||
|
||||
// Header: id grande + 3 badges (domain / lang / purity)
|
||||
ImGui::TextUnformatted(fn.id.c_str());
|
||||
if (!fn.domain.empty()) { badge(fn.domain.c_str(), BadgeVariant::Info); ImGui::SameLine(); }
|
||||
if (!fn.lang.empty()) { badge(fn.lang.c_str(), BadgeVariant::Default); ImGui::SameLine(); }
|
||||
if (!fn.purity.empty()) { badge(fn.purity.c_str(), variant_for_purity(fn.purity)); }
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (!fn.description.empty()) {
|
||||
ImGui::TextWrapped("%s", fn.description.c_str());
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
if (!fn.signature.empty()) {
|
||||
ImGui::TextDisabled("signature");
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.08f, 0.08f, 0.08f, 1));
|
||||
ImGui::BeginChild("##fn_sig", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 2.4f), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGui::TextUnformatted(fn.signature.c_str());
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// uses_functions[]
|
||||
char hdr_fns[64];
|
||||
std::snprintf(hdr_fns, sizeof(hdr_fns),
|
||||
TI_FUNCTION " Uses functions (%zu)###uses_fns",
|
||||
fn.uses_functions.size());
|
||||
if (ImGui::CollapsingHeader(hdr_fns, ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
if (fn.uses_functions.empty()) {
|
||||
ImGui::TextDisabled(" (none)");
|
||||
} else {
|
||||
for (size_t i = 0; i < fn.uses_functions.size(); i++) {
|
||||
const std::string& dep = fn.uses_functions[i];
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
||||
ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||
ImGui::TreeNodeEx(dep.c_str(), flags, "%s %s", TI_CODE, dep.c_str());
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (!fp.selected_id.empty() && fp.selected_id != dep) {
|
||||
fp.breadcrumb.push_back(fp.selected_id);
|
||||
}
|
||||
fp.selected_id = dep;
|
||||
fp.loaded = false;
|
||||
fp.load_error.clear();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// uses_types[]
|
||||
char hdr_types[64];
|
||||
std::snprintf(hdr_types, sizeof(hdr_types),
|
||||
TI_NETWORK " Uses types (%zu)###uses_types",
|
||||
fn.uses_types.size());
|
||||
if (ImGui::CollapsingHeader(hdr_types)) {
|
||||
if (fn.uses_types.empty()) {
|
||||
ImGui::TextDisabled(" (none)");
|
||||
} else {
|
||||
for (auto& t : fn.uses_types) ImGui::BulletText("%s", t.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// All Runs panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static data_table::State g_st_all_runs;
|
||||
static std::vector<std::string> g_back_all_runs;
|
||||
static std::vector<const char*> g_ptrs_all_runs;
|
||||
|
||||
void draw_all_runs(const std::string& /*api_url*/,
|
||||
const std::vector<dag_ui::DagRunRow>& runs_all)
|
||||
{
|
||||
if (!ImGui::Begin(TI_HISTORY " All Runs")) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (runs_all.empty()) {
|
||||
empty_state(TI_HISTORY, "No runs yet",
|
||||
"Lanza algun DAG desde DAG List para que aparezca aqui.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by started_at desc (most recent first). Hacer copia para no mutar el cache.
|
||||
std::vector<const dag_ui::DagRunRow*> sorted;
|
||||
sorted.reserve(runs_all.size());
|
||||
for (auto& r : runs_all) sorted.push_back(&r);
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const dag_ui::DagRunRow* a, const dag_ui::DagRunRow* b){
|
||||
return a->started_at > b->started_at;
|
||||
});
|
||||
|
||||
data_table::TableInput ti;
|
||||
ti.name = "all_runs";
|
||||
ti.headers = {"Run ID", "DAG", "Status", "Trigger", "Started", "Finished", "Duration"};
|
||||
ti.types = {
|
||||
data_table::ColumnType::String,
|
||||
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>(sorted.size());
|
||||
ti.cols = static_cast<int>(ti.headers.size());
|
||||
|
||||
auto status_badges = [](){
|
||||
std::vector<data_table::BadgeRule> rules;
|
||||
rules.push_back({"success", "#22c55e", ""});
|
||||
rules.push_back({"failed", "#ef4444", ""});
|
||||
rules.push_back({"running", "#eab308", ""});
|
||||
rules.push_back({"pending", "#94a3b8", ""});
|
||||
rules.push_back({"cancelled", "#6b7280", ""});
|
||||
return rules;
|
||||
};
|
||||
auto trigger_badges = [](){
|
||||
std::vector<data_table::BadgeRule> rules;
|
||||
rules.push_back({"manual", "#3b82f6", ""});
|
||||
rules.push_back({"cron", "#a855f7", ""});
|
||||
rules.push_back({"api", "#06b6d4", ""});
|
||||
return rules;
|
||||
};
|
||||
|
||||
ti.column_specs.resize(ti.cols);
|
||||
for (int i = 0; i < ti.cols; i++) ti.column_specs[i].id = ti.headers[i];
|
||||
ti.column_specs[2].renderer = data_table::CellRenderer::Badge;
|
||||
ti.column_specs[2].badges = status_badges();
|
||||
ti.column_specs[3].renderer = data_table::CellRenderer::Badge;
|
||||
ti.column_specs[3].badges = trigger_badges();
|
||||
|
||||
// Helper: duracion humana entre started_at y finished_at (best-effort).
|
||||
auto duration_str = [](const std::string& s, const std::string& f) -> std::string {
|
||||
if (s.empty() || f.empty()) return "-";
|
||||
// Parse ISO 8601 minimalist (YYYY-MM-DDTHH:MM:SS).
|
||||
auto to_secs = [](const std::string& t) -> long long {
|
||||
int Y,M,D,h,mi,se;
|
||||
if (std::sscanf(t.c_str(), "%d-%d-%dT%d:%d:%d", &Y,&M,&D,&h,&mi,&se) != 6) return 0;
|
||||
std::tm tm = {}; tm.tm_year=Y-1900; tm.tm_mon=M-1; tm.tm_mday=D;
|
||||
tm.tm_hour=h; tm.tm_min=mi; tm.tm_sec=se;
|
||||
#ifdef _WIN32
|
||||
return static_cast<long long>(_mkgmtime(&tm));
|
||||
#else
|
||||
return static_cast<long long>(timegm(&tm));
|
||||
#endif
|
||||
};
|
||||
long long ss = to_secs(s), ff = to_secs(f);
|
||||
if (ss == 0 || ff == 0 || ff < ss) return "-";
|
||||
long long dur = ff - ss;
|
||||
if (dur < 60) return std::to_string(dur) + "s";
|
||||
if (dur < 3600) return std::to_string(dur/60) + "m " + std::to_string(dur%60) + "s";
|
||||
return std::to_string(dur/3600) + "h " + std::to_string((dur%3600)/60) + "m";
|
||||
};
|
||||
|
||||
g_back_all_runs.clear();
|
||||
g_back_all_runs.reserve(sorted.size() * ti.cols);
|
||||
for (auto* r : sorted) {
|
||||
// Truncate run id for display (keep last 8 chars).
|
||||
std::string short_id = r->id;
|
||||
if (short_id.size() > 12) short_id = "..." + short_id.substr(short_id.size() - 8);
|
||||
g_back_all_runs.push_back(short_id);
|
||||
g_back_all_runs.push_back(r->dag_name);
|
||||
g_back_all_runs.push_back(r->status);
|
||||
g_back_all_runs.push_back(r->trigger);
|
||||
g_back_all_runs.push_back(r->started_at);
|
||||
g_back_all_runs.push_back(r->finished_at);
|
||||
g_back_all_runs.push_back(duration_str(r->started_at, r->finished_at));
|
||||
}
|
||||
cells_to_ptrs(g_back_all_runs, g_ptrs_all_runs);
|
||||
ti.cells = g_ptrs_all_runs.data();
|
||||
|
||||
std::vector<data_table::TableEvent> events;
|
||||
ImGui::BeginChild("##all_runs_wrap", ImVec2(-1, -1));
|
||||
data_table::render("##dt_all_runs", {ti}, g_st_all_runs, &events);
|
||||
ImGui::EndChild();
|
||||
|
||||
for (auto& ev : events) {
|
||||
if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
|
||||
ev.row >= 0 && ev.row < static_cast<int>(sorted.size())) {
|
||||
selection().run_id = sorted[ev.row]->id;
|
||||
selection().dag_name = sorted[ev.row]->dag_name;
|
||||
caches().run_detail_loaded = false;
|
||||
caches().dag_detail_loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace dag_ui_tabs
|
||||
|
||||
@@ -32,6 +32,19 @@ struct Caches {
|
||||
|
||||
Caches& caches();
|
||||
|
||||
// Estado del panel lateral "Function" — registry metadata para el function_id
|
||||
// seleccionado actualmente. selected_id == "" -> panel oculto. breadcrumb mantiene
|
||||
// el historial de navegacion para soportar el boton Back.
|
||||
struct FunctionPanelState {
|
||||
std::string selected_id; // "" = panel oculto
|
||||
dag_ui::FnInfo cached;
|
||||
bool loaded = false;
|
||||
std::string load_error;
|
||||
std::vector<std::string> breadcrumb; // ids visitados antes del actual
|
||||
};
|
||||
|
||||
FunctionPanelState& function_panel();
|
||||
|
||||
// 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,
|
||||
@@ -47,4 +60,18 @@ void draw_run_detail(const std::string& api_url);
|
||||
void draw_timeline(const std::string& api_url,
|
||||
const std::vector<dag_ui::DagRunRow>& runs_all);
|
||||
|
||||
// Health panel: KPIs derivados de runs_all (client-side).
|
||||
// runs_24h, success_rate, failed_runs_24h, pending_runs.
|
||||
void draw_health(const std::string& api_url,
|
||||
const std::vector<dag_ui::DagRunRow>& runs_all);
|
||||
|
||||
// All Runs panel: historico completo de runs (todas las DAGs). Tabla
|
||||
// ordenada por started_at desc. Click row -> set selection().run_id.
|
||||
void draw_all_runs(const std::string& api_url,
|
||||
const std::vector<dag_ui::DagRunRow>& runs_all);
|
||||
|
||||
// Function panel: detalle de la funcion del registry seleccionada (id, domain,
|
||||
// purity, signature, uses_functions[], uses_types[]). Lazy-load por click.
|
||||
void draw_function_panel(const std::string& api_url, bool* p_open);
|
||||
|
||||
} // namespace dag_ui_tabs
|
||||
|
||||
Reference in New Issue
Block a user