migrate function/type/app tables to data_table_cpp_viz (issue 0081-J)

- Replace table_view() calls in draw_recent_functions, draw_apps_list,
  draw_analysis_list, draw_types_list, and vaults panel with
  data_table::render() via fn_table_viz static lib.
- Migrate Monitor sub-tabs Top Functions, Violations, Copied Code to
  data_table::render() with persistent State per panel.
- Keep Recent Executions and Failed Functions as custom ImGui tables
  (per-cell coloring + tooltips not supported by data_table).
- Layout-splitter tables (kpi_grid, chart_grid, monitor_kpi,
  proj_layout, explorer_layout) intentionally not migrated.
- CMakeLists: target_link_libraries(registry_dashboard PRIVATE fn_table_viz).
- app.md uses_functions += data_table_cpp_viz + full fn_table_viz stack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 14:39:51 +02:00
parent 00ee6a93e3
commit 3d7c5bc0a1
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)
# 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
if(WIN32)
target_link_libraries(registry_dashboard PRIVATE ws2_32)
+12
View File
@@ -11,6 +11,18 @@ uses_functions:
- pie_chart_cpp_viz
- table_view_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)
- dashboard_panel_cpp_core
- dashboard_grid_cpp_core
+326 -139
View File
@@ -9,6 +9,8 @@
#include "viz/pie_chart.h"
#include "viz/table_view.h"
#include "viz/sparkline.h"
#include "viz/data_table.h"
#include "core/data_table_types.h"
#include "core/icons_tabler.h"
#include "core/dashboard_panel.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;
// ---------------------------------------------------------------------------
// 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
enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 };
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");
return;
}
const char* headers[] = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"};
constexpr int cols = 7;
std::vector<std::string> cell_strings;
cell_strings.reserve(funcs.size() * cols);
for (auto& f : funcs) {
cell_strings.push_back(f.name);
cell_strings.push_back(f.lang);
cell_strings.push_back(f.domain);
cell_strings.push_back(f.kind);
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()));
data_table::TableInput ti;
auto backing = make_cells_recent_funcs(funcs, ti);
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##recent_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_recent_funcs", {ti}, g_dt_recent_funcs);
ImGui::EndChild();
}
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'");
return;
}
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Git", "Description"};
constexpr int cols = 6;
std::vector<std::string> cell_strings;
cell_strings.reserve(apps.size() * cols);
for (auto& a : apps) {
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang);
cell_strings.push_back(a.domain);
cell_strings.push_back(a.framework);
// 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()));
data_table::TableInput ti;
auto backing = make_cells_apps(apps, ti);
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##apps_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_apps", {ti}, g_dt_apps);
ImGui::EndChild();
}
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");
return;
}
const char* headers[] = {"Name", "Lang", "Domain", "Description"};
constexpr int cols = 4;
std::vector<std::string> cell_strings;
cell_strings.reserve(analyses.size() * cols);
for (auto& a : analyses) {
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang);
cell_strings.push_back(a.domain);
cell_strings.push_back(a.description);
}
auto cells = to_cstr(cell_strings);
table_view("##analysis", headers, cols, cells.data(), static_cast<int>(analyses.size()));
data_table::TableInput ti;
auto backing = make_cells_analysis(analyses, ti);
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##analysis_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_analysis", {ti}, g_dt_analysis);
ImGui::EndChild();
}
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");
return;
}
const char* headers[] = {"Name", "Lang", "Domain", "Algebraic", "Description"};
constexpr int cols = 5;
std::vector<std::string> cell_strings;
cell_strings.reserve(types.size() * cols);
for (auto& t : types) {
cell_strings.push_back(t.name);
cell_strings.push_back(t.lang);
cell_strings.push_back(t.domain);
cell_strings.push_back(t.algebraic);
cell_strings.push_back(t.description);
}
auto cells = to_cstr(cell_strings);
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
data_table::TableInput ti;
auto backing = make_cells_types(types, ti);
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##types_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_types", {ti}, g_dt_types);
ImGui::EndChild();
}
// ---------------------------------------------------------------------------
@@ -686,27 +838,44 @@ void draw_monitor(RegistryData& data) {
if (cu.top_functions.empty()) {
ImGui::TextDisabled("No function calls recorded yet. Hook fires on next session.");
} else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_top_fn", 6, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("Function ID");
ImGui::TableSetupColumn("Calls");
ImGui::TableSetupColumn("7d");
ImGui::TableSetupColumn("Errors");
ImGui::TableSetupColumn("Error %");
ImGui::TableSetupColumn("Mean ms");
ImGui::TableHeadersRow();
for (const auto& r : cu.top_functions) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.function_id.c_str());
ImGui::TableSetColumnIndex(1); ImGui::Text("%d", r.calls_total);
ImGui::TableSetColumnIndex(2); ImGui::Text("%d", r.calls_7d);
ImGui::TableSetColumnIndex(3); ImGui::Text("%d", r.errors_total);
ImGui::TableSetColumnIndex(4); ImGui::Text("%.1f%%", r.error_rate * 100.0);
ImGui::TableSetColumnIndex(5); ImGui::Text("%.0f", r.mean_duration_ms);
}
ImGui::EndTable();
// Build TableInput for data_table::render
data_table::TableInput ti;
ti.name = "top_functions";
ti.headers = {"Function ID", "Calls", "7d", "Errors", "Error %", "Mean ms"};
ti.types = {
data_table::ColumnType::String,
data_table::ColumnType::Int,
data_table::ColumnType::Int,
data_table::ColumnType::Int,
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) {
backing.push_back(r.function_id);
std::snprintf(buf, sizeof(buf), "%d", r.calls_total);
backing.push_back(buf);
std::snprintf(buf, sizeof(buf), "%d", r.calls_7d);
backing.push_back(buf);
std::snprintf(buf, sizeof(buf), "%d", r.errors_total);
backing.push_back(buf);
std::snprintf(buf, sizeof(buf), "%.1f%%", r.error_rate * 100.0);
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();
}
@@ -714,25 +883,35 @@ void draw_monitor(RegistryData& data) {
if (cu.recent_violations.empty()) {
ImGui::TextDisabled("No antipattern violations detected.");
} else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_viol", 5, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("When");
ImGui::TableSetupColumn("Rule");
ImGui::TableSetupColumn("Severity");
ImGui::TableSetupColumn("Function");
ImGui::TableSetupColumn("Snippet");
ImGui::TableHeadersRow();
for (const auto& r : cu.recent_violations) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(format_ts(r.ts).c_str());
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(r.rule_id.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.severity.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.function_id.c_str());
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.command_snippet.c_str());
}
ImGui::EndTable();
data_table::TableInput ti;
ti.name = "violations";
ti.headers = {"When", "Rule", "Severity", "Function", "Snippet"};
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>(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) {
backing.push_back(format_ts(r.ts));
backing.push_back(r.rule_id);
backing.push_back(r.severity);
backing.push_back(r.function_id);
backing.push_back(r.command_snippet);
}
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();
}
@@ -793,25 +972,37 @@ void draw_monitor(RegistryData& data) {
if (cu.copies.empty()) {
ImGui::TextDisabled("No copied code detected. Run `fn doctor copied-code` or `call_monitor copied-code`.");
} else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_copies", 5, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("Kind");
ImGui::TableSetupColumn("Sim");
ImGui::TableSetupColumn("App File");
ImGui::TableSetupColumn("App Function");
ImGui::TableSetupColumn("Registry ID");
ImGui::TableHeadersRow();
for (const auto& r : cu.copies) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.kind.c_str());
ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f", r.similarity);
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.app_file.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.app_function.c_str());
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.registry_id.c_str());
}
ImGui::EndTable();
data_table::TableInput ti;
ti.name = "copies";
ti.headers = {"Kind", "Sim", "App File", "App Function", "Registry ID"};
ti.types = {
data_table::ColumnType::String,
data_table::ColumnType::Float,
data_table::ColumnType::String,
data_table::ColumnType::String,
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) {
backing.push_back(r.kind);
std::snprintf(buf, sizeof(buf), "%.2f", r.similarity);
backing.push_back(buf);
backing.push_back(r.app_file);
backing.push_back(r.app_function);
backing.push_back(r.registry_id);
}
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();
}
@@ -916,18 +1107,14 @@ void draw_projects_list(RegistryData& data) {
empty_state("( no data )", "No vaults in this project",
"Use the + Add button with kind = Vault (requires a project)");
} else {
const char* headers[] = {"Name", "Path", "Symlink", "Description"};
std::vector<std::string> cells;
cells.reserve(d.vaults.size() * 4);
for (auto& v : d.vaults) {
cells.push_back(v.name);
cells.push_back(v.path);
cells.push_back(v.symlink ? "yes" : "no");
cells.push_back(v.description);
}
auto cp = to_cstr(cells);
table_view("##vaults", headers, 4, cp.data(),
static_cast<int>(d.vaults.size()));
data_table::TableInput ti;
auto backing = make_cells_vaults(d.vaults, ti);
std::vector<const char*> ptrs;
cells_to_ptrs(backing, ptrs);
ti.cells = ptrs.data();
ImGui::BeginChild("##vaults_dt_wrap", ImVec2(-1, -1));
data_table::render("##dt_vaults", {ti}, g_dt_vaults);
ImGui::EndChild();
}
ImGui::EndTabItem();
}