merge data_table migration (issue 0081-J)

Migrates registry_dashboard tables to data_table::render() via fn_table_viz.
This commit is contained in:
2026-05-15 14:39:55 +02:00
3 changed files with 343 additions and 139 deletions
+5
View File
@@ -53,6 +53,11 @@ target_include_directories(registry_dashboard PRIVATE
target_link_libraries(registry_dashboard PRIVATE SQLite::SQLite3) target_link_libraries(registry_dashboard PRIVATE SQLite::SQLite3)
# Issue 0081-J: data_table::render via fn_table_viz static lib
if(TARGET fn_table_viz)
target_link_libraries(registry_dashboard PRIVATE fn_table_viz)
endif()
# Sockets: ws2_32 on Windows, nothing extra on Linux # Sockets: ws2_32 on Windows, nothing extra on Linux
if(WIN32) if(WIN32)
target_link_libraries(registry_dashboard PRIVATE ws2_32) target_link_libraries(registry_dashboard PRIVATE ws2_32)
+12
View File
@@ -11,6 +11,18 @@ uses_functions:
- pie_chart_cpp_viz - pie_chart_cpp_viz
- table_view_cpp_viz - table_view_cpp_viz
- sparkline_cpp_viz - sparkline_cpp_viz
# data_table stack (issue 0081-J)
- data_table_cpp_viz
- viz_render_cpp_viz
- compute_stage_cpp_core
- compute_pipeline_cpp_core
- tql_emit_cpp_core
- tql_apply_cpp_core
- tql_to_sql_cpp_core
- lua_engine_cpp_core
- join_tables_cpp_core
- auto_detect_type_cpp_core
- compute_column_stats_cpp_core
# core (dashboard primitives) # core (dashboard primitives)
- dashboard_panel_cpp_core - dashboard_panel_cpp_core
- dashboard_grid_cpp_core - dashboard_grid_cpp_core
+323 -136
View File
@@ -9,6 +9,8 @@
#include "viz/pie_chart.h" #include "viz/pie_chart.h"
#include "viz/table_view.h" #include "viz/table_view.h"
#include "viz/sparkline.h" #include "viz/sparkline.h"
#include "viz/data_table.h"
#include "core/data_table_types.h"
#include "core/icons_tabler.h" #include "core/icons_tabler.h"
#include "core/dashboard_panel.h" #include "core/dashboard_panel.h"
#include "core/dashboard_grid.h" #include "core/dashboard_grid.h"
@@ -103,6 +105,191 @@ static std::string format_ts_relative(long long ts) {
} }
static fn_ui::ProcessRunner g_add_runner; static fn_ui::ProcessRunner g_add_runner;
// ---------------------------------------------------------------------------
// data_table::State — persistent per panel (issue 0081-J)
// ---------------------------------------------------------------------------
// One State per data-table panel. Must NOT be stack-local (see data_table.md
// Gotchas: "State no stack-local").
static data_table::State g_dt_recent_funcs;
static data_table::State g_dt_apps;
static data_table::State g_dt_analysis;
static data_table::State g_dt_types;
static data_table::State g_dt_vaults;
static data_table::State g_dt_top_fn;
static data_table::State g_dt_violations;
static data_table::State g_dt_copies;
// ---------------------------------------------------------------------------
// Helpers: build a data_table::TableInput from registry row vectors
// ---------------------------------------------------------------------------
// Build a flat cells vector (row-major) + keep string backing alive.
// Returns backing storage; `ti` is populated in-place.
static std::vector<std::string> make_cells_recent_funcs(
const std::vector<FunctionRow>& funcs,
data_table::TableInput& ti)
{
ti.name = "functions";
ti.headers = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"};
ti.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::Date,
};
ti.rows = static_cast<int>(funcs.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(funcs.size() * ti.cols);
for (const auto& f : funcs) {
backing.push_back(f.name);
backing.push_back(f.lang);
backing.push_back(f.domain);
backing.push_back(f.kind);
backing.push_back(f.purity);
backing.push_back(f.tested ? "yes" : "no");
backing.push_back(f.created_at.substr(0, 10));
}
return backing;
}
static std::vector<std::string> make_cells_apps(
const std::vector<AppRow>& apps,
data_table::TableInput& ti)
{
ti.name = "apps";
ti.headers = {"Name", "Lang", "Domain", "Framework", "Git", "Description"};
ti.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,
};
ti.rows = static_cast<int>(apps.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(apps.size() * ti.cols);
for (const auto& a : apps) {
std::string git_status;
if (!a.repo_url.empty()) {
git_status = "remote";
} else if (!a.dir_path.empty()) {
std::error_code ec;
if (std::filesystem::exists(
std::filesystem::path(a.dir_path) / ".git", ec))
git_status = "local";
else
git_status = "-";
} else {
git_status = "-";
}
backing.push_back(a.name);
backing.push_back(a.lang);
backing.push_back(a.domain);
backing.push_back(a.framework);
backing.push_back(git_status);
backing.push_back(a.description);
}
return backing;
}
static std::vector<std::string> make_cells_analysis(
const std::vector<AnalysisRow>& analyses,
data_table::TableInput& ti)
{
ti.name = "analysis";
ti.headers = {"Name", "Lang", "Domain", "Description"};
ti.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
};
ti.rows = static_cast<int>(analyses.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(analyses.size() * ti.cols);
for (const auto& a : analyses) {
backing.push_back(a.name);
backing.push_back(a.lang);
backing.push_back(a.domain);
backing.push_back(a.description);
}
return backing;
}
static std::vector<std::string> make_cells_types(
const std::vector<TypeRow>& types,
data_table::TableInput& ti)
{
ti.name = "types";
ti.headers = {"Name", "Lang", "Domain", "Algebraic", "Description"};
ti.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
};
ti.rows = static_cast<int>(types.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(types.size() * ti.cols);
for (const auto& t : types) {
backing.push_back(t.name);
backing.push_back(t.lang);
backing.push_back(t.domain);
backing.push_back(t.algebraic);
backing.push_back(t.description);
}
return backing;
}
static std::vector<std::string> make_cells_vaults(
const std::vector<VaultRow>& vaults,
data_table::TableInput& ti)
{
ti.name = "vaults";
ti.headers = {"Name", "Path", "Symlink", "Description"};
ti.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
};
ti.rows = static_cast<int>(vaults.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(vaults.size() * ti.cols);
for (const auto& v : vaults) {
backing.push_back(v.name);
backing.push_back(v.path);
backing.push_back(v.symlink ? "yes" : "no");
backing.push_back(v.description);
}
return backing;
}
// Converts backing vector to flat const char* array for TableInput.cells.
// `ptrs` must outlive the render() call.
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();
}
// Add modal state // Add modal state
enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 }; enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 };
static bool g_show_add = false; static bool g_show_add = false;
@@ -275,21 +462,15 @@ void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
"Run 'fn index' to populate the registry"); "Run 'fn index' to populate the registry");
return; return;
} }
const char* headers[] = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"}; data_table::TableInput ti;
constexpr int cols = 7; auto backing = make_cells_recent_funcs(funcs, ti);
std::vector<std::string> cell_strings; std::vector<const char*> ptrs;
cell_strings.reserve(funcs.size() * cols); cells_to_ptrs(backing, ptrs);
for (auto& f : funcs) { ti.cells = ptrs.data();
cell_strings.push_back(f.name);
cell_strings.push_back(f.lang); ImGui::BeginChild("##recent_dt_wrap", ImVec2(-1, -1));
cell_strings.push_back(f.domain); data_table::render("##dt_recent_funcs", {ti}, g_dt_recent_funcs);
cell_strings.push_back(f.kind); ImGui::EndChild();
cell_strings.push_back(f.purity);
cell_strings.push_back(f.tested ? "yes" : "no");
cell_strings.push_back(f.created_at.substr(0, 10));
}
auto cells = to_cstr(cell_strings);
table_view("##recent", headers, cols, cells.data(), static_cast<int>(funcs.size()));
} }
void draw_apps_list(const std::vector<AppRow>& apps) { void draw_apps_list(const std::vector<AppRow>& apps) {
@@ -298,37 +479,15 @@ void draw_apps_list(const std::vector<AppRow>& apps) {
"Use the + Add button above or run 'fn sync'"); "Use the + Add button above or run 'fn sync'");
return; return;
} }
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Git", "Description"}; data_table::TableInput ti;
constexpr int cols = 6; auto backing = make_cells_apps(apps, ti);
std::vector<std::string> cell_strings; std::vector<const char*> ptrs;
cell_strings.reserve(apps.size() * cols); cells_to_ptrs(backing, ptrs);
for (auto& a : apps) { ti.cells = ptrs.data();
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang); ImGui::BeginChild("##apps_dt_wrap", ImVec2(-1, -1));
cell_strings.push_back(a.domain); data_table::render("##dt_apps", {ti}, g_dt_apps);
cell_strings.push_back(a.framework); ImGui::EndChild();
// Indicador de git: tiene repo remoto (gitea), solo local, o ninguno.
// - "remote": repo_url poblado en el frontmatter del app.md
// - "local": hay .git/ en dir_path pero sin repo_url
// - "-": ni .git ni repo_url
std::string git_status;
if (!a.repo_url.empty()) {
git_status = "remote";
} else if (!a.dir_path.empty()) {
std::error_code ec;
if (std::filesystem::exists(std::filesystem::path(a.dir_path) / ".git", ec)) {
git_status = "local";
} else {
git_status = "-";
}
} else {
git_status = "-";
}
cell_strings.push_back(git_status);
cell_strings.push_back(a.description);
}
auto cells = to_cstr(cell_strings);
table_view("##apps", headers, cols, cells.data(), static_cast<int>(apps.size()));
} }
void draw_analysis_list(const std::vector<AnalysisRow>& analyses) { void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
@@ -337,18 +496,15 @@ void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
"Use the + Add button above with kind = Analysis"); "Use the + Add button above with kind = Analysis");
return; return;
} }
const char* headers[] = {"Name", "Lang", "Domain", "Description"}; data_table::TableInput ti;
constexpr int cols = 4; auto backing = make_cells_analysis(analyses, ti);
std::vector<std::string> cell_strings; std::vector<const char*> ptrs;
cell_strings.reserve(analyses.size() * cols); cells_to_ptrs(backing, ptrs);
for (auto& a : analyses) { ti.cells = ptrs.data();
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang); ImGui::BeginChild("##analysis_dt_wrap", ImVec2(-1, -1));
cell_strings.push_back(a.domain); data_table::render("##dt_analysis", {ti}, g_dt_analysis);
cell_strings.push_back(a.description); ImGui::EndChild();
}
auto cells = to_cstr(cell_strings);
table_view("##analysis", headers, cols, cells.data(), static_cast<int>(analyses.size()));
} }
void draw_types_list(const std::vector<TypeRow>& types) { void draw_types_list(const std::vector<TypeRow>& types) {
@@ -357,19 +513,15 @@ void draw_types_list(const std::vector<TypeRow>& types) {
"Types are indexed from the registry alongside functions"); "Types are indexed from the registry alongside functions");
return; return;
} }
const char* headers[] = {"Name", "Lang", "Domain", "Algebraic", "Description"}; data_table::TableInput ti;
constexpr int cols = 5; auto backing = make_cells_types(types, ti);
std::vector<std::string> cell_strings; std::vector<const char*> ptrs;
cell_strings.reserve(types.size() * cols); cells_to_ptrs(backing, ptrs);
for (auto& t : types) { ti.cells = ptrs.data();
cell_strings.push_back(t.name);
cell_strings.push_back(t.lang); ImGui::BeginChild("##types_dt_wrap", ImVec2(-1, -1));
cell_strings.push_back(t.domain); data_table::render("##dt_types", {ti}, g_dt_types);
cell_strings.push_back(t.algebraic); ImGui::EndChild();
cell_strings.push_back(t.description);
}
auto cells = to_cstr(cell_strings);
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -686,27 +838,44 @@ void draw_monitor(RegistryData& data) {
if (cu.top_functions.empty()) { if (cu.top_functions.empty()) {
ImGui::TextDisabled("No function calls recorded yet. Hook fires on next session."); ImGui::TextDisabled("No function calls recorded yet. Hook fires on next session.");
} else { } else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders // Build TableInput for data_table::render
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; data_table::TableInput ti;
if (ImGui::BeginTable("##monitor_top_fn", 6, tf, ImVec2(0, 0))) { ti.name = "top_functions";
ImGui::TableSetupColumn("Function ID"); ti.headers = {"Function ID", "Calls", "7d", "Errors", "Error %", "Mean ms"};
ImGui::TableSetupColumn("Calls"); ti.types = {
ImGui::TableSetupColumn("7d"); data_table::ColumnType::String,
ImGui::TableSetupColumn("Errors"); data_table::ColumnType::Int,
ImGui::TableSetupColumn("Error %"); data_table::ColumnType::Int,
ImGui::TableSetupColumn("Mean ms"); data_table::ColumnType::Int,
ImGui::TableHeadersRow(); data_table::ColumnType::Float,
data_table::ColumnType::Float,
};
ti.rows = static_cast<int>(cu.top_functions.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(cu.top_functions.size() * 6);
char buf[32];
for (const auto& r : cu.top_functions) { for (const auto& r : cu.top_functions) {
ImGui::TableNextRow(); backing.push_back(r.function_id);
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.function_id.c_str()); std::snprintf(buf, sizeof(buf), "%d", r.calls_total);
ImGui::TableSetColumnIndex(1); ImGui::Text("%d", r.calls_total); backing.push_back(buf);
ImGui::TableSetColumnIndex(2); ImGui::Text("%d", r.calls_7d); std::snprintf(buf, sizeof(buf), "%d", r.calls_7d);
ImGui::TableSetColumnIndex(3); ImGui::Text("%d", r.errors_total); backing.push_back(buf);
ImGui::TableSetColumnIndex(4); ImGui::Text("%.1f%%", r.error_rate * 100.0); std::snprintf(buf, sizeof(buf), "%d", r.errors_total);
ImGui::TableSetColumnIndex(5); ImGui::Text("%.0f", r.mean_duration_ms); backing.push_back(buf);
} std::snprintf(buf, sizeof(buf), "%.1f%%", r.error_rate * 100.0);
ImGui::EndTable(); backing.push_back(buf);
std::snprintf(buf, sizeof(buf), "%.0f", r.mean_duration_ms);
backing.push_back(buf);
} }
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##top_fn_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_top_fn", {ti}, g_dt_top_fn);
ImGui::EndChild();
} }
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
@@ -714,25 +883,35 @@ void draw_monitor(RegistryData& data) {
if (cu.recent_violations.empty()) { if (cu.recent_violations.empty()) {
ImGui::TextDisabled("No antipattern violations detected."); ImGui::TextDisabled("No antipattern violations detected.");
} else { } else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders data_table::TableInput ti;
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; ti.name = "violations";
if (ImGui::BeginTable("##monitor_viol", 5, tf, ImVec2(0, 0))) { ti.headers = {"When", "Rule", "Severity", "Function", "Snippet"};
ImGui::TableSetupColumn("When"); ti.types = {
ImGui::TableSetupColumn("Rule"); data_table::ColumnType::String,
ImGui::TableSetupColumn("Severity"); data_table::ColumnType::String,
ImGui::TableSetupColumn("Function"); data_table::ColumnType::String,
ImGui::TableSetupColumn("Snippet"); data_table::ColumnType::String,
ImGui::TableHeadersRow(); data_table::ColumnType::String,
};
ti.rows = static_cast<int>(cu.recent_violations.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(cu.recent_violations.size() * 5);
for (const auto& r : cu.recent_violations) { for (const auto& r : cu.recent_violations) {
ImGui::TableNextRow(); backing.push_back(format_ts(r.ts));
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(format_ts(r.ts).c_str()); backing.push_back(r.rule_id);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(r.rule_id.c_str()); backing.push_back(r.severity);
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.severity.c_str()); backing.push_back(r.function_id);
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.function_id.c_str()); backing.push_back(r.command_snippet);
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.command_snippet.c_str());
}
ImGui::EndTable();
} }
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##viol_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_violations", {ti}, g_dt_violations);
ImGui::EndChild();
} }
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
@@ -793,25 +972,37 @@ void draw_monitor(RegistryData& data) {
if (cu.copies.empty()) { if (cu.copies.empty()) {
ImGui::TextDisabled("No copied code detected. Run `fn doctor copied-code` or `call_monitor copied-code`."); ImGui::TextDisabled("No copied code detected. Run `fn doctor copied-code` or `call_monitor copied-code`.");
} else { } else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders data_table::TableInput ti;
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; ti.name = "copies";
if (ImGui::BeginTable("##monitor_copies", 5, tf, ImVec2(0, 0))) { ti.headers = {"Kind", "Sim", "App File", "App Function", "Registry ID"};
ImGui::TableSetupColumn("Kind"); ti.types = {
ImGui::TableSetupColumn("Sim"); data_table::ColumnType::String,
ImGui::TableSetupColumn("App File"); data_table::ColumnType::Float,
ImGui::TableSetupColumn("App Function"); data_table::ColumnType::String,
ImGui::TableSetupColumn("Registry ID"); data_table::ColumnType::String,
ImGui::TableHeadersRow(); data_table::ColumnType::String,
};
ti.rows = static_cast<int>(cu.copies.size());
ti.cols = static_cast<int>(ti.headers.size());
std::vector<std::string> backing;
backing.reserve(cu.copies.size() * 5);
char buf[32];
for (const auto& r : cu.copies) { for (const auto& r : cu.copies) {
ImGui::TableNextRow(); backing.push_back(r.kind);
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.kind.c_str()); std::snprintf(buf, sizeof(buf), "%.2f", r.similarity);
ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f", r.similarity); backing.push_back(buf);
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.app_file.c_str()); backing.push_back(r.app_file);
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.app_function.c_str()); backing.push_back(r.app_function);
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.registry_id.c_str()); backing.push_back(r.registry_id);
}
ImGui::EndTable();
} }
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##copies_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_copies", {ti}, g_dt_copies);
ImGui::EndChild();
} }
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
@@ -916,18 +1107,14 @@ void draw_projects_list(RegistryData& data) {
empty_state("( no data )", "No vaults in this project", empty_state("( no data )", "No vaults in this project",
"Use the + Add button with kind = Vault (requires a project)"); "Use the + Add button with kind = Vault (requires a project)");
} else { } else {
const char* headers[] = {"Name", "Path", "Symlink", "Description"}; data_table::TableInput ti;
std::vector<std::string> cells; auto backing = make_cells_vaults(d.vaults, ti);
cells.reserve(d.vaults.size() * 4); std::vector<const char*> ptrs;
for (auto& v : d.vaults) { cells_to_ptrs(backing, ptrs);
cells.push_back(v.name); ti.cells = ptrs.data();
cells.push_back(v.path); ImGui::BeginChild("##vaults_dt_wrap", ImVec2(-1, -1));
cells.push_back(v.symlink ? "yes" : "no"); data_table::render("##dt_vaults", {ti}, g_dt_vaults);
cells.push_back(v.description); ImGui::EndChild();
}
auto cp = to_cstr(cells);
table_view("##vaults", headers, 4, cp.data(),
static_cast<int>(d.vaults.size()));
} }
ImGui::EndTabItem(); ImGui::EndTabItem();
} }