Files
data_factory/tabs.cpp
T
2026-05-16 16:34:49 +02:00

585 lines
21 KiB
C++

#include "tabs.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>
namespace data_factory_ui {
// ---------------------------------------------------------------------------
// 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);
}
static BadgeVariant variant_for_status(const std::string& st) {
if (st == "success") return BadgeVariant::Success;
if (st == "failed") return BadgeVariant::Error;
if (st == "running") return BadgeVariant::Warning;
if (st == "cancelled") return BadgeVariant::Default;
return BadgeVariant::Default;
}
// 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
// ---------------------------------------------------------------------------
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)
{
int cols = show_schedule ? 6 : 5;
if (!ImGui::BeginTable(table_id, cols,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY))
{
return;
}
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.20f);
ImGui::TableSetupColumn("Function", ImGuiTableColumnFlags_WidthStretch, 0.30f);
if (show_schedule)
ImGui::TableSetupColumn("Schedule", ImGuiTableColumnFlags_WidthStretch, 0.12f);
ImGui::TableSetupColumn("Last Run", ImGuiTableColumnFlags_WidthStretch, 0.18f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.10f);
ImGui::TableSetupColumn("Rows/KB", ImGuiTableColumnFlags_WidthStretch, 0.10f);
ImGui::TableHeadersRow();
int row_idx = 0;
for (auto& n : nodes) {
if (n.kind != filter_kind) continue;
ImGui::PushID(row_idx++);
ImGui::TableNextRow();
// Name (selectable -> sets selection)
ImGui::TableNextColumn();
bool selected = (selection().node_id == n.id);
if (ImGui::Selectable(n.name.c_str(), selected,
ImGuiSelectableFlags_SpanAllColumns))
{
selection().node_id = n.id;
// Invalidate function cache if function_id changed.
if (function_cache().function_id != n.function_id) {
function_cache() = {};
function_cache().function_id = n.function_id;
}
}
if (!n.enabled) {
ImGui::SameLine();
badge("disabled", BadgeVariant::Default);
}
// Function id
ImGui::TableNextColumn();
if (n.function_id.empty()) ImGui::TextDisabled("(none)");
else ImGui::TextUnformatted(n.function_id.c_str());
// Schedule
if (show_schedule) {
ImGui::TableNextColumn();
if (n.schedule_cron.empty()) ImGui::TextDisabled("manual");
else ImGui::TextUnformatted(n.schedule_cron.c_str());
}
// Last run
const data_factory::Run* lr = last_run_for(n.id, runs_all);
ImGui::TableNextColumn();
if (lr) ImGui::TextUnformatted(lr->started_at.c_str());
else ImGui::TextDisabled("-");
// Status badge
ImGui::TableNextColumn();
if (lr) badge(lr->status.c_str(), variant_for_status(lr->status));
else ImGui::TextDisabled("-");
// Rows / KB
ImGui::TableNextColumn();
if (lr) ImGui::Text("%lld / %lld", lr->rows_out, lr->kb_out);
else ImGui::TextDisabled("-");
ImGui::PopID();
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// 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();
}
// ---------------------------------------------------------------------------
// 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;
}
if (ImGui::BeginTable("##df_databases", 6,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY))
{
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch, 0.18f);
ImGui::TableSetupColumn("Kind", ImGuiTableColumnFlags_WidthStretch, 0.10f);
ImGui::TableSetupColumn("URI", ImGuiTableColumnFlags_WidthStretch, 0.32f);
ImGui::TableSetupColumn("Tables", ImGuiTableColumnFlags_WidthStretch, 0.10f);
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthStretch, 0.15f);
ImGui::TableSetupColumn("Last Seen",ImGuiTableColumnFlags_WidthStretch, 0.15f);
ImGui::TableHeadersRow();
for (auto& d : dbs) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(d.label.empty() ? d.id.c_str() : d.label.c_str());
ImGui::TableNextColumn();
badge(d.kind.c_str(), BadgeVariant::Info);
ImGui::TableNextColumn();
ImGui::TextUnformatted(d.uri.c_str());
ImGui::TableNextColumn();
ImGui::Text("%lld", d.table_count);
ImGui::TableNextColumn();
ImGui::TextUnformatted(format_bytes(d.size_bytes).c_str());
ImGui::TableNextColumn();
if (d.last_seen_at.empty()) ImGui::TextDisabled("-");
else ImGui::TextUnformatted(d.last_seen_at.c_str());
}
ImGui::EndTable();
}
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;
if (ImGui::BeginTable("##df_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), "%.1f%%", success_rate);
ImGui::TextDisabled("%d / %d terminal", success_all, terminal);
ImGui::TableNextColumn();
ImGui::Text("%s Failed (24h)", TI_ALERT_TRIANGLE);
if (failed_24h > 0)
ImGui::TextColored(ImVec4(0.95f, 0.35f, 0.30f, 1), "%d", failed_24h);
else
ImGui::Text("%d", failed_24h);
ImGui::TextDisabled("pending: %d", pending_total);
ImGui::TableNextColumn();
ImGui::Text("%s Throughput (24h)", TI_BOLT);
ImGui::Text("%lld rows", rows_24h);
ImGui::TextDisabled("%lld KB", kb_24h);
ImGui::EndTable();
}
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);
}
}
// 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);
int shown = 0;
if (ImGui::BeginTable("##df_node_runs", 5,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
{
ImGui::TableSetupColumn("Started", ImGuiTableColumnFlags_WidthStretch, 0.30f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.12f);
ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_WidthStretch, 0.13f);
ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthStretch, 0.10f);
ImGui::TableSetupColumn("Trigger", ImGuiTableColumnFlags_WidthStretch, 0.15f);
ImGui::TableHeadersRow();
for (auto& r : runs_all) {
if (r.node_id != nid) continue;
if (shown >= 10) break;
shown++;
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(r.started_at.c_str());
ImGui::TableNextColumn();
badge(r.status.c_str(), variant_for_status(r.status));
ImGui::TableNextColumn();
ImGui::TextUnformatted(format_duration(r.duration_ms).c_str());
ImGui::TableNextColumn();
ImGui::Text("%lld", r.rows_out);
ImGui::TableNextColumn();
ImGui::TextUnformatted(r.trigger.c_str());
}
ImGui::EndTable();
}
if (shown == 0) {
ImGui::TextDisabled("(no runs for this node yet)");
}
ImGui::End();
}
} // namespace data_factory_ui