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:
@@ -93,6 +93,43 @@ struct ProjectRow {
|
|||||||
int vaults_count = 0;
|
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 {
|
struct ProjectDetail {
|
||||||
std::string id; // "" si no hay seleccion; "orphans" para huerfanas
|
std::string id; // "" si no hay seleccion; "orphans" para huerfanas
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|||||||
+124
@@ -308,6 +308,130 @@ bool load_project_detail_http(const std::string& api_url,
|
|||||||
return true;
|
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
|
// Mutation endpoints
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+17
@@ -18,6 +18,23 @@ bool load_project_detail_http(const std::string& api_url,
|
|||||||
const std::string& id,
|
const std::string& id,
|
||||||
ProjectDetail& out);
|
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).
|
// Operaciones de mutacion (thread-safe porque http_post ya lo es).
|
||||||
// Devuelven el body de respuesta en `out_body`. true si HTTP status 2xx.
|
// 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);
|
bool http_post_reindex(const std::string& api_url, std::string& out_body);
|
||||||
|
|||||||
@@ -26,8 +26,11 @@
|
|||||||
#include "core/toast.h"
|
#include "core/toast.h"
|
||||||
#include "core/process_runner.h"
|
#include "core/process_runner.h"
|
||||||
#include "core/tree_view.h"
|
#include "core/tree_view.h"
|
||||||
|
#include "core/selectable_text.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -60,8 +63,22 @@ static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
|||||||
return out;
|
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() {
|
static void trigger_reload() {
|
||||||
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
|
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"};
|
const char* labels[] = {"Pure", "Impure"};
|
||||||
float values[] = {static_cast<float>(data.stats.pure_functions),
|
float values[] = {static_cast<float>(data.stats.pure_functions),
|
||||||
static_cast<float>(data.stats.impure_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();
|
chart_panel_end();
|
||||||
}
|
}
|
||||||
ImGui::TableSetColumnIndex(3);
|
ImGui::TableSetColumnIndex(3);
|
||||||
@@ -180,7 +197,7 @@ void draw_charts(RegistryData& data, float height) {
|
|||||||
auto labels = to_cstr(data.kind_labels);
|
auto labels = to_cstr(data.kind_labels);
|
||||||
if (!labels.empty())
|
if (!labels.empty())
|
||||||
pie_chart("##kind", labels.data(), data.kind_values.data(),
|
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();
|
chart_panel_end();
|
||||||
}
|
}
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
@@ -590,6 +607,263 @@ static void draw_actions_bar() {
|
|||||||
last_reindex_state = now;
|
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
|
// Main draw
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -613,26 +887,37 @@ void draw_dashboard(RegistryData& data) {
|
|||||||
draw_actions_bar();
|
draw_actions_bar();
|
||||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||||
|
|
||||||
draw_kpi_row(data);
|
// Navegacion top-level: cada tab ocupa toda la zona de contenido. El
|
||||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
// 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
|
// Altura del bloque de charts proporcional al espacio disponible:
|
||||||
// espacio restante despues de KPIs/header/tabs (clamped a [200, 360] px
|
// ~32% del espacio restante despues de KPIs (clamped a [200, 360]).
|
||||||
// para que ni se aplaste ni domine en pantallas grandes).
|
const float remaining_h = ImGui::GetContentRegionAvail().y;
|
||||||
const float remaining_h = ImGui::GetContentRegionAvail().y;
|
float chart_h = remaining_h * 0.40f;
|
||||||
float chart_h = remaining_h * 0.32f;
|
if (chart_h < 200.0f) chart_h = 200.0f;
|
||||||
if (chart_h < 200.0f) chart_h = 200.0f;
|
if (chart_h > 360.0f) chart_h = 360.0f;
|
||||||
if (chart_h > 360.0f) chart_h = 360.0f;
|
draw_charts(data, chart_h);
|
||||||
draw_charts(data, chart_h);
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
||||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
||||||
|
|
||||||
if (ImGui::BeginTabBar("##tables")) {
|
// Tabla de funciones recientes para que el Dashboard tenga algo
|
||||||
if (ImGui::BeginTabItem("Projects")) {
|
// accionable abajo sin tener que cambiar de pestana.
|
||||||
draw_projects_list(data);
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||||
|
ImGui::TextUnformatted("Recent Functions");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
draw_recent_functions(data.recent_funcs);
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
if (ImGui::BeginTabItem("Recent Functions")) {
|
if (ImGui::BeginTabItem("Explorer")) {
|
||||||
draw_recent_functions(data.recent_funcs);
|
draw_functions_explorer();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
if (ImGui::BeginTabItem("Projects")) {
|
||||||
|
draw_projects_list(data);
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
if (ImGui::BeginTabItem("Apps")) {
|
if (ImGui::BeginTabItem("Apps")) {
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ void draw_apps_list(const std::vector<AppRow>& apps);
|
|||||||
void draw_analysis_list(const std::vector<AnalysisRow>& analyses);
|
void draw_analysis_list(const std::vector<AnalysisRow>& analyses);
|
||||||
void draw_types_list(const std::vector<TypeRow>& types);
|
void draw_types_list(const std::vector<TypeRow>& types);
|
||||||
void draw_projects_list(RegistryData& data);
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user