feat: Settings submenu (Settings.../About...), git column, projects tab
- main.cpp: registrar info About via fn_ui::about_window_set_info - views.cpp: nueva columna "Git" en la tabla Apps (remote/local/-) - data.h/cpp + data_http.cpp: AppRow gana repo_url + dir_path - views.cpp: actions bar (Reindex / + Add / Reload / inbox) y modal Add - views.cpp: tab Projects con tree + detalle anidado - data_http.cpp: load_projects_http, load_project_detail_http, http_post_* - http_client.cpp: SO_RCVTIMEO en Windows como DWORD ms (timeout 5 ms bug) - CMakeLists: limpieza de srcs duplicados con fn_framework - app.md: notas operativas y estado actual Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
#include "views.h"
|
||||
#include "data_http.h"
|
||||
#include <filesystem>
|
||||
#include "imgui.h"
|
||||
#include "implot.h"
|
||||
|
||||
@@ -15,10 +17,42 @@
|
||||
#include "core/page_header.h"
|
||||
#include "core/empty_state.h"
|
||||
#include "core/badge.h"
|
||||
#include "core/button.h"
|
||||
#include "core/icon_button.h"
|
||||
#include "core/toolbar.h"
|
||||
#include "core/modal_dialog.h"
|
||||
#include "core/text_input.h"
|
||||
#include "core/select.h"
|
||||
#include "core/toast.h"
|
||||
#include "core/process_runner.h"
|
||||
#include "core/tree_view.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string g_api_url;
|
||||
static fn_ui::ProcessRunner g_reindex_runner;
|
||||
static fn_ui::ProcessRunner g_add_runner;
|
||||
|
||||
// Add modal state
|
||||
enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 };
|
||||
static bool g_show_add = false;
|
||||
static int g_add_kind_idx = static_cast<int>(AddKind::App);
|
||||
static int g_add_project_idx = -1; // -1 = (none / sin project)
|
||||
static int g_add_lang_idx = 0;
|
||||
static int g_add_domain_idx = 0;
|
||||
static char g_add_name[128] = {};
|
||||
static char g_add_desc[256] = {};
|
||||
static char g_add_packages[256] = {};
|
||||
static char g_add_vault_path[512] = {};
|
||||
|
||||
void views_set_api_url(const std::string& url) { g_api_url = url; }
|
||||
|
||||
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
||||
std::vector<const char*> out;
|
||||
out.reserve(v.size());
|
||||
@@ -26,15 +60,20 @@ static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static void trigger_reload() {
|
||||
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KPI row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
|
||||
// ImGui::BeginTable da celdas con ancho constrained, algo que BeginGroup
|
||||
// (dashboard_grid) no hace — necesario para que el BeginChild dentro de
|
||||
// kpi_card ocupe exactamente la celda y no desborde.
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
||||
| ImGuiTableFlags_NoPadOuterX;
|
||||
|
||||
@@ -59,10 +98,10 @@ void draw_kpi_row(const RegistryStats& stats) {
|
||||
}
|
||||
}
|
||||
|
||||
// Chart panel con tamano FIJO (no AutoResizeY) para evitar el feedback loop
|
||||
// con ImPlot que provocaba deslizamiento lateral de las barras y scrollbar
|
||||
// intermitente. Usa los mismos tokens que dashboard_panel para consistencia
|
||||
// visual, pero con tamano determinista.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Charts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool chart_panel_begin(const char* title, const ImVec2& size) {
|
||||
using namespace fn_tokens;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
||||
@@ -88,13 +127,8 @@ static void chart_panel_end() {
|
||||
}
|
||||
|
||||
void draw_charts(RegistryData& data, float height) {
|
||||
// ImGui::BeginTable para reparto equitativo de ancho en 4 columnas y que
|
||||
// cada chart_panel tenga ancho constrained via GetContentRegionAvail.
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
||||
| ImGuiTableFlags_NoPadOuterX;
|
||||
|
||||
// Altura util dentro del panel (height total - title row - separator - padding).
|
||||
// El plot recibe exactamente esto, asi que no hay redimensionado recursivo.
|
||||
const float plot_h = height - 48.0f;
|
||||
|
||||
if (ImGui::BeginTable("##chart_grid", 4, flags)) {
|
||||
@@ -110,7 +144,6 @@ void draw_charts(RegistryData& data, float height) {
|
||||
static_cast<int>(labels.size()), 0.67f, plot_h);
|
||||
chart_panel_end();
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
{
|
||||
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
||||
@@ -121,7 +154,6 @@ void draw_charts(RegistryData& data, float height) {
|
||||
static_cast<int>(labels.size()), 0.67f, plot_h);
|
||||
chart_panel_end();
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
{
|
||||
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
||||
@@ -132,7 +164,6 @@ void draw_charts(RegistryData& data, float height) {
|
||||
pie_chart("##purity", labels, values, 2);
|
||||
chart_panel_end();
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
{
|
||||
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
||||
@@ -143,11 +174,14 @@ void draw_charts(RegistryData& data, float height) {
|
||||
static_cast<int>(labels.size()));
|
||||
chart_panel_end();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
||||
if (funcs.empty()) {
|
||||
empty_state("( no data )", "No functions yet",
|
||||
@@ -174,11 +208,11 @@ void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
||||
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'");
|
||||
"Use the + Add button above or run 'fn sync'");
|
||||
return;
|
||||
}
|
||||
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Description"};
|
||||
constexpr int cols = 5;
|
||||
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) {
|
||||
@@ -186,6 +220,24 @@ void draw_apps_list(const std::vector<AppRow>& apps) {
|
||||
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);
|
||||
@@ -195,15 +247,16 @@ void draw_apps_list(const std::vector<AppRow>& apps) {
|
||||
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>'");
|
||||
"Use the + Add button above with kind = Analysis");
|
||||
return;
|
||||
}
|
||||
const char* headers[] = {"Name", "Domain", "Description"};
|
||||
constexpr int cols = 3;
|
||||
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);
|
||||
}
|
||||
@@ -232,45 +285,337 @@ void draw_types_list(const std::vector<TypeRow>& types) {
|
||||
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;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Projects view
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string g_selected_project_id = "";
|
||||
static ProjectDetail g_project_detail;
|
||||
|
||||
static void refresh_project_detail() {
|
||||
g_project_detail = ProjectDetail{};
|
||||
if (g_selected_project_id.empty() || g_api_url.empty()) return;
|
||||
load_project_detail_http(g_api_url, g_selected_project_id, g_project_detail);
|
||||
}
|
||||
|
||||
void draw_projects_list(RegistryData& data) {
|
||||
if (data.projects.empty() && data.orphan_apps == 0
|
||||
&& data.orphan_analyses == 0 && data.orphan_vaults == 0) {
|
||||
empty_state("( no data )", "No projects yet",
|
||||
"Create a project under projects/{name}/ with project.md and reindex");
|
||||
return;
|
||||
}
|
||||
|
||||
// Dos columnas: izquierda arbol, derecha detalle.
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
|
||||
| ImGuiTableFlags_SizingStretchProp;
|
||||
if (!ImGui::BeginTable("##proj_layout", 2, flags)) return;
|
||||
|
||||
ImGui::TableSetupColumn("tree", ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||
ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.5f);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// --- Left column: tree ---
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::BeginChild("##proj_tree", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||
for (const auto& p : data.projects) {
|
||||
bool sel = (g_selected_project_id == p.id);
|
||||
char label[256];
|
||||
std::snprintf(label, sizeof(label), "%s [%d/%d/%d]",
|
||||
p.name.c_str(), p.apps_count, p.analyses_count, p.vaults_count);
|
||||
fn_ui::tree_leaf(p.id.c_str(), label, sel);
|
||||
if (fn_ui::tree_node_clicked()) {
|
||||
g_selected_project_id = p.id;
|
||||
refresh_project_detail();
|
||||
}
|
||||
}
|
||||
// Orphans
|
||||
if (data.orphan_apps + data.orphan_analyses + data.orphan_vaults > 0) {
|
||||
bool sel = (g_selected_project_id == "orphans");
|
||||
char label[128];
|
||||
std::snprintf(label, sizeof(label), "(orphans) [%d/%d/%d]",
|
||||
data.orphan_apps, data.orphan_analyses, data.orphan_vaults);
|
||||
fn_ui::tree_leaf("orphans", label, sel);
|
||||
if (fn_ui::tree_node_clicked()) {
|
||||
g_selected_project_id = "orphans";
|
||||
refresh_project_detail();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// --- Right column: detail ---
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::BeginChild("##proj_detail", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||
if (g_selected_project_id.empty()) {
|
||||
empty_state("\xe2\x86\x90", "Select a project",
|
||||
"Click a project on the left to see its apps, analyses and vaults");
|
||||
} else {
|
||||
const auto& d = g_project_detail;
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
|
||||
if (!d.name.empty())
|
||||
ImGui::TextUnformatted(d.name.c_str());
|
||||
else
|
||||
ImGui::TextUnformatted(g_selected_project_id.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
if (!d.description.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
ImGui::TextWrapped("%s", d.description.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::BeginTabBar("##proj_tabs")) {
|
||||
char tab_apps[64]; std::snprintf(tab_apps, sizeof(tab_apps), "Apps (%zu)", d.apps.size());
|
||||
char tab_analyses[64]; std::snprintf(tab_analyses, sizeof(tab_analyses), "Analysis (%zu)", d.analyses.size());
|
||||
char tab_vaults[64]; std::snprintf(tab_vaults, sizeof(tab_vaults), "Vaults (%zu)", d.vaults.size());
|
||||
|
||||
if (ImGui::BeginTabItem(tab_apps)) {
|
||||
draw_apps_list(d.apps);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem(tab_analyses)) {
|
||||
draw_analysis_list(d.analyses);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem(tab_vaults)) {
|
||||
if (d.vaults.empty()) {
|
||||
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()));
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions bar (Reindex + Add button) + Add modal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const char* kLangs[] = {"go", "py", "ts", "sh", "cpp"};
|
||||
static const char* kDomains[] = {"core", "infra", "finance", "datascience",
|
||||
"cybersecurity", "shell", "tui", "pipelines",
|
||||
"browser", "viz", "gfx", "notebook"};
|
||||
|
||||
static void submit_add() {
|
||||
if (g_api_url.empty()) {
|
||||
fn_ui::toast_push(fn_ui::ToastKind::Error, "API URL not set");
|
||||
return;
|
||||
}
|
||||
std::string name = g_add_name;
|
||||
std::string desc = g_add_desc;
|
||||
std::string project;
|
||||
if (g_add_project_idx >= 0) {
|
||||
// g_add_project_idx indexa la lista live de projects (resolved en el modal)
|
||||
// guardada en g_project_ids_cache — ver draw_add_modal
|
||||
}
|
||||
|
||||
// Resolvemos project en draw_add_modal y lo pasamos via g_add_project_resolved
|
||||
extern std::string g_add_project_resolved;
|
||||
project = g_add_project_resolved;
|
||||
|
||||
AddKind kind = static_cast<AddKind>(g_add_kind_idx);
|
||||
std::string url = g_api_url;
|
||||
|
||||
fn_ui::runner_trigger(g_add_runner,
|
||||
[kind, url, name, desc, project,
|
||||
lang_idx = g_add_lang_idx, domain_idx = g_add_domain_idx,
|
||||
packages = std::string(g_add_packages),
|
||||
vault_path = std::string(g_add_vault_path)
|
||||
](std::string& out) -> bool {
|
||||
std::string body;
|
||||
bool ok = false;
|
||||
switch (kind) {
|
||||
case AddKind::App:
|
||||
ok = http_post_add_app(url, name, kLangs[lang_idx],
|
||||
kDomains[domain_idx], project, desc, body);
|
||||
break;
|
||||
case AddKind::Analysis:
|
||||
ok = http_post_add_analysis(url, name, project, packages, desc, body);
|
||||
break;
|
||||
case AddKind::Vault:
|
||||
ok = http_post_add_vault(url, name, project, vault_path, desc, body);
|
||||
break;
|
||||
}
|
||||
out = body;
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
|
||||
std::string g_add_project_resolved = "";
|
||||
|
||||
static void draw_add_modal(RegistryData& data) {
|
||||
if (!fn_ui::modal_dialog_begin("Add...", &g_show_add, ImVec2(460, 0))) {
|
||||
fn_ui::modal_dialog_end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Kind selector
|
||||
const char* kinds[] = {"App", "Analysis", "Vault"};
|
||||
fn_ui::select("Kind", &g_add_kind_idx, kinds, 3);
|
||||
|
||||
// Project selector (del registro vivo)
|
||||
std::vector<std::string> proj_labels;
|
||||
std::vector<std::string> proj_ids;
|
||||
for (auto& p : data.projects) {
|
||||
proj_labels.push_back(p.name);
|
||||
proj_ids.push_back(p.id);
|
||||
}
|
||||
std::vector<const char*> proj_cstr;
|
||||
for (auto& s : proj_labels) proj_cstr.push_back(s.c_str());
|
||||
|
||||
AddKind kind = static_cast<AddKind>(g_add_kind_idx);
|
||||
fn_ui::select("Project", &g_add_project_idx, proj_cstr.data(),
|
||||
static_cast<int>(proj_cstr.size()),
|
||||
kind != AddKind::Vault /* vault obliga a project */);
|
||||
g_add_project_resolved = (g_add_project_idx >= 0 && g_add_project_idx < (int)proj_ids.size())
|
||||
? proj_ids[g_add_project_idx] : "";
|
||||
|
||||
fn_ui::text_input("Name", g_add_name, sizeof(g_add_name),
|
||||
"snake_case, a-z0-9_");
|
||||
fn_ui::text_input("Description", g_add_desc, sizeof(g_add_desc));
|
||||
|
||||
// Campos especificos segun kind
|
||||
if (kind == AddKind::App) {
|
||||
fn_ui::select("Lang", &g_add_lang_idx, kLangs, 5);
|
||||
fn_ui::select("Domain", &g_add_domain_idx, kDomains, 12);
|
||||
} else if (kind == AddKind::Analysis) {
|
||||
fn_ui::text_input("Packages (CSV)", g_add_packages, sizeof(g_add_packages),
|
||||
"polars,scikit-learn,torch");
|
||||
} else if (kind == AddKind::Vault) {
|
||||
fn_ui::text_input("Path (abs, opcional)", g_add_vault_path, sizeof(g_add_vault_path),
|
||||
"/home/lucas/vaults/my_data");
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||
|
||||
// Status del runner en curso
|
||||
fn_ui::runner_status(g_add_runner, "Creating...");
|
||||
|
||||
// Detectar transicion running->done para cerrar y notificar
|
||||
static fn_ui::RunnerState last_state = fn_ui::RunnerState::Idle;
|
||||
fn_ui::RunnerState now = g_add_runner.state();
|
||||
if (last_state == fn_ui::RunnerState::Running
|
||||
&& (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) {
|
||||
const bool ok = (now == fn_ui::RunnerState::Success);
|
||||
fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error,
|
||||
ok ? "Created OK — reloading" : g_add_runner.message().c_str());
|
||||
if (ok) {
|
||||
g_show_add = false;
|
||||
g_add_name[0] = '\0';
|
||||
g_add_desc[0] = '\0';
|
||||
g_add_packages[0] = '\0';
|
||||
g_add_vault_path[0] = '\0';
|
||||
trigger_reload();
|
||||
}
|
||||
}
|
||||
last_state = now;
|
||||
|
||||
ImGui::Separator();
|
||||
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) g_show_add = false;
|
||||
ImGui::SameLine();
|
||||
const bool disabled = g_add_runner.is_busy() || g_add_name[0] == '\0';
|
||||
if (disabled) ImGui::BeginDisabled();
|
||||
if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) {
|
||||
submit_add();
|
||||
}
|
||||
if (disabled) ImGui::EndDisabled();
|
||||
|
||||
fn_ui::modal_dialog_end();
|
||||
}
|
||||
|
||||
static void draw_actions_bar() {
|
||||
if (g_api_url.empty()) return;
|
||||
|
||||
fn_ui::toolbar_begin();
|
||||
if (fn_ui::button("Reindex", fn_ui::ButtonVariant::Primary)
|
||||
&& !g_reindex_runner.is_busy()) {
|
||||
const std::string url = g_api_url;
|
||||
fn_ui::runner_trigger(g_reindex_runner, [url](std::string& out) -> bool {
|
||||
return http_post_reindex(url, out);
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button("+ Add", fn_ui::ButtonVariant::Secondary)) {
|
||||
g_show_add = true;
|
||||
g_add_runner.reset();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button("Reload", fn_ui::ButtonVariant::Subtle)) {
|
||||
trigger_reload();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
fn_ui::toast_inbox_button("##inbox");
|
||||
fn_ui::toolbar_end();
|
||||
|
||||
// Status del reindex debajo del toolbar
|
||||
static fn_ui::RunnerState last_reindex_state = fn_ui::RunnerState::Idle;
|
||||
fn_ui::RunnerState now = g_reindex_runner.state();
|
||||
if (now != fn_ui::RunnerState::Idle) {
|
||||
fn_ui::runner_status(g_reindex_runner, "Reindexing...");
|
||||
}
|
||||
if (last_reindex_state == fn_ui::RunnerState::Running
|
||||
&& (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) {
|
||||
const bool ok = (now == fn_ui::RunnerState::Success);
|
||||
fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error,
|
||||
g_reindex_runner.message().c_str());
|
||||
if (ok) trigger_reload();
|
||||
}
|
||||
last_reindex_state = now;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main draw
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void draw_dashboard(RegistryData& data) {
|
||||
// Tema aplicado por fn::run_app() (app_base.h, ThemeMode::FnDark default).
|
||||
|
||||
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",
|
||||
"%d functions · %d types · %d apps · %d analyses · %zu projects",
|
||||
data.stats.total_functions, data.stats.total_types,
|
||||
data.stats.total_apps, data.stats.total_analysis);
|
||||
data.stats.total_apps, data.stats.total_analysis,
|
||||
data.projects.size());
|
||||
|
||||
// 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_actions_bar();
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||
|
||||
draw_kpi_row(data.stats);
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
||||
|
||||
// Charts — altura FIJA en pixeles (no depende del resize de la ventana).
|
||||
// Antes usabamos remaining*0.35, pero eso recalculaba todo el layout al
|
||||
// redimensionar, provocando vibracion visible en los plots.
|
||||
constexpr float chart_h = 260.0f;
|
||||
draw_charts(data, chart_h);
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
||||
|
||||
// Tables
|
||||
if (ImGui::BeginTabBar("##tables")) {
|
||||
if (ImGui::BeginTabItem("Projects")) {
|
||||
draw_projects_list(data);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Recent Functions")) {
|
||||
draw_recent_functions(data.recent_funcs);
|
||||
ImGui::EndTabItem();
|
||||
@@ -290,5 +635,10 @@ void draw_dashboard(RegistryData& data) {
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
draw_add_modal(data);
|
||||
|
||||
fullscreen_window_end();
|
||||
|
||||
// Toasts encima de todo
|
||||
fn_ui::toast_render();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user