Files
registry_dashboard/views.cpp
T
egutierrez 570103d83e feat(ui): migrar a tokens + primitivos del design system
Dashboard ahora usa:
- fn_tokens::apply_dark_theme() al primer frame (colors + spacing
  + radius + rounding consistentes con @fn_library / Mantine).
- page_header_begin/end para el header (en vez de Text + Separator
  + Button manual). Subtítulo con conteos de entidades.
- empty_state en las 4 tablas cuando están vacías — mensaje amable
  con acción sugerida en lugar de tabla vacía silenciosa.

Nuevas deps de compilación (.cpp fuentes añadidas al CMakeLists):
tokens.cpp, badge.cpp, empty_state.cpp, page_header.cpp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:52:08 +02:00

239 lines
8.3 KiB
C++

#include "views.h"
#include "imgui.h"
#include "implot.h"
#include "viz/kpi_card.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/table_view.h"
#include "viz/sparkline.h"
#include "core/dashboard_panel.h"
#include "core/dashboard_grid.h"
#include "core/fps_overlay.h"
#include "core/fullscreen_window.h"
#include "core/tokens.h"
#include "core/page_header.h"
#include "core/empty_state.h"
#include "core/badge.h"
#include <cstdio>
#include <vector>
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
std::vector<const char*> out;
out.reserve(v.size());
for (auto& s : v) out.push_back(s.c_str());
return out;
}
void draw_kpi_row(const RegistryStats& stats) {
float tested_pct = stats.total_functions > 0
? 100.0f * stats.tested_functions / stats.total_functions : 0.0f;
float pure_pct = stats.total_functions > 0
? 100.0f * stats.pure_functions / stats.total_functions : 0.0f;
dashboard_grid_begin(8, 8.0f);
kpi_card("Functions", static_cast<float>(stats.total_functions), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Types", static_cast<float>(stats.total_types), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Apps", static_cast<float>(stats.total_apps), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Analysis", static_cast<float>(stats.total_analysis), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Unit Tests", static_cast<float>(stats.total_unit_tests), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Proposals", static_cast<float>(stats.total_proposals), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Tested", tested_pct, 0, nullptr, 0, "%.0f%%");
dashboard_grid_next();
kpi_card("Pure", pure_pct, 0, nullptr, 0, "%.0f%%");
dashboard_grid_end();
}
void draw_charts(RegistryData& data, float height) {
dashboard_grid_begin(4, 8.0f);
if (dashboard_panel_begin("By Language", 0, height)) {
auto labels = to_cstr(data.lang_labels);
if (!labels.empty())
bar_chart("##lang", labels.data(), data.lang_values.data(), static_cast<int>(labels.size()));
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("By Domain", 0, height)) {
auto labels = to_cstr(data.domain_labels);
if (!labels.empty())
bar_chart("##domain", labels.data(), data.domain_values.data(), static_cast<int>(labels.size()));
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Purity", 0, 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);
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Kind", 0, 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()));
}
dashboard_panel_end();
dashboard_grid_end();
}
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
if (funcs.empty()) {
empty_state("( no data )", "No functions yet",
"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()));
}
void draw_apps_list(const std::vector<AppRow>& apps) {
if (apps.empty()) {
empty_state("( no data )", "No apps registered",
"Clone apps with 'fn app clone <id>' or run 'fn sync'");
return;
}
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Description"};
constexpr int cols = 5;
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);
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) {
if (analyses.empty()) {
empty_state("( no data )", "No analysis yet",
"Create one with 'fn run init_jupyter_analysis <name>'");
return;
}
const char* headers[] = {"Name", "Domain", "Description"};
constexpr int cols = 3;
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.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()));
}
void draw_types_list(const std::vector<TypeRow>& types) {
if (types.empty()) {
empty_state("( no data )", "No types yet",
"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()));
}
void draw_dashboard(RegistryData& data) {
// Aplicar tema una sola vez por vida de la app.
static bool theme_applied = false;
if (!theme_applied) {
fn_tokens::apply_dark_theme();
theme_applied = true;
}
fps_overlay();
fullscreen_window_begin("##dashboard");
// Subtitle con conteos — contexto rápido para el usuario
char subtitle[128];
std::snprintf(subtitle, sizeof(subtitle),
"%d functions · %d types · %d apps · %d analyses",
data.stats.total_functions, data.stats.total_types,
data.stats.total_apps, data.stats.total_analysis);
// Header con acción Reload a la derecha
page_header_begin("fn_registry Dashboard", subtitle);
ImGui::SameLine(ImGui::GetWindowWidth() - 120.0f);
if (ImGui::Button("Reload")) {
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
}
page_header_end();
// KPIs
draw_kpi_row(data.stats);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
// Charts — 35% de la altura restante
float remaining = ImGui::GetContentRegionAvail().y;
float chart_h = remaining * 0.35f;
if (chart_h < 150.0f) chart_h = 150.0f;
draw_charts(data, chart_h);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
// Tables
if (ImGui::BeginTabBar("##tables")) {
if (ImGui::BeginTabItem("Recent Functions")) {
draw_recent_functions(data.recent_funcs);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Apps")) {
draw_apps_list(data.apps);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Analysis")) {
draw_analysis_list(data.analyses);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Types")) {
draw_types_list(data.types);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
fullscreen_window_end();
}