docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-4
@@ -9,10 +9,8 @@ add_imgui_app(data_factory
|
|||||||
)
|
)
|
||||||
target_include_directories(data_factory PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(data_factory PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
# fn_table_viz: optional, kept for parity with dag_engine_ui.
|
# fn_module_data_table: required — tabs.cpp uses data_table::render (issue 0081).
|
||||||
if(TARGET fn_table_viz)
|
target_link_libraries(data_factory PRIVATE fn_module_data_table)
|
||||||
target_link_libraries(data_factory PRIVATE fn_table_viz)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(data_factory PRIVATE ws2_32)
|
target_link_libraries(data_factory PRIVATE ws2_32)
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ tags: [imgui, dashboard, data-pipeline, factory, http, websocket]
|
|||||||
uses_functions:
|
uses_functions:
|
||||||
- empty_state_cpp_core
|
- empty_state_cpp_core
|
||||||
- badge_cpp_core
|
- badge_cpp_core
|
||||||
|
- data_table_cpp_viz
|
||||||
uses_types: []
|
uses_types: []
|
||||||
|
uses_modules: [data_table_cpp]
|
||||||
framework: "imgui"
|
framework: "imgui"
|
||||||
entry_point: "main.cpp"
|
entry_point: "main.cpp"
|
||||||
dir_path: "apps/data_factory"
|
dir_path: "apps/data_factory"
|
||||||
repo_url: "https://gitea.organic-machine.com/dataforge/data_factory"
|
repo_url: "https://gitea.organic-machine.com/dataforge/data_factory"
|
||||||
|
icon:
|
||||||
|
phosphor: "factory"
|
||||||
|
accent: "#f97316"
|
||||||
e2e_checks:
|
e2e_checks:
|
||||||
- id: build_cmake
|
- id: build_cmake
|
||||||
cmd: "cmake --build cpp/build -j --target data_factory"
|
cmd: "cmake --build cpp/build -j --target data_factory"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -102,6 +102,17 @@ static void parse_run(const json& j, Run& r) {
|
|||||||
r.duration_ms = get_int64(j, "duration_ms");
|
r.duration_ms = get_int64(j, "duration_ms");
|
||||||
r.trigger = get_str(j, "trigger");
|
r.trigger = get_str(j, "trigger");
|
||||||
r.error = get_str(j, "error");
|
r.error = get_str(j, "error");
|
||||||
|
r.storage_db_id = get_str(j, "storage_db_id");
|
||||||
|
r.storage_table = get_str(j, "storage_table");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_table_entry(const json& j, TableEntry& t) {
|
||||||
|
t.database_id = get_str(j, "database_id");
|
||||||
|
t.database_label = get_str(j, "database_label");
|
||||||
|
t.database_kind = get_str(j, "database_kind");
|
||||||
|
t.table_name = get_str(j, "table_name");
|
||||||
|
t.row_count = get_int64(j, "row_count");
|
||||||
|
t.error = get_str(j, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parse_db(const json& j, DatabaseInfo& d) {
|
static void parse_db(const json& j, DatabaseInfo& d) {
|
||||||
@@ -191,6 +202,30 @@ bool list_databases_http(const std::string& api_url,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool list_tables_http(const std::string& api_url,
|
||||||
|
std::vector<TableEntry>& out) {
|
||||||
|
std::string host;
|
||||||
|
int port;
|
||||||
|
if (!parse_url(api_url, host, port)) return false;
|
||||||
|
HttpClient cli(host, port);
|
||||||
|
auto res = cli.get("/api/datafactory/tables");
|
||||||
|
if (!res.ok()) {
|
||||||
|
fprintf(stderr, "[df_http] list_tables failed: status=%d\n", res.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto j = json::parse(res.body, nullptr, false);
|
||||||
|
if (!j.is_object() || !j.contains("tables") || !j["tables"].is_array()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.clear();
|
||||||
|
for (auto& item : j["tables"]) {
|
||||||
|
TableEntry t;
|
||||||
|
parse_table_entry(item, t);
|
||||||
|
out.push_back(std::move(t));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool get_function_http(const std::string& api_url,
|
bool get_function_http(const std::string& api_url,
|
||||||
const std::string& function_id,
|
const std::string& function_id,
|
||||||
FnInfo& out) {
|
FnInfo& out) {
|
||||||
@@ -219,4 +254,61 @@ bool get_function_http(const std::string& api_url,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get_table_preview_http(const std::string& api_url,
|
||||||
|
const std::string& database_id,
|
||||||
|
const std::string& table,
|
||||||
|
int limit, int offset,
|
||||||
|
TablePreview& out) {
|
||||||
|
std::string host;
|
||||||
|
int port;
|
||||||
|
if (!parse_url(api_url, host, port)) return false;
|
||||||
|
if (database_id.empty() || table.empty()) return false;
|
||||||
|
HttpClient cli(host, port);
|
||||||
|
// Build query string manually (no URL encoding needed — IDs/table names
|
||||||
|
// are alphanumeric per the server-side validation regex).
|
||||||
|
char path[512];
|
||||||
|
std::snprintf(path, sizeof(path),
|
||||||
|
"/api/datafactory/preview?database_id=%s&table=%s&limit=%d&offset=%d",
|
||||||
|
database_id.c_str(), table.c_str(), limit, offset);
|
||||||
|
auto res = cli.get(path);
|
||||||
|
if (!res.ok()) {
|
||||||
|
fprintf(stderr, "[df_http] get_table_preview(%s.%s) failed: status=%d body=%s\n",
|
||||||
|
database_id.c_str(), table.c_str(), res.status, res.body.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto j = json::parse(res.body, nullptr, false);
|
||||||
|
if (!j.is_object()) return false;
|
||||||
|
|
||||||
|
out.database_id = get_str(j, "database_id");
|
||||||
|
out.table_name = get_str(j, "table_name");
|
||||||
|
out.total_rows = get_int64(j, "total_rows");
|
||||||
|
out.limit = get_int64(j, "limit");
|
||||||
|
out.offset = get_int64(j, "offset");
|
||||||
|
|
||||||
|
out.columns.clear();
|
||||||
|
if (j.contains("columns") && j["columns"].is_array()) {
|
||||||
|
for (auto& c : j["columns"]) {
|
||||||
|
std::string name = get_str(c, "name");
|
||||||
|
std::string type = get_str(c, "type");
|
||||||
|
out.columns.emplace_back(name, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.rows.clear();
|
||||||
|
if (j.contains("rows") && j["rows"].is_array()) {
|
||||||
|
for (auto& row : j["rows"]) {
|
||||||
|
if (!row.is_array()) continue;
|
||||||
|
std::vector<std::string> r;
|
||||||
|
r.reserve(row.size());
|
||||||
|
for (auto& cell : row) {
|
||||||
|
if (cell.is_string()) r.push_back(cell.get<std::string>());
|
||||||
|
else if (cell.is_null()) r.push_back("");
|
||||||
|
else r.push_back(cell.dump());
|
||||||
|
}
|
||||||
|
out.rows.push_back(std::move(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace data_factory
|
} // namespace data_factory
|
||||||
|
|||||||
+30
@@ -34,6 +34,8 @@ struct Run {
|
|||||||
long long duration_ms = 0;
|
long long duration_ms = 0;
|
||||||
std::string trigger;
|
std::string trigger;
|
||||||
std::string error;
|
std::string error;
|
||||||
|
std::string storage_db_id;
|
||||||
|
std::string storage_table;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DatabaseInfo {
|
struct DatabaseInfo {
|
||||||
@@ -47,6 +49,25 @@ struct DatabaseInfo {
|
|||||||
std::string last_seen_at;
|
std::string last_seen_at;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TableEntry {
|
||||||
|
std::string database_id;
|
||||||
|
std::string database_label;
|
||||||
|
std::string database_kind;
|
||||||
|
std::string table_name;
|
||||||
|
long long row_count = 0;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TablePreview {
|
||||||
|
std::string database_id;
|
||||||
|
std::string table_name;
|
||||||
|
std::vector<std::pair<std::string, std::string>> columns; // (name, type)
|
||||||
|
std::vector<std::vector<std::string>> rows;
|
||||||
|
long long total_rows = 0;
|
||||||
|
long long limit = 100;
|
||||||
|
long long offset = 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Mirrors dag_engine_ui FnInfo (response shape of GET /api/functions/{id}).
|
// Mirrors dag_engine_ui FnInfo (response shape of GET /api/functions/{id}).
|
||||||
struct FnInfo {
|
struct FnInfo {
|
||||||
std::string id;
|
std::string id;
|
||||||
@@ -69,8 +90,17 @@ bool list_runs_http(const std::string& api_url, const std::string& node_id,
|
|||||||
bool list_databases_http(const std::string& api_url,
|
bool list_databases_http(const std::string& api_url,
|
||||||
std::vector<DatabaseInfo>& out);
|
std::vector<DatabaseInfo>& out);
|
||||||
|
|
||||||
|
bool list_tables_http(const std::string& api_url,
|
||||||
|
std::vector<TableEntry>& out);
|
||||||
|
|
||||||
bool get_function_http(const std::string& api_url,
|
bool get_function_http(const std::string& api_url,
|
||||||
const std::string& function_id,
|
const std::string& function_id,
|
||||||
FnInfo& out);
|
FnInfo& out);
|
||||||
|
|
||||||
|
bool get_table_preview_http(const std::string& api_url,
|
||||||
|
const std::string& database_id,
|
||||||
|
const std::string& table,
|
||||||
|
int limit, int offset,
|
||||||
|
TablePreview& out);
|
||||||
|
|
||||||
} // namespace data_factory
|
} // namespace data_factory
|
||||||
|
|||||||
@@ -26,20 +26,23 @@ static std::string g_ws_path = "/api/ws/datafactory";
|
|||||||
static std::vector<data_factory::Node> g_nodes;
|
static std::vector<data_factory::Node> g_nodes;
|
||||||
static std::vector<data_factory::Run> g_runs_all;
|
static std::vector<data_factory::Run> g_runs_all;
|
||||||
static std::vector<data_factory::DatabaseInfo> g_databases;
|
static std::vector<data_factory::DatabaseInfo> g_databases;
|
||||||
|
static std::vector<data_factory::TableEntry> g_tables;
|
||||||
static WsClient g_ws;
|
static WsClient g_ws;
|
||||||
static int g_ws_msg_count = 0;
|
static int g_ws_msg_count = 0;
|
||||||
static bool g_initial_fetched = false;
|
static bool g_initial_fetched = false;
|
||||||
static bool g_refresh_pending = false;
|
static bool g_refresh_pending = false;
|
||||||
|
|
||||||
// Panel toggles.
|
// Panel toggles.
|
||||||
static bool g_show_map = true;
|
static bool g_show_map = true;
|
||||||
static bool g_show_extractors = true;
|
static bool g_show_extractors = true;
|
||||||
static bool g_show_transformers= true;
|
static bool g_show_transformers = true;
|
||||||
static bool g_show_databases = true;
|
static bool g_show_databases = true;
|
||||||
static bool g_show_sinks = true;
|
static bool g_show_tables = true;
|
||||||
static bool g_show_health = true;
|
static bool g_show_sinks = true;
|
||||||
static bool g_show_detail = true;
|
static bool g_show_health = true;
|
||||||
static bool g_show_live = false;
|
static bool g_show_detail = true;
|
||||||
|
static bool g_show_table_preview = true;
|
||||||
|
static bool g_show_live = false;
|
||||||
|
|
||||||
static void upsert_run(const data_factory::Run& r) {
|
static void upsert_run(const data_factory::Run& r) {
|
||||||
for (auto& existing : g_runs_all) {
|
for (auto& existing : g_runs_all) {
|
||||||
@@ -132,6 +135,7 @@ static void render() {
|
|||||||
g_refresh_pending = false;
|
g_refresh_pending = false;
|
||||||
data_factory::list_nodes_http(g_api_url, "", g_nodes);
|
data_factory::list_nodes_http(g_api_url, "", g_nodes);
|
||||||
data_factory::list_databases_http(g_api_url, g_databases);
|
data_factory::list_databases_http(g_api_url, g_databases);
|
||||||
|
data_factory::list_tables_http(g_api_url, g_tables);
|
||||||
std::vector<data_factory::Run> tmp;
|
std::vector<data_factory::Run> tmp;
|
||||||
if (data_factory::list_runs_http(g_api_url, "", 200, tmp)) {
|
if (data_factory::list_runs_http(g_api_url, "", 200, tmp)) {
|
||||||
for (auto& r : tmp) upsert_run(r);
|
for (auto& r : tmp) upsert_run(r);
|
||||||
@@ -149,11 +153,14 @@ static void render() {
|
|||||||
if (g_show_extractors) data_factory_ui::draw_extractors(g_api_url, g_nodes, g_runs_all);
|
if (g_show_extractors) data_factory_ui::draw_extractors(g_api_url, g_nodes, g_runs_all);
|
||||||
if (g_show_transformers) data_factory_ui::draw_transformers(g_api_url, g_nodes, g_runs_all);
|
if (g_show_transformers) data_factory_ui::draw_transformers(g_api_url, g_nodes, g_runs_all);
|
||||||
if (g_show_databases) data_factory_ui::draw_databases(g_api_url, g_databases);
|
if (g_show_databases) data_factory_ui::draw_databases(g_api_url, g_databases);
|
||||||
|
if (g_show_tables) data_factory_ui::draw_tables(g_api_url, g_tables);
|
||||||
if (g_show_sinks) data_factory_ui::draw_sinks(g_api_url, g_nodes, g_runs_all);
|
if (g_show_sinks) data_factory_ui::draw_sinks(g_api_url, g_nodes, g_runs_all);
|
||||||
if (g_show_health) data_factory_ui::draw_health(g_api_url, g_runs_all);
|
if (g_show_health) data_factory_ui::draw_health(g_api_url, g_runs_all);
|
||||||
if (g_show_detail) data_factory_ui::draw_node_detail_panel(
|
if (g_show_detail) data_factory_ui::draw_node_detail_panel(
|
||||||
g_api_url, g_nodes, g_runs_all, &g_show_detail);
|
g_api_url, g_nodes, g_runs_all, &g_show_detail);
|
||||||
if (g_show_live) draw_live();
|
if (g_show_table_preview) data_factory_ui::draw_table_preview_panel(
|
||||||
|
g_api_url, &g_show_table_preview);
|
||||||
|
if (g_show_live) draw_live();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self-test: blocking HTTP GET to sqlite_api /api/datafactory/nodes. No GUI.
|
// Self-test: blocking HTTP GET to sqlite_api /api/datafactory/nodes. No GUI.
|
||||||
@@ -179,14 +186,16 @@ int main(int argc, char** argv) {
|
|||||||
g_ws.start(g_ws_host, g_ws_port, g_ws_path);
|
g_ws.start(g_ws_host, g_ws_port, g_ws_path);
|
||||||
|
|
||||||
static fn_ui::PanelToggle panels[] = {
|
static fn_ui::PanelToggle panels[] = {
|
||||||
{ "Map", nullptr, &g_show_map },
|
{ "Map", nullptr, &g_show_map },
|
||||||
{ "Extractors", nullptr, &g_show_extractors },
|
{ "Extractors", nullptr, &g_show_extractors },
|
||||||
{ "Transformers", nullptr, &g_show_transformers },
|
{ "Transformers", nullptr, &g_show_transformers },
|
||||||
{ "Databases", nullptr, &g_show_databases },
|
{ "Databases", nullptr, &g_show_databases },
|
||||||
{ "Sinks", nullptr, &g_show_sinks },
|
{ "Tables", nullptr, &g_show_tables },
|
||||||
{ "Health", nullptr, &g_show_health },
|
{ "Table Preview", nullptr, &g_show_table_preview },
|
||||||
{ "Node Detail", nullptr, &g_show_detail },
|
{ "Sinks", nullptr, &g_show_sinks },
|
||||||
{ "Live (WS)", nullptr, &g_show_live },
|
{ "Health", nullptr, &g_show_health },
|
||||||
|
{ "Node Detail", nullptr, &g_show_detail },
|
||||||
|
{ "Live (WS)", nullptr, &g_show_live },
|
||||||
};
|
};
|
||||||
|
|
||||||
fn::AppConfig cfg;
|
fn::AppConfig cfg;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Migration 002: track where each run's extracted data is stored.
|
||||||
|
-- Aditiva, idempotente (SQLite ALTER ADD COLUMN + "duplicate column" ignorado en app code).
|
||||||
|
|
||||||
|
ALTER TABLE runs ADD COLUMN storage_db_id TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE runs ADD COLUMN storage_table TEXT NOT NULL DEFAULT '';
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "tabs.h"
|
#include "tabs.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include "core/data_table_types.h"
|
||||||
#include "core/icons_tabler.h"
|
#include "core/icons_tabler.h"
|
||||||
#include "core/empty_state.h"
|
#include "core/empty_state.h"
|
||||||
#include "core/badge.h"
|
#include "core/badge.h"
|
||||||
@@ -9,9 +11,115 @@
|
|||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace data_factory_ui {
|
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
|
// Globals
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -64,14 +172,6 @@ static time_t parse_rfc3339(const std::string& s) {
|
|||||||
return std::mktime(&tm);
|
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.
|
// Pick most-recent run per node from runs_all.
|
||||||
static const data_factory::Run* last_run_for(
|
static const data_factory::Run* last_run_for(
|
||||||
const std::string& node_id,
|
const std::string& node_id,
|
||||||
@@ -89,83 +189,119 @@ static const data_factory::Run* last_run_for(
|
|||||||
// Generic kind table — used by extractors / transformers / sinks
|
// Generic kind table — used by extractors / transformers / sinks
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static void draw_node_table(const char* table_id,
|
// 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::vector<data_factory::Node>& nodes,
|
||||||
const std::string& filter_kind,
|
const std::string& filter_kind,
|
||||||
const std::vector<data_factory::Run>& runs_all,
|
const std::vector<data_factory::Run>& runs_all,
|
||||||
bool show_schedule)
|
bool show_schedule)
|
||||||
{
|
{
|
||||||
int cols = show_schedule ? 6 : 5;
|
// Pick the per-kind state + backing.
|
||||||
if (!ImGui::BeginTable(table_id, cols,
|
data_table::State* st = &g_st_nodes_extractors;
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
std::vector<std::string>* backing = &g_back_extractors;
|
||||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY))
|
std::vector<const char*>* ptrs = &g_ptrs_extractors;
|
||||||
{
|
const char* dt_id = "##dt_extractors";
|
||||||
return;
|
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";
|
||||||
}
|
}
|
||||||
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;
|
// Filter nodes for the current kind, and pre-resolve their last-run.
|
||||||
for (auto& n : nodes) {
|
std::vector<const data_factory::Node*> filtered;
|
||||||
if (n.kind != filter_kind) continue;
|
filtered.reserve(nodes.size());
|
||||||
ImGui::PushID(row_idx++);
|
for (auto& n : nodes) if (n.kind == filter_kind) filtered.push_back(&n);
|
||||||
ImGui::TableNextRow();
|
|
||||||
|
|
||||||
// Name (selectable -> sets selection)
|
data_table::TableInput tbl;
|
||||||
ImGui::TableNextColumn();
|
tbl.name = filter_kind;
|
||||||
bool selected = (selection().node_id == n.id);
|
if (show_schedule) {
|
||||||
if (ImGui::Selectable(n.name.c_str(), selected,
|
tbl.headers = {"Name", "Function", "Schedule", "Last Run", "Status", "Rows/KB", "Enabled"};
|
||||||
ImGuiSelectableFlags_SpanAllColumns))
|
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;
|
selection().node_id = n.id;
|
||||||
// Invalidate function cache if function_id changed.
|
|
||||||
if (function_cache().function_id != n.function_id) {
|
if (function_cache().function_id != n.function_id) {
|
||||||
function_cache() = {};
|
function_cache() = {};
|
||||||
function_cache().function_id = n.function_id;
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -243,6 +379,101 @@ void draw_sinks(const std::string& /*api_url*/,
|
|||||||
ImGui::End();
|
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
|
// Databases
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -260,35 +491,44 @@ void draw_databases(const std::string& /*api_url*/,
|
|||||||
ImGui::End();
|
ImGui::End();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ImGui::BeginTable("##df_databases", 6,
|
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
||||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY))
|
|
||||||
{
|
{
|
||||||
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch, 0.18f);
|
data_table::TableInput tbl;
|
||||||
ImGui::TableSetupColumn("Kind", ImGuiTableColumnFlags_WidthStretch, 0.10f);
|
tbl.name = "databases";
|
||||||
ImGui::TableSetupColumn("URI", ImGuiTableColumnFlags_WidthStretch, 0.32f);
|
tbl.headers = {"Label", "Kind", "URI", "Tables", "Size", "Last Seen"};
|
||||||
ImGui::TableSetupColumn("Tables", ImGuiTableColumnFlags_WidthStretch, 0.10f);
|
tbl.types = {
|
||||||
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthStretch, 0.15f);
|
data_table::ColumnType::String,
|
||||||
ImGui::TableSetupColumn("Last Seen",ImGuiTableColumnFlags_WidthStretch, 0.15f);
|
data_table::ColumnType::String,
|
||||||
ImGui::TableHeadersRow();
|
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) {
|
for (auto& d : dbs) {
|
||||||
ImGui::TableNextRow();
|
g_back_databases.push_back(d.label.empty() ? d.id : d.label);
|
||||||
ImGui::TableNextColumn();
|
g_back_databases.push_back(d.kind);
|
||||||
ImGui::TextUnformatted(d.label.empty() ? d.id.c_str() : d.label.c_str());
|
g_back_databases.push_back(d.uri);
|
||||||
ImGui::TableNextColumn();
|
{
|
||||||
badge(d.kind.c_str(), BadgeVariant::Info);
|
char buf[32];
|
||||||
ImGui::TableNextColumn();
|
std::snprintf(buf, sizeof(buf), "%lld", d.table_count);
|
||||||
ImGui::TextUnformatted(d.uri.c_str());
|
g_back_databases.push_back(buf);
|
||||||
ImGui::TableNextColumn();
|
}
|
||||||
ImGui::Text("%lld", d.table_count);
|
g_back_databases.push_back(format_bytes(d.size_bytes));
|
||||||
ImGui::TableNextColumn();
|
g_back_databases.push_back(d.last_seen_at.empty() ? "-" : d.last_seen_at);
|
||||||
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();
|
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();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
@@ -338,35 +578,45 @@ void draw_health(const std::string& /*api_url*/,
|
|||||||
float success_rate = (terminal > 0)
|
float success_rate = (terminal > 0)
|
||||||
? (100.0f * (float)success_all / (float)terminal) : 0.0f;
|
? (100.0f * (float)success_all / (float)terminal) : 0.0f;
|
||||||
|
|
||||||
if (ImGui::BeginTable("##df_kpis", 4,
|
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame))
|
|
||||||
{
|
{
|
||||||
ImGui::TableNextRow();
|
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];
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
g_back_kpis.clear();
|
||||||
ImGui::Text("%s Runs (24h)", TI_ACTIVITY);
|
g_back_kpis.reserve(tbl.rows * tbl.cols);
|
||||||
ImGui::Text("%d", runs_24h);
|
char buf[64];
|
||||||
ImGui::TextDisabled("success: %d", success_24h);
|
// 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);
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
cells_to_ptrs(g_back_kpis, g_ptrs_kpis);
|
||||||
ImGui::Text("%s Success rate", TI_CHECK);
|
tbl.cells = g_ptrs_kpis.data();
|
||||||
ImGui::TextColored(ImVec4(0.30f, 0.85f, 0.40f, 1), "%.1f%%", success_rate);
|
|
||||||
ImGui::TextDisabled("%d / %d terminal", success_all, terminal);
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
data_table::render("##dt_kpis", {tbl}, g_st_kpis);
|
||||||
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::Separator();
|
||||||
@@ -498,6 +748,27 @@ void draw_node_detail_panel(const std::string& api_url,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
// Function metadata card (lazy load)
|
||||||
if (!node->function_id.empty()) {
|
if (!node->function_id.empty()) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -545,37 +816,217 @@ void draw_node_detail_panel(const std::string& api_url,
|
|||||||
// Recent runs (top 10)
|
// Recent runs (top 10)
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Text("%s Recent runs", TI_HISTORY);
|
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) {
|
// Filter + cap.
|
||||||
if (r.node_id != nid) continue;
|
std::vector<const data_factory::Run*> shown_runs;
|
||||||
if (shown >= 10) break;
|
shown_runs.reserve(10);
|
||||||
shown++;
|
for (auto& r : runs_all) {
|
||||||
ImGui::TableNextRow();
|
if (r.node_id != nid) continue;
|
||||||
ImGui::TableNextColumn();
|
shown_runs.push_back(&r);
|
||||||
ImGui::TextUnformatted(r.started_at.c_str());
|
if (shown_runs.size() >= 10) break;
|
||||||
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) {
|
|
||||||
|
if (shown_runs.empty()) {
|
||||||
ImGui::TextDisabled("(no runs for this node yet)");
|
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();
|
ImGui::End();
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ void draw_transformers(const std::string& api_url,
|
|||||||
void draw_databases(const std::string& api_url,
|
void draw_databases(const std::string& api_url,
|
||||||
const std::vector<data_factory::DatabaseInfo>& dbs);
|
const std::vector<data_factory::DatabaseInfo>& dbs);
|
||||||
|
|
||||||
|
void draw_tables(const std::string& api_url,
|
||||||
|
const std::vector<data_factory::TableEntry>& tables);
|
||||||
|
|
||||||
void draw_sinks(const std::string& api_url,
|
void draw_sinks(const std::string& api_url,
|
||||||
const std::vector<data_factory::Node>& nodes,
|
const std::vector<data_factory::Node>& nodes,
|
||||||
const std::vector<data_factory::Run>& runs_all);
|
const std::vector<data_factory::Run>& runs_all);
|
||||||
@@ -59,4 +62,6 @@ void draw_node_detail_panel(const std::string& api_url,
|
|||||||
const std::vector<data_factory::Run>& runs_all,
|
const std::vector<data_factory::Run>& runs_all,
|
||||||
bool* p_open);
|
bool* p_open);
|
||||||
|
|
||||||
|
void draw_table_preview_panel(const std::string& api_url, bool* p_open);
|
||||||
|
|
||||||
} // namespace data_factory_ui
|
} // namespace data_factory_ui
|
||||||
|
|||||||
Reference in New Issue
Block a user