Files

1036 lines
38 KiB
C++

#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"
#include <imgui.h>
#include <algorithm>
#include <cstdio>
#include <ctime>
#include <map>
#include <string>
#include <vector>
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<std::string> g_back_extractors;
static std::vector<const char*> g_ptrs_extractors;
static std::vector<std::string> g_back_transformers;
static std::vector<const char*> g_ptrs_transformers;
static std::vector<std::string> g_back_sinks;
static std::vector<const char*> g_ptrs_sinks;
static std::vector<std::string> g_back_tables;
static std::vector<const char*> g_ptrs_tables;
static std::vector<std::string> g_back_databases;
static std::vector<const char*> g_ptrs_databases;
static std::vector<std::string> g_back_kpis;
static std::vector<const char*> g_ptrs_kpis;
static std::vector<std::string> g_back_node_runs;
static std::vector<const char*> g_ptrs_node_runs;
static std::vector<std::string> g_back_preview;
static std::vector<const char*> 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<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();
}
// Shared BadgeRules.
static std::vector<data_table::BadgeRule> run_status_badges() {
return {
{"success", "#22c55e", "success"},
{"failed", "#ef4444", "failed"},
{"running", "#3b82f6", "running"},
{"pending", "#94a3b8", "pending"},
{"cancelled", "#6b7280", "cancelled"},
};
}
static std::vector<data_table::BadgeRule> kind_badges() {
return {
{"extractor", "#0ea5e9", "extractor"},
{"transformer", "#a855f7", "transformer"},
{"sink", "#f97316", "sink"},
{"database", "#14b8a6", "database"},
{"validator", "#eab308", "validator"},
};
}
static std::vector<data_table::BadgeRule> enabled_badges() {
return {
{"yes", "#22c55e", "yes"},
{"no", "#6b7280", "no"},
};
}
// CategoricalChip helpers (dot izquierda + texto, siempre visible).
static std::vector<data_table::ChipRule> run_status_chips() {
return {
{"success", "#22c55e"},
{"failed", "#ef4444"},
{"running", "#3b82f6"},
{"pending", "#94a3b8"},
{"cancelled", "#6b7280"},
};
}
static std::vector<data_table::ChipRule> kind_chips() {
return {
{"extractor", "#0ea5e9"},
{"transformer", "#a855f7"},
{"sink", "#f97316"},
{"database", "#14b8a6"},
{"validator", "#eab308"},
{"duckdb", "#f59e0b"},
{"sqlite", "#6366f1"},
};
}
// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------
Selection& selection() {
static Selection s;
return s;
}
FunctionCache& function_cache() {
static FunctionCache fc;
return fc;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
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 format_bytes(long long bytes) {
if (bytes <= 0) return "-";
const double kb = 1024.0;
const double mb = kb * 1024.0;
const double gb = mb * 1024.0;
char buf[32];
if (bytes < (long long)kb) std::snprintf(buf, sizeof(buf), "%lld B", bytes);
else if (bytes < (long long)mb) std::snprintf(buf, sizeof(buf), "%.1f KB", bytes / kb);
else if (bytes < (long long)gb) std::snprintf(buf, sizeof(buf), "%.1f MB", bytes / mb);
else std::snprintf(buf, sizeof(buf), "%.2f GB", bytes / gb);
return buf;
}
static time_t parse_rfc3339(const std::string& s) {
if (s.size() < 19) return 0;
std::tm tm{};
tm.tm_year = std::atoi(s.substr(0, 4).c_str()) - 1900;
tm.tm_mon = std::atoi(s.substr(5, 2).c_str()) - 1;
tm.tm_mday = std::atoi(s.substr(8, 2).c_str());
tm.tm_hour = std::atoi(s.substr(11, 2).c_str());
tm.tm_min = std::atoi(s.substr(14, 2).c_str());
tm.tm_sec = std::atoi(s.substr(17, 2).c_str());
tm.tm_isdst = -1;
return std::mktime(&tm);
}
// Pick most-recent run per node from runs_all.
static const data_factory::Run* last_run_for(
const std::string& node_id,
const std::vector<data_factory::Run>& runs_all)
{
const data_factory::Run* best = nullptr;
for (auto& r : runs_all) {
if (r.node_id != node_id) continue;
if (!best || r.started_at > best->started_at) best = &r;
}
return best;
}
// ---------------------------------------------------------------------------
// Generic kind table — used by extractors / transformers / sinks
// ---------------------------------------------------------------------------
// 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<data_factory::Node>& nodes,
const std::string& filter_kind,
const std::vector<data_factory::Run>& runs_all,
bool show_schedule)
{
// Pick the per-kind state + backing.
data_table::State* st = &g_st_nodes_extractors;
std::vector<std::string>* backing = &g_back_extractors;
std::vector<const char*>* 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";
}
// Filter nodes for the current kind, and pre-resolve their last-run.
std::vector<const data_factory::Node*> filtered;
filtered.reserve(nodes.size());
for (auto& n : nodes) if (n.kind == filter_kind) filtered.push_back(&n);
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<int>(filtered.size());
tbl.cols = static_cast<int>(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<data_table::TableEvent> 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<int>(filtered.size()))
{
const data_factory::Node& n = *filtered[ev.row];
selection().node_id = n.id;
if (function_cache().function_id != n.function_id) {
function_cache() = {};
function_cache().function_id = n.function_id;
}
}
}
}
// ---------------------------------------------------------------------------
// Extractors
// ---------------------------------------------------------------------------
void draw_extractors(const std::string& /*api_url*/,
const std::vector<data_factory::Node>& nodes,
const std::vector<data_factory::Run>& runs_all)
{
if (!ImGui::Begin(TI_DOWNLOAD " Extractors")) {
ImGui::End();
return;
}
int count = 0;
for (auto& n : nodes) if (n.kind == "extractor") count++;
if (count == 0) {
empty_state(TI_DOWNLOAD, "No extractors",
"Register an extractor node via sqlite_api.");
ImGui::End();
return;
}
ImGui::TextDisabled("%d extractor nodes. Click row -> see Node Detail.", count);
draw_node_table("##df_extractors", nodes, "extractor", runs_all, true);
ImGui::End();
}
// ---------------------------------------------------------------------------
// Transformers
// ---------------------------------------------------------------------------
void draw_transformers(const std::string& /*api_url*/,
const std::vector<data_factory::Node>& nodes,
const std::vector<data_factory::Run>& runs_all)
{
if (!ImGui::Begin(TI_REFRESH " Transformers")) {
ImGui::End();
return;
}
int count = 0;
for (auto& n : nodes) if (n.kind == "transformer") count++;
if (count == 0) {
empty_state(TI_REFRESH, "No transformers",
"Register a transformer node via sqlite_api.");
ImGui::End();
return;
}
ImGui::TextDisabled("%d transformer nodes.", count);
draw_node_table("##df_transformers", nodes, "transformer", runs_all, false);
ImGui::End();
}
// ---------------------------------------------------------------------------
// Sinks
// ---------------------------------------------------------------------------
void draw_sinks(const std::string& /*api_url*/,
const std::vector<data_factory::Node>& nodes,
const std::vector<data_factory::Run>& runs_all)
{
if (!ImGui::Begin(TI_UPLOAD " Sinks")) {
ImGui::End();
return;
}
int count = 0;
for (auto& n : nodes) if (n.kind == "sink") count++;
if (count == 0) {
empty_state(TI_UPLOAD, "No sinks",
"Register a sink node via sqlite_api.");
ImGui::End();
return;
}
ImGui::TextDisabled("%d sink nodes.", count);
draw_node_table("##df_sinks", nodes, "sink", runs_all, false);
ImGui::End();
}
// ---------------------------------------------------------------------------
// Tables
// ---------------------------------------------------------------------------
void draw_tables(const std::string& /*api_url*/,
const std::vector<data_factory::TableEntry>& 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<int>(tables.size());
tbl.cols = static_cast<int>(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<data_table::TableEvent> 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<int>(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
// ---------------------------------------------------------------------------
void draw_databases(const std::string& /*api_url*/,
const std::vector<data_factory::DatabaseInfo>& dbs)
{
if (!ImGui::Begin(TI_DATABASE " Databases")) {
ImGui::End();
return;
}
if (dbs.empty()) {
empty_state(TI_DATABASE, "No databases registered",
"POST /api/datafactory/databases to register a DB.");
ImGui::End();
return;
}
{
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<int>(dbs.size());
tbl.cols = static_cast<int>(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) {
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);
}
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();
}
// ---------------------------------------------------------------------------
// Health
// ---------------------------------------------------------------------------
void draw_health(const std::string& /*api_url*/,
const std::vector<data_factory::Run>& runs_all)
{
if (!ImGui::Begin(TI_ACTIVITY " Health")) {
ImGui::End();
return;
}
if (runs_all.empty()) {
empty_state(TI_ACTIVITY, "No runs yet",
"Trigger a node to populate health metrics.");
ImGui::End();
return;
}
const time_t now = std::time(nullptr);
const time_t cutoff_24h = now - 86400;
int runs_24h = 0, success_24h = 0, failed_24h = 0;
int pending_total = 0;
long long rows_24h = 0, kb_24h = 0;
int success_all = 0, failed_all = 0, cancelled_all = 0;
for (auto& r : runs_all) {
if (r.status == "running" || r.status == "pending") pending_total++;
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 || t < cutoff_24h) continue;
runs_24h++;
rows_24h += r.rows_out;
kb_24h += r.kb_out;
if (r.status == "success") success_24h++;
if (r.status == "failed") failed_24h++;
}
int terminal = success_all + failed_all + cancelled_all;
float success_rate = (terminal > 0)
? (100.0f * (float)success_all / (float)terminal) : 0.0f;
{
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];
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);
cells_to_ptrs(g_back_kpis, g_ptrs_kpis);
tbl.cells = g_ptrs_kpis.data();
data_table::render("##dt_kpis", {tbl}, g_st_kpis);
}
ImGui::Separator();
ImGui::TextDisabled("Computed client-side from %zu runs in cache.", runs_all.size());
ImGui::End();
}
// ---------------------------------------------------------------------------
// Map (placeholder: flat tree by kind)
// ---------------------------------------------------------------------------
void draw_map(const std::string& /*api_url*/,
const std::vector<data_factory::Node>& nodes)
{
if (!ImGui::Begin(TI_LIST " Map")) {
ImGui::End();
return;
}
if (nodes.empty()) {
empty_state(TI_LIST, "No nodes",
"Register nodes via sqlite_api /api/datafactory/nodes.");
ImGui::End();
return;
}
// Group by kind.
std::map<std::string, std::vector<const data_factory::Node*>> by_kind;
for (auto& n : nodes) by_kind[n.kind].push_back(&n);
static const char* kind_order[] = {
"extractor", "transformer", "database", "sink", "validator"
};
static const char* kind_label[] = {
"Extractors", "Transformers", "Databases", "Sinks", "Validators"
};
const int n_kinds = sizeof(kind_order) / sizeof(kind_order[0]);
for (int i = 0; i < n_kinds; i++) {
auto it = by_kind.find(kind_order[i]);
int count = (it != by_kind.end()) ? (int)it->second.size() : 0;
char header[128];
std::snprintf(header, sizeof(header), "%s (%d)###%s_hdr",
kind_label[i], count, kind_order[i]);
if (!ImGui::TreeNodeEx(header, ImGuiTreeNodeFlags_DefaultOpen)) continue;
if (count == 0) {
ImGui::TextDisabled(" (empty)");
} else {
for (auto* p : it->second) {
ImGui::PushID(p->id.c_str());
ImGui::Bullet();
ImGui::SameLine();
bool sel = (selection().node_id == p->id);
if (ImGui::Selectable(p->name.c_str(), sel)) {
selection().node_id = p->id;
if (function_cache().function_id != p->function_id) {
function_cache() = {};
function_cache().function_id = p->function_id;
}
}
ImGui::SameLine();
if (!p->function_id.empty()) {
ImGui::TextDisabled(" -> %s", p->function_id.c_str());
}
ImGui::PopID();
}
}
ImGui::TreePop();
}
ImGui::End();
}
// ---------------------------------------------------------------------------
// Node detail panel (side)
// ---------------------------------------------------------------------------
void draw_node_detail_panel(const std::string& api_url,
const std::vector<data_factory::Node>& nodes,
const std::vector<data_factory::Run>& runs_all,
bool* p_open)
{
if (!ImGui::Begin(TI_INFO_CIRCLE " Node Detail", p_open)) {
ImGui::End();
return;
}
const std::string& nid = selection().node_id;
if (nid.empty()) {
empty_state(TI_INFO_CIRCLE, "Nothing selected",
"Click a row in any tab to inspect the node.");
ImGui::End();
return;
}
const data_factory::Node* node = nullptr;
for (auto& n : nodes) if (n.id == nid) { node = &n; break; }
if (!node) {
ImGui::TextDisabled("Node not in current cache: %s", nid.c_str());
ImGui::End();
return;
}
// Header
ImGui::Text("%s %s", TI_INFO_CIRCLE, node->name.c_str());
ImGui::SameLine();
badge(node->kind.c_str(), BadgeVariant::Info);
if (!node->enabled) {
ImGui::SameLine();
badge("disabled", BadgeVariant::Default);
}
ImGui::Separator();
if (!node->description.empty()) {
ImGui::TextWrapped("%s", node->description.c_str());
ImGui::Separator();
}
ImGui::Text("id: %s", node->id.c_str());
ImGui::Text("kind: %s", node->kind.c_str());
if (!node->function_id.empty()) {
ImGui::Text("function: %s", node->function_id.c_str());
}
if (!node->schedule_cron.empty()) {
ImGui::Text("schedule: %s", node->schedule_cron.c_str());
}
if (!node->tags.empty()) {
ImGui::Text("tags:");
for (auto& t : node->tags) {
ImGui::SameLine();
badge(t.c_str(), BadgeVariant::Default);
}
}
// 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();
auto& fc = function_cache();
if (fc.function_id != node->function_id) {
fc = {};
fc.function_id = node->function_id;
}
if (!fc.loaded && fc.error.empty()) {
if (data_factory::get_function_http(api_url, fc.function_id, fc.info)) {
fc.loaded = true;
} else {
fc.error = "Failed to fetch /api/functions/" + fc.function_id;
}
}
if (fc.loaded) {
ImGui::Text("%s Registry function", TI_BOX);
if (!fc.info.domain.empty()) {
ImGui::SameLine();
badge(fc.info.domain.c_str(), BadgeVariant::Info);
}
if (!fc.info.purity.empty()) {
ImGui::SameLine();
BadgeVariant v = (fc.info.purity == "pure")
? BadgeVariant::Success : BadgeVariant::Warning;
badge(fc.info.purity.c_str(), v);
}
if (!fc.info.lang.empty()) {
ImGui::SameLine();
badge(fc.info.lang.c_str(), BadgeVariant::Default);
}
if (!fc.info.signature.empty()) {
ImGui::TextWrapped("sig: %s", fc.info.signature.c_str());
}
if (!fc.info.description.empty()) {
ImGui::TextWrapped("%s", fc.info.description.c_str());
}
} else if (!fc.error.empty()) {
ImGui::TextColored(ImVec4(0.95f, 0.4f, 0.4f, 1), "%s", fc.error.c_str());
} else {
ImGui::TextDisabled("Loading function metadata...");
}
}
// Recent runs (top 10)
ImGui::Separator();
ImGui::Text("%s Recent runs", TI_HISTORY);
// Filter + cap.
std::vector<const data_factory::Run*> 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_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<int>(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<data_table::TableEvent> 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<int>(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<int>(ps.cache.columns.size());
int nrows = static_cast<int>(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<size_t>(nrows) * ncols);
for (auto& row : ps.cache.rows) {
for (int c = 0; c < ncols; c++) {
g_back_preview.push_back(c < static_cast<int>(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<long long>(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<int>(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();
}
} // namespace data_factory_ui