3ad26e4f6b
- app.md - appicon.ico - autoextract_panel.cpp - recipes_panel.cpp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
358 lines
13 KiB
C++
358 lines
13 KiB
C++
// recipes_panel — Listado de recetas (YAML) en
|
|
// projects/navegator/profiles/default/recipes/*.yaml.
|
|
//
|
|
// Acciones por fila:
|
|
// Run -> subprocess Python con cdp_extract_recipe (record_run=True).
|
|
// Edit -> abre InputTextMultiline con el YAML; "Save" reescribe.
|
|
// Delete -> rm + refresh list.
|
|
// Open in data_factory -> noop (placeholder; mostraria link/cmd).
|
|
|
|
#include "imgui.h"
|
|
#include "core/icons_tabler.h"
|
|
#include "core/tokens.h"
|
|
#include "data_table/data_table.h"
|
|
#include "core/data_table_types.h"
|
|
|
|
#include "py_subprocess.h"
|
|
#include "session_state.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <windows.h>
|
|
#else
|
|
# include <dirent.h>
|
|
# include <sys/stat.h>
|
|
#endif
|
|
|
|
namespace navegator {
|
|
|
|
namespace {
|
|
|
|
struct RecipeRow {
|
|
std::string name;
|
|
std::string url_pattern;
|
|
std::string yaml_path;
|
|
std::string last_run_status;
|
|
std::string last_run_at;
|
|
int rows_last_run = 0;
|
|
};
|
|
|
|
struct RecipesState {
|
|
std::mutex mu;
|
|
std::vector<RecipeRow> rows;
|
|
std::string status;
|
|
std::string last_error;
|
|
std::atomic<bool> busy{false};
|
|
int editing_idx = -1;
|
|
std::string edit_buf;
|
|
char edit_textarea[16384] = {0};
|
|
};
|
|
RecipesState g_rs;
|
|
|
|
std::string recipes_dir() {
|
|
std::string root = py_resolve_registry_root();
|
|
if (root.empty()) return "";
|
|
#ifdef _WIN32
|
|
return root + "\\projects\\navegator\\profiles\\default\\recipes";
|
|
#else
|
|
return root + "/projects/navegator/profiles/default/recipes";
|
|
#endif
|
|
}
|
|
|
|
std::string slurp(const std::string& path) {
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f) return "";
|
|
std::ostringstream ss; ss << f.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
// Mini-parser YAML especifico: solo extrae name + url_pattern.
|
|
void parse_recipe_min(const std::string& body, RecipeRow& r) {
|
|
std::stringstream ss(body);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
auto strip = [](std::string s){
|
|
size_t a = s.find_first_not_of(" \t");
|
|
size_t b = s.find_last_not_of(" \t\r");
|
|
return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1);
|
|
};
|
|
if (line.rfind("name:", 0) == 0) {
|
|
r.name = strip(line.substr(5));
|
|
if (!r.name.empty() && (r.name.front()=='"' || r.name.front()=='\'')) {
|
|
r.name = r.name.substr(1, r.name.size() - 2);
|
|
}
|
|
} else if (line.rfind("url_pattern:", 0) == 0) {
|
|
r.url_pattern = strip(line.substr(12));
|
|
if (!r.url_pattern.empty() && (r.url_pattern.front()=='"' || r.url_pattern.front()=='\'')) {
|
|
r.url_pattern = r.url_pattern.substr(1, r.url_pattern.size() - 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> list_yaml_files(const std::string& dir) {
|
|
std::vector<std::string> out;
|
|
#ifdef _WIN32
|
|
std::string pattern = dir + "\\*.yaml";
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h = FindFirstFileA(pattern.c_str(), &fd);
|
|
if (h == INVALID_HANDLE_VALUE) return out;
|
|
do {
|
|
out.push_back(dir + "\\" + fd.cFileName);
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
#else
|
|
DIR* d = opendir(dir.c_str());
|
|
if (!d) return out;
|
|
while (auto e = readdir(d)) {
|
|
std::string n = e->d_name;
|
|
if (n.size() > 5 && n.substr(n.size() - 5) == ".yaml") {
|
|
out.push_back(dir + "/" + n);
|
|
}
|
|
}
|
|
closedir(d);
|
|
#endif
|
|
std::sort(out.begin(), out.end());
|
|
return out;
|
|
}
|
|
|
|
void refresh_list() {
|
|
std::string dir = recipes_dir();
|
|
if (dir.empty()) {
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.last_error = "FN_REGISTRY_ROOT not set";
|
|
return;
|
|
}
|
|
auto files = list_yaml_files(dir);
|
|
std::vector<RecipeRow> rows;
|
|
for (const auto& f : files) {
|
|
RecipeRow r; r.yaml_path = f;
|
|
std::string body = slurp(f);
|
|
parse_recipe_min(body, r);
|
|
if (r.name.empty()) {
|
|
// fallback al basename sin ext
|
|
size_t p1 = f.find_last_of("/\\");
|
|
std::string base = (p1 == std::string::npos) ? f : f.substr(p1 + 1);
|
|
if (base.size() > 5) base = base.substr(0, base.size() - 5);
|
|
r.name = base;
|
|
}
|
|
rows.push_back(std::move(r));
|
|
}
|
|
// Anota last_run_* desde data_factory.runs (subprocess sqlite3 best-effort).
|
|
// Lo dejamos como TODO — la primera version queda con campos vacios.
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.rows = std::move(rows);
|
|
g_rs.last_error.clear();
|
|
g_rs.status = "Listed " + std::to_string(g_rs.rows.size()) + " recipes";
|
|
}
|
|
|
|
void run_recipe_async(const std::string& yaml_path) {
|
|
if (g_rs.busy.exchange(true)) return;
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.status = "Running " + yaml_path;
|
|
}
|
|
std::thread([yaml_path]() {
|
|
const char* code = R"PY(
|
|
import sys, os, json, traceback
|
|
root = os.environ.get('FN_REGISTRY_ROOT', '')
|
|
if not root:
|
|
print(json.dumps({"error":"FN_REGISTRY_ROOT not set"})); sys.exit(2)
|
|
for sub in ('pipelines','core','infra'):
|
|
sys.path.insert(0, os.path.join(root, 'python', 'functions', sub))
|
|
try:
|
|
from cdp_extract_recipe import cdp_extract_recipe
|
|
path = sys.argv[1]
|
|
res = cdp_extract_recipe(path, debug_port=9222, record_run=True)
|
|
print(json.dumps(res if isinstance(res, dict) else {"result": res}))
|
|
except Exception as e:
|
|
print(json.dumps({"error": str(e), "trace": traceback.format_exc()})); sys.exit(1)
|
|
)PY";
|
|
std::vector<std::string> argv;
|
|
argv.push_back(py_resolve_interpreter());
|
|
argv.push_back("-c");
|
|
argv.push_back(code);
|
|
argv.push_back(yaml_path);
|
|
PyResult r = py_run(argv, 120000);
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
if (r.exit_code != 0) {
|
|
g_rs.last_error = r.error.empty() ? "python exited non-zero" : r.error;
|
|
g_rs.status = "Run failed";
|
|
} else {
|
|
g_rs.status = "Run OK: " + r.stdout_data.substr(0, 200);
|
|
}
|
|
}
|
|
g_rs.busy.store(false);
|
|
refresh_list();
|
|
}).detach();
|
|
}
|
|
|
|
void delete_recipe(const std::string& path) {
|
|
std::remove(path.c_str());
|
|
refresh_list();
|
|
}
|
|
|
|
} // anon
|
|
|
|
void render_recipes_panel(bool* p_open) {
|
|
if (!ImGui::Begin(TI_LIST_DETAILS " Recipes", p_open)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (ImGui::Button(TI_REFRESH " Refresh")) refresh_list();
|
|
ImGui::SameLine();
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
if (!g_rs.status.empty()) ImGui::Text("%s", g_rs.status.c_str());
|
|
if (!g_rs.last_error.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
|
|
ImGui::TextWrapped("Error: %s", g_rs.last_error.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
|
|
std::vector<RecipeRow> rows_copy;
|
|
int editing_idx = -1;
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
rows_copy = g_rs.rows;
|
|
editing_idx = g_rs.editing_idx;
|
|
}
|
|
|
|
if (rows_copy.empty()) {
|
|
ImGui::TextDisabled("No recipes in projects/navegator/profiles/default/recipes/.");
|
|
ImGui::TextDisabled("Use AutoExtract panel to create one.");
|
|
} else {
|
|
// Tabla de recetas — migrado a data_table::render (issue 0107g, Patron B).
|
|
// Las acciones Run/Edit/Delete/Open se mapean a columnas Button con action_id.
|
|
// ev.row indexa rows_copy directamente para recuperar yaml_path.
|
|
static data_table::State g_st_recipes;
|
|
static std::vector<std::string> g_back_recipes;
|
|
static std::vector<const char*> g_ptrs_recipes;
|
|
|
|
g_back_recipes.clear();
|
|
for (const auto& r : rows_copy) {
|
|
g_back_recipes.push_back(r.name);
|
|
g_back_recipes.push_back(r.url_pattern);
|
|
g_back_recipes.push_back(r.last_run_status.empty() ? "-" : r.last_run_status);
|
|
g_back_recipes.push_back(r.last_run_at.empty() ? "-" : r.last_run_at);
|
|
g_back_recipes.push_back(std::to_string(r.rows_last_run));
|
|
g_back_recipes.push_back("Run"); // col 5: accion run
|
|
g_back_recipes.push_back("Edit"); // col 6: accion edit
|
|
g_back_recipes.push_back("Delete"); // col 7: accion delete
|
|
g_back_recipes.push_back("Open"); // col 8: accion open_df
|
|
}
|
|
g_ptrs_recipes.clear();
|
|
for (const auto& s : g_back_recipes) g_ptrs_recipes.push_back(s.c_str());
|
|
|
|
data_table::TableInput tbl;
|
|
tbl.name = "recipes_tbl";
|
|
tbl.headers = {"name", "url_pattern", "last_status", "last_at", "rows",
|
|
"run", "edit", "delete", "open"};
|
|
tbl.types = {
|
|
data_table::ColumnType::String, data_table::ColumnType::String,
|
|
data_table::ColumnType::String, data_table::ColumnType::String,
|
|
data_table::ColumnType::Int,
|
|
data_table::ColumnType::String, data_table::ColumnType::String,
|
|
data_table::ColumnType::String, data_table::ColumnType::String,
|
|
};
|
|
tbl.cells = g_ptrs_recipes.data();
|
|
tbl.rows = (int)rows_copy.size();
|
|
tbl.cols = 9;
|
|
|
|
tbl.column_specs.resize(tbl.cols);
|
|
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
|
// last_status → CategoricalChip
|
|
tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip;
|
|
tbl.column_specs[2].chips = {
|
|
{"ok", "#22c55e"}, {"success", "#22c55e"},
|
|
{"error", "#ef4444"}, {"failed", "#ef4444"},
|
|
{"-", "#a3a3a3"},
|
|
};
|
|
// Botones de accion
|
|
tbl.column_specs[5].renderer = data_table::CellRenderer::Button;
|
|
tbl.column_specs[5].button_action = "run_recipe";
|
|
tbl.column_specs[5].button_label = "Run";
|
|
tbl.column_specs[5].button_color_hex = "#3b82f6";
|
|
tbl.column_specs[6].renderer = data_table::CellRenderer::Button;
|
|
tbl.column_specs[6].button_action = "edit_recipe";
|
|
tbl.column_specs[6].button_label = "Edit";
|
|
tbl.column_specs[7].renderer = data_table::CellRenderer::Button;
|
|
tbl.column_specs[7].button_action = "delete_recipe";
|
|
tbl.column_specs[7].button_label = "Delete";
|
|
tbl.column_specs[7].button_color_hex = "#ef4444";
|
|
tbl.column_specs[8].renderer = data_table::CellRenderer::Button;
|
|
tbl.column_specs[8].button_action = "open_df";
|
|
tbl.column_specs[8].button_label = "Open";
|
|
|
|
std::vector<data_table::TableEvent> rec_events;
|
|
ImGui::BeginChild("##recipes_tbl_host", ImVec2(-1, 300));
|
|
data_table::render("##recipes_dt", {tbl}, g_st_recipes, &rec_events);
|
|
ImGui::EndChild();
|
|
|
|
for (const auto& ev : rec_events) {
|
|
if (ev.kind != data_table::TableEventKind::ButtonClick) continue;
|
|
if (ev.row < 0 || ev.row >= (int)rows_copy.size()) continue;
|
|
const RecipeRow& r = rows_copy[ev.row];
|
|
if (ev.action_id == "run_recipe") {
|
|
run_recipe_async(r.yaml_path);
|
|
} else if (ev.action_id == "edit_recipe") {
|
|
std::string body = slurp(r.yaml_path);
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.editing_idx = ev.row;
|
|
g_rs.edit_buf = body;
|
|
std::snprintf(g_rs.edit_textarea, sizeof(g_rs.edit_textarea),
|
|
"%s", body.c_str());
|
|
} else if (ev.action_id == "delete_recipe") {
|
|
delete_recipe(r.yaml_path);
|
|
} else if (ev.action_id == "open_df") {
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.status = "open in data_factory: " + r.name + " (not wired)";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (editing_idx >= 0 && editing_idx < (int)rows_copy.size()) {
|
|
ImGui::Separator();
|
|
ImGui::Text("Editing: %s", rows_copy[editing_idx].yaml_path.c_str());
|
|
ImGui::InputTextMultiline("##rec_edit", g_rs.edit_textarea,
|
|
sizeof(g_rs.edit_textarea),
|
|
ImVec2(-1, 220));
|
|
if (ImGui::Button(TI_DEVICE_FLOPPY " Save")) {
|
|
std::ofstream f(rows_copy[editing_idx].yaml_path, std::ios::binary);
|
|
if (f) {
|
|
f << g_rs.edit_textarea;
|
|
f.close();
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.status = "Saved " + rows_copy[editing_idx].yaml_path;
|
|
g_rs.editing_idx = -1;
|
|
}
|
|
refresh_list();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
std::lock_guard<std::mutex> lk(g_rs.mu);
|
|
g_rs.editing_idx = -1;
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
} // namespace navegator
|