8c5152fca4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1036 lines
38 KiB
C++
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
|