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:
@@ -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")) {
|
||||
|
||||
Reference in New Issue
Block a user