feat(dashboard): functions explorer + top-level tabs + pie chart fit

- Pie charts (Purity, Kind) ahora reciben plot_h explicito para que respeten
  el panel contenedor y no desborden cuando la ventana se reduce.
- Nueva pestana "Explorer": split layout con lista filtrable a la izquierda
  (search por nombre/descripcion + combos lang/domain) y panel de detalle
  a la derecha con tabs Code (read-only multiline), Documentation (selectable
  text), Tests (con dropdown si la funcion tiene varios tests, muestra lang +
  file_path + codigo) y Metadata (id, version, file_path, returns,
  uses_functions, uses_types, params_schema, example, notes).
- Reorganizacion de navegacion: TabBar movido arriba, justo debajo de la
  toolbar. Tabs: Dashboard | Explorer | Projects | Apps | Analysis | Types.
  Cada tab ocupa toda la zona de contenido. Dashboard incluye KPIs + charts
  + tabla de Recent Functions.
- Cache del Explorer (lista de funciones) invalidado en trigger_reload.
- Nuevas funciones HTTP: load_function_detail_http, load_all_functions_http,
  load_unit_tests_http (todas usan /api/databases/registry/query con escape
  defensivo de comilla simple en IDs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 23:13:02 +02:00
parent 641723bdf1
commit ddf8e76ad6
5 changed files with 485 additions and 18 deletions
+37
View File
@@ -93,6 +93,43 @@ struct ProjectRow {
int vaults_count = 0;
};
// Test unitario asociado a una funcion (1:N — una funcion puede tener varios).
struct UnitTestRow {
std::string id;
std::string function_id;
std::string name;
std::string lang;
std::string file_path;
std::string code;
std::string created_at;
};
// Detalle completo de una funcion (codigo + documentacion).
// Cargado on-demand cuando el usuario selecciona una funcion en Explorer.
struct FunctionDetail {
std::string id;
std::string name;
std::string lang;
std::string domain;
std::string kind;
std::string purity;
std::string version;
std::string signature;
std::string description;
std::string code;
std::string documentation;
std::string notes;
std::string example;
std::string params_schema;
std::string uses_functions;
std::string uses_types;
std::string returns;
std::string error_type;
std::string file_path;
std::string created_at;
bool tested = false;
};
struct ProjectDetail {
std::string id; // "" si no hay seleccion; "orphans" para huerfanas
std::string name;
+124
View File
@@ -308,6 +308,130 @@ bool load_project_detail_http(const std::string& api_url,
return true;
}
// ---------------------------------------------------------------------------
// Function detail endpoints (Explorer)
// ---------------------------------------------------------------------------
bool load_function_detail_http(const std::string& api_url,
const std::string& id,
FunctionDetail& out) {
std::string host; int port;
if (!parse_url(api_url, host, port)) return false;
HttpClient cli(host, port);
// /api/databases/<db>/query solo acepta { "sql": ... } sin args
// parametrizados (ver handleQuery en sqlite_api). Escapamos comilla
// simple para que un id con apostrofe no rompa la query — los IDs del
// registry son [a-z0-9_]+ asi que el escape es defensivo.
std::string escaped;
escaped.reserve(id.size());
for (char c : id) {
if (c == '\'') escaped += "''";
else escaped.push_back(c);
}
std::string sql =
"SELECT id, name, lang, domain, kind, purity, version, signature, "
"description, code, documentation, notes, example, params_schema, "
"uses_functions, uses_types, returns, error_type, file_path, "
"created_at, tested FROM functions WHERE id = '" + escaped + "'";
auto j = api_query(cli, sql.c_str());
if (j.is_null() || !j.contains("rows") || j["rows"].empty()) return false;
auto& row = j["rows"][0];
out = FunctionDetail{};
out.id = extract_str(row, 0);
out.name = extract_str(row, 1);
out.lang = extract_str(row, 2);
out.domain = extract_str(row, 3);
out.kind = extract_str(row, 4);
out.purity = extract_str(row, 5);
out.version = extract_str(row, 6);
out.signature = extract_str(row, 7);
out.description = extract_str(row, 8);
out.code = extract_str(row, 9);
out.documentation = extract_str(row, 10);
out.notes = extract_str(row, 11);
out.example = extract_str(row, 12);
out.params_schema = extract_str(row, 13);
out.uses_functions= extract_str(row, 14);
out.uses_types = extract_str(row, 15);
out.returns = extract_str(row, 16);
out.error_type = extract_str(row, 17);
out.file_path = extract_str(row, 18);
out.created_at = extract_str(row, 19);
out.tested = extract_row_int(row, 20) != 0;
return true;
}
bool load_all_functions_http(const std::string& api_url,
std::vector<FunctionRow>& out) {
std::string host; int port;
if (!parse_url(api_url, host, port)) return false;
HttpClient cli(host, port);
auto j = api_query(cli,
"SELECT id, name, lang, domain, kind, purity, description, "
"created_at, tested FROM functions ORDER BY name");
if (j.is_null() || !j.contains("rows")) return false;
out.clear();
out.reserve(j["rows"].size());
for (auto& row : j["rows"]) {
FunctionRow r;
r.id = extract_str(row, 0);
r.name = extract_str(row, 1);
r.lang = extract_str(row, 2);
r.domain = extract_str(row, 3);
r.kind = extract_str(row, 4);
r.purity = extract_str(row, 5);
r.description = extract_str(row, 6);
r.created_at = extract_str(row, 7);
r.tested = extract_row_int(row, 8) != 0;
out.push_back(std::move(r));
}
return true;
}
bool load_unit_tests_http(const std::string& api_url,
const std::string& function_id,
std::vector<UnitTestRow>& out) {
std::string host; int port;
if (!parse_url(api_url, host, port)) return false;
HttpClient cli(host, port);
// Mismo escape defensivo que en load_function_detail_http — los IDs son
// [a-z0-9_]+ pero por consistencia escapamos comilla simple.
std::string escaped;
escaped.reserve(function_id.size());
for (char c : function_id) {
if (c == '\'') escaped += "''";
else escaped.push_back(c);
}
std::string sql =
"SELECT id, function_id, name, lang, file_path, code, created_at "
"FROM unit_tests WHERE function_id = '" + escaped + "' ORDER BY name";
auto j = api_query(cli, sql.c_str());
if (j.is_null() || !j.contains("rows")) return false;
out.clear();
for (auto& row : j["rows"]) {
UnitTestRow r;
r.id = extract_str(row, 0);
r.function_id = extract_str(row, 1);
r.name = extract_str(row, 2);
r.lang = extract_str(row, 3);
r.file_path = extract_str(row, 4);
r.code = extract_str(row, 5);
r.created_at = extract_str(row, 6);
out.push_back(std::move(r));
}
return true;
}
// ---------------------------------------------------------------------------
// Mutation endpoints
// ---------------------------------------------------------------------------
+17
View File
@@ -18,6 +18,23 @@ bool load_project_detail_http(const std::string& api_url,
const std::string& id,
ProjectDetail& out);
// Load detalle completo de una funcion (codigo + documentacion + metadata).
// Usado por la pestana Explorer.
bool load_function_detail_http(const std::string& api_url,
const std::string& id,
FunctionDetail& out);
// Load lista plana de funciones para el Explorer (id, name, lang, domain,
// kind, purity, description, tested) — todas las funciones, no solo recientes.
bool load_all_functions_http(const std::string& api_url,
std::vector<FunctionRow>& out);
// Load tests unitarios asociados a una funcion (tabla unit_tests con FK
// function_id). Devuelve la lista vacia si la funcion no tiene tests.
bool load_unit_tests_http(const std::string& api_url,
const std::string& function_id,
std::vector<UnitTestRow>& out);
// Operaciones de mutacion (thread-safe porque http_post ya lo es).
// Devuelven el body de respuesta en `out_body`. true si HTTP status 2xx.
bool http_post_reindex(const std::string& api_url, std::string& out_body);
+303 -18
View File
@@ -26,8 +26,11 @@
#include "core/toast.h"
#include "core/process_runner.h"
#include "core/tree_view.h"
#include "core/selectable_text.h"
#include <cstdio>
#include <cstring>
#include <cctype>
#include <string>
#include <vector>
@@ -60,8 +63,22 @@ static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
return out;
}
// Cache del Explorer: forward declarado aqui para que trigger_reload pueda
// invalidarlo (la lista completa de funciones se cachea hasta que el usuario
// dispara Reload o Reindex).
static std::vector<FunctionRow> g_explorer_funcs;
static bool g_explorer_loaded = false;
static std::string g_explorer_selected_id;
static FunctionDetail g_explorer_detail;
static std::vector<UnitTestRow> g_explorer_tests;
static int g_explorer_test_idx = 0;
static char g_explorer_filter[128] = {};
static int g_explorer_lang_idx = 0;
static int g_explorer_domain_idx = 0;
static void trigger_reload() {
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
g_explorer_loaded = false;
}
// ---------------------------------------------------------------------------
@@ -170,7 +187,7 @@ void draw_charts(RegistryData& data, float height) {
const char* labels[] = {"Pure", "Impure"};
float values[] = {static_cast<float>(data.stats.pure_functions),
static_cast<float>(data.stats.impure_functions)};
pie_chart("##purity", labels, values, 2);
pie_chart("##purity", labels, values, 2, 0.0f, plot_h);
chart_panel_end();
}
ImGui::TableSetColumnIndex(3);
@@ -180,7 +197,7 @@ void draw_charts(RegistryData& data, float height) {
auto labels = to_cstr(data.kind_labels);
if (!labels.empty())
pie_chart("##kind", labels.data(), data.kind_values.data(),
static_cast<int>(labels.size()));
static_cast<int>(labels.size()), 0.0f, plot_h);
chart_panel_end();
}
ImGui::EndTable();
@@ -590,6 +607,263 @@ static void draw_actions_bar() {
last_reindex_state = now;
}
// ---------------------------------------------------------------------------
// Functions Explorer
// ---------------------------------------------------------------------------
// Pestana con dos columnas: a la izquierda lista filtrable de funciones del
// registry; a la derecha codigo + documentacion + metadata de la funcion
// seleccionada. La lista se carga en el primer render (lazy) y se cachea —
// el boton "Reload" del toolbar fuerza recarga al disparar trigger_reload
// (que pone g_explorer_loaded = false).
static const char* kExplorerLangs[] = {"all", "go", "py", "ts", "sh", "cpp"};
static const char* kExplorerDomains[] = {"all", "core", "infra", "finance",
"datascience", "cybersecurity",
"shell", "tui", "pipelines",
"browser", "viz", "gfx", "notebook"};
static bool str_contains_ci(const std::string& haystack, const char* needle) {
if (!needle || !*needle) return true;
std::string h = haystack;
std::string n = needle;
for (auto& c : h) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (auto& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return h.find(n) != std::string::npos;
}
static void explorer_select(const std::string& id) {
g_explorer_selected_id = id;
g_explorer_detail = FunctionDetail{};
g_explorer_tests.clear();
g_explorer_test_idx = 0;
if (!g_api_url.empty() && !id.empty()) {
load_function_detail_http(g_api_url, id, g_explorer_detail);
load_unit_tests_http(g_api_url, id, g_explorer_tests);
}
}
static void explorer_reload_list() {
g_explorer_funcs.clear();
g_explorer_loaded = false;
if (g_api_url.empty()) return;
if (load_all_functions_http(g_api_url, g_explorer_funcs)) {
g_explorer_loaded = true;
}
}
void draw_functions_explorer() {
if (!g_explorer_loaded) {
explorer_reload_list();
}
if (g_explorer_funcs.empty()) {
empty_state("( no data )", "No functions to explore",
"Run 'fn index' or check the API connection");
return;
}
const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
| ImGuiTableFlags_SizingStretchProp;
if (!ImGui::BeginTable("##explorer_layout", 2, flags)) return;
ImGui::TableSetupColumn("list", ImGuiTableColumnFlags_WidthStretch, 1.0f);
ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.4f);
ImGui::TableNextRow();
// --- Left: filter + list ---
ImGui::TableSetColumnIndex(0);
ImGui::BeginChild("##explorer_left", ImVec2(0, 0), ImGuiChildFlags_Borders);
fn_ui::text_input("Search", g_explorer_filter, sizeof(g_explorer_filter),
"name or description");
ImGui::PushItemWidth(120.0f);
ImGui::Combo("##lang", &g_explorer_lang_idx, kExplorerLangs,
IM_ARRAYSIZE(kExplorerLangs));
ImGui::SameLine();
ImGui::Combo("##domain", &g_explorer_domain_idx, kExplorerDomains,
IM_ARRAYSIZE(kExplorerDomains));
ImGui::PopItemWidth();
ImGui::Spacing();
// Conteo de resultados visibles + lista
int visible = 0;
ImGui::BeginChild("##explorer_list", ImVec2(0, 0));
for (const auto& f : g_explorer_funcs) {
if (g_explorer_lang_idx > 0
&& f.lang != kExplorerLangs[g_explorer_lang_idx]) continue;
if (g_explorer_domain_idx > 0
&& f.domain != kExplorerDomains[g_explorer_domain_idx]) continue;
if (g_explorer_filter[0] != '\0'
&& !str_contains_ci(f.name, g_explorer_filter)
&& !str_contains_ci(f.description, g_explorer_filter)) continue;
visible++;
bool sel = (g_explorer_selected_id == f.id);
char label[256];
std::snprintf(label, sizeof(label), "%s [%s/%s]",
f.name.c_str(), f.lang.c_str(), f.domain.c_str());
if (ImGui::Selectable(label, sel, ImGuiSelectableFlags_AllowOverlap)) {
explorer_select(f.id);
}
}
ImGui::EndChild();
(void)visible;
ImGui::EndChild();
// --- Right: detail ---
ImGui::TableSetColumnIndex(1);
ImGui::BeginChild("##explorer_right", ImVec2(0, 0), ImGuiChildFlags_Borders);
if (g_explorer_selected_id.empty()) {
empty_state("\xe2\x86\x90", "Select a function",
"Click a function on the left to see its code and documentation");
} else {
const auto& d = g_explorer_detail;
// Header: nombre + signature
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
ImGui::TextUnformatted(d.name.empty()
? g_explorer_selected_id.c_str() : d.name.c_str());
ImGui::PopStyleColor();
// Badges meta (lang/domain/kind/purity/tested)
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("%s · %s · %s · %s%s",
d.lang.c_str(), d.domain.c_str(),
d.kind.c_str(), d.purity.c_str(),
d.tested ? " · tested" : "");
ImGui::PopStyleColor();
if (!d.description.empty()) {
ImGui::Spacing();
fn_ui::selectable_text_wrapped(d.description.c_str());
}
if (!d.signature.empty()) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::TextUnformatted("Signature:");
ImGui::PopStyleColor();
fn_ui::selectable_text_wrapped(d.signature.c_str());
}
ImGui::Separator();
// Tabs Code / Documentation / Notes / Example
if (ImGui::BeginTabBar("##fn_detail_tabs")) {
if (ImGui::BeginTabItem("Code")) {
if (d.code.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted("(no code stored)");
ImGui::PopStyleColor();
} else {
// InputTextMultiline read-only — permite seleccion y copia,
// y maneja scroll horizontal/vertical para codigo largo.
ImVec2 sz = ImGui::GetContentRegionAvail();
ImGui::InputTextMultiline("##code",
const_cast<char*>(d.code.c_str()),
d.code.size() + 1, sz,
ImGuiInputTextFlags_ReadOnly);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Documentation")) {
if (d.documentation.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted("(no documentation)");
ImGui::PopStyleColor();
} else {
ImGui::BeginChild("##doc_scroll", ImVec2(0, 0));
fn_ui::selectable_text_wrapped(d.documentation.c_str());
ImGui::EndChild();
}
ImGui::EndTabItem();
}
// Tests: caja con dropdown si hay varios + visor del codigo del
// test seleccionado. Nombre del tab incluye el conteo para que se
// vea de un vistazo si la funcion tiene tests o no.
char tests_tab_label[32];
std::snprintf(tests_tab_label, sizeof(tests_tab_label),
"Tests (%zu)", g_explorer_tests.size());
if (ImGui::BeginTabItem(tests_tab_label)) {
if (g_explorer_tests.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted("(no unit tests for this function)");
ImGui::PopStyleColor();
} else {
// Combo para elegir entre los varios tests (la mayoria
// de funciones solo tienen 1, pero algunas tienen varios
// ficheros de tests).
if (g_explorer_test_idx >= (int)g_explorer_tests.size())
g_explorer_test_idx = 0;
std::vector<std::string> names;
names.reserve(g_explorer_tests.size());
for (auto& t : g_explorer_tests) names.push_back(t.name);
std::vector<const char*> name_cstr;
for (auto& n : names) name_cstr.push_back(n.c_str());
ImGui::PushItemWidth(-FLT_MIN);
ImGui::Combo("##test_select", &g_explorer_test_idx,
name_cstr.data(),
static_cast<int>(name_cstr.size()));
ImGui::PopItemWidth();
const auto& t = g_explorer_tests[g_explorer_test_idx];
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("%s · %s", t.lang.c_str(),
t.file_path.empty() ? "(no path)" : t.file_path.c_str());
ImGui::PopStyleColor();
ImGui::Separator();
if (t.code.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted("(no code stored for this test)");
ImGui::PopStyleColor();
} else {
ImVec2 sz = ImGui::GetContentRegionAvail();
ImGui::InputTextMultiline("##test_code",
const_cast<char*>(t.code.c_str()),
t.code.size() + 1, sz,
ImGuiInputTextFlags_ReadOnly);
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Metadata")) {
ImGui::BeginChild("##meta_scroll", ImVec2(0, 0));
auto kv = [](const char* k, const std::string& v) {
if (v.empty()) return;
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::TextUnformatted(k);
ImGui::PopStyleColor();
fn_ui::selectable_text_wrapped(v.c_str());
ImGui::Spacing();
};
kv("ID:", d.id);
kv("Version:", d.version);
kv("File path:", d.file_path);
kv("Created:", d.created_at);
kv("Returns:", d.returns);
kv("Error type:", d.error_type);
kv("Uses functions:", d.uses_functions);
kv("Uses types:", d.uses_types);
kv("Params schema:", d.params_schema);
kv("Example:", d.example);
kv("Notes:", d.notes);
ImGui::EndChild();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::EndChild();
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// Main draw
// ---------------------------------------------------------------------------
@@ -613,26 +887,37 @@ void draw_dashboard(RegistryData& data) {
draw_actions_bar();
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
draw_kpi_row(data);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
// Navegacion top-level: cada tab ocupa toda la zona de contenido. El
// primero ("Dashboard") incluye los KPIs + charts + tabla de recientes;
// los demas son vistas dedicadas a su entidad.
if (ImGui::BeginTabBar("##main_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) {
if (ImGui::BeginTabItem("Dashboard")) {
draw_kpi_row(data);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
// Altura del bloque de charts proporcional al espacio disponible: ~28% del
// espacio restante despues de KPIs/header/tabs (clamped a [200, 360] px
// para que ni se aplaste ni domine en pantallas grandes).
const float remaining_h = ImGui::GetContentRegionAvail().y;
float chart_h = remaining_h * 0.32f;
if (chart_h < 200.0f) chart_h = 200.0f;
if (chart_h > 360.0f) chart_h = 360.0f;
draw_charts(data, chart_h);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
// Altura del bloque de charts proporcional al espacio disponible:
// ~32% del espacio restante despues de KPIs (clamped a [200, 360]).
const float remaining_h = ImGui::GetContentRegionAvail().y;
float chart_h = remaining_h * 0.40f;
if (chart_h < 200.0f) chart_h = 200.0f;
if (chart_h > 360.0f) chart_h = 360.0f;
draw_charts(data, chart_h);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
if (ImGui::BeginTabBar("##tables")) {
if (ImGui::BeginTabItem("Projects")) {
draw_projects_list(data);
// Tabla de funciones recientes para que el Dashboard tenga algo
// accionable abajo sin tener que cambiar de pestana.
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::TextUnformatted("Recent Functions");
ImGui::PopStyleColor();
draw_recent_functions(data.recent_funcs);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Recent Functions")) {
draw_recent_functions(data.recent_funcs);
if (ImGui::BeginTabItem("Explorer")) {
draw_functions_explorer();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Projects")) {
draw_projects_list(data);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Apps")) {
+4
View File
@@ -18,3 +18,7 @@ void draw_apps_list(const std::vector<AppRow>& apps);
void draw_analysis_list(const std::vector<AnalysisRow>& analyses);
void draw_types_list(const std::vector<TypeRow>& types);
void draw_projects_list(RegistryData& data);
// Explorer: lista navegable de funciones + visor de codigo y documentacion.
// Carga la lista completa al primer render via /api/databases/registry/query.
void draw_functions_explorer();