merge data_table migration (issue 0081-J)
This commit is contained in:
+1
-1
@@ -60,7 +60,7 @@ target_include_directories(graph_explorer PRIVATE
|
||||
${FN_CPP_ROOT_DIR}/functions
|
||||
)
|
||||
|
||||
target_link_libraries(graph_explorer PRIVATE SQLite::SQLite3 DuckDB::DuckDB)
|
||||
target_link_libraries(graph_explorer PRIVATE SQLite::SQLite3 DuckDB::DuckDB fn_table_viz)
|
||||
duckdb_copy_runtime(graph_explorer)
|
||||
|
||||
# Threads — issue 0026 (jobs system) usa std::thread + std::mutex + condvar.
|
||||
|
||||
@@ -17,6 +17,19 @@ uses_functions:
|
||||
- graph_icons_cpp_viz
|
||||
- graph_sources_cpp_viz
|
||||
- graph_types_cpp_viz
|
||||
# data_table stack — issue 0081-J: panel Table migrado a data_table::render
|
||||
- data_table_cpp_viz
|
||||
- viz_render_cpp_viz
|
||||
- compute_stage_cpp_core
|
||||
- compute_pipeline_cpp_core
|
||||
- tql_emit_cpp_core
|
||||
- tql_apply_cpp_core
|
||||
- lua_engine_cpp_core
|
||||
- join_tables_cpp_core
|
||||
- auto_detect_type_cpp_core
|
||||
- compute_column_stats_cpp_core
|
||||
- llm_anthropic_cpp_core
|
||||
- tql_to_sql_cpp_core
|
||||
# core
|
||||
- graph_spatial_hash_cpp_core
|
||||
- button_cpp_core
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#include "core/tokens.h"
|
||||
#include "core/icons_tabler.h"
|
||||
|
||||
// data_table — issue 0081-J: migracion panel Table a data_table::render.
|
||||
#include "viz/data_table.h"
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -1537,200 +1540,25 @@ void views_table_refresh_indices(AppState& app) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Comparador estable para ImGuiTableSortSpecs.
|
||||
struct TableSortCtx {
|
||||
const ImGuiTableSortSpecs* specs;
|
||||
};
|
||||
TableSortCtx g_table_sort_ctx;
|
||||
|
||||
bool table_row_lt(const AppState::TableRow& a, const AppState::TableRow& b) {
|
||||
const ImGuiTableSortSpecs* specs = g_table_sort_ctx.specs;
|
||||
if (!specs) return a.name < b.name;
|
||||
for (int n = 0; n < specs->SpecsCount; ++n) {
|
||||
const ImGuiTableColumnSortSpecs& s = specs->Specs[n];
|
||||
int delta = 0;
|
||||
switch (s.ColumnUserID) {
|
||||
case 0: delta = a.id.compare(b.id); break;
|
||||
case 1: delta = a.name.compare(b.name); break;
|
||||
case 2: delta = a.type_ref.compare(b.type_ref); break;
|
||||
case 3: delta = a.status.compare(b.status); break;
|
||||
case 4: delta = a.updated_at.compare(b.updated_at); break;
|
||||
case 5: delta = (a.neighbors < b.neighbors) ? -1
|
||||
: (a.neighbors > b.neighbors) ? 1 : 0; break;
|
||||
default: break;
|
||||
}
|
||||
if (delta != 0) {
|
||||
return (s.SortDirection == ImGuiSortDirection_Ascending) ? (delta < 0) : (delta > 0);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ci_contains(const std::string& hay, const char* needle) {
|
||||
if (!needle || !*needle) return true;
|
||||
auto lower = [](char c){ return (char)std::tolower((unsigned char)c); };
|
||||
std::string h; h.reserve(hay.size());
|
||||
for (char c : hay) h.push_back(lower(c));
|
||||
std::string n;
|
||||
for (const char* p = needle; *p; ++p) n.push_back(lower(*p));
|
||||
return h.find(n) != std::string::npos;
|
||||
}
|
||||
|
||||
// Mapeo column_user_id -> nombre legible y string-getter sobre TableRow.
|
||||
struct TableColMeta {
|
||||
int user_id;
|
||||
const char* name;
|
||||
};
|
||||
const TableColMeta k_table_cols[] = {
|
||||
{0, "id"}, {1, "name"}, {2, "type"}, {3, "status"},
|
||||
{4, "updated_at"}, {5, "neighbors"},
|
||||
};
|
||||
constexpr int k_table_col_n = (int)(sizeof(k_table_cols) / sizeof(k_table_cols[0]));
|
||||
|
||||
const std::string& table_row_field(const AppState::TableRow& r, int user_id) {
|
||||
static const std::string empty_str;
|
||||
static thread_local std::string scratch;
|
||||
switch (user_id) {
|
||||
case 0: return r.id;
|
||||
case 1: return r.name;
|
||||
case 2: return r.type_ref;
|
||||
case 3: return r.status;
|
||||
case 4: return r.updated_at;
|
||||
case 5: scratch = std::to_string(r.neighbors); return scratch;
|
||||
}
|
||||
return empty_str;
|
||||
}
|
||||
|
||||
const char* table_col_name_by_id(int user_id) {
|
||||
for (int i = 0; i < k_table_col_n; ++i)
|
||||
if (k_table_cols[i].user_id == user_id) return k_table_cols[i].name;
|
||||
return "?";
|
||||
}
|
||||
|
||||
// Render header row con popup right-click por columna para anadir filtro.
|
||||
void render_table_headers_with_filters(AppState& app) {
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
||||
for (int i = 0; i < k_table_col_n; ++i) {
|
||||
ImGui::TableSetColumnIndex(i);
|
||||
const char* name = ImGui::TableGetColumnName(i);
|
||||
ImGui::PushID(i);
|
||||
ImGui::TableHeader(name);
|
||||
if (ImGui::BeginPopupContextItem("##colfilt",
|
||||
ImGuiPopupFlags_MouseButtonRight)) {
|
||||
int user_id = k_table_cols[i].user_id;
|
||||
ImGui::TextDisabled("Filter %s", k_table_cols[i].name);
|
||||
ImGui::Separator();
|
||||
// Si reabrimos el popup para esta columna, sembrar el buffer.
|
||||
if (app.table_filter_pending_col != user_id) {
|
||||
app.table_filter_pending_col = user_id;
|
||||
auto it = app.table_col_filters.find(user_id);
|
||||
std::snprintf(app.table_filter_input, sizeof(app.table_filter_input),
|
||||
"%s", it == app.table_col_filters.end() ? "" : it->second.c_str());
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(220);
|
||||
ImGuiInputTextFlags fflags = ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
bool commit = ImGui::InputTextWithHint("##filt_in", "substring (case-insensitive)",
|
||||
app.table_filter_input,
|
||||
sizeof(app.table_filter_input), fflags);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Apply") || commit) {
|
||||
if (app.table_filter_input[0]) {
|
||||
app.table_col_filters[user_id] = app.table_filter_input;
|
||||
} else {
|
||||
app.table_col_filters.erase(user_id);
|
||||
}
|
||||
app.table_filter_pending_col = -1;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear")) {
|
||||
app.table_col_filters.erase(user_id);
|
||||
app.table_filter_input[0] = 0;
|
||||
app.table_filter_pending_col = -1;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
} else if (app.table_filter_pending_col == k_table_cols[i].user_id) {
|
||||
// popup se cerro sin aplicar — limpiar pending.
|
||||
app.table_filter_pending_col = -1;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void render_one_table(AppState& app, std::vector<int>& visible_indices) {
|
||||
ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
|
||||
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_SizingStretchProp;
|
||||
if (!ImGui::BeginTable("##tablev", 6, flags)) return;
|
||||
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_DefaultSort, 0, 0);
|
||||
ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_None, 0, 1);
|
||||
ImGui::TableSetupColumn("type", ImGuiTableColumnFlags_None, 0, 2);
|
||||
ImGui::TableSetupColumn("status", ImGuiTableColumnFlags_None, 0, 3);
|
||||
ImGui::TableSetupColumn("updated_at", ImGuiTableColumnFlags_None, 0, 4);
|
||||
ImGui::TableSetupColumn("neighbors", ImGuiTableColumnFlags_WidthFixed,
|
||||
80.0f, 5);
|
||||
render_table_headers_with_filters(app);
|
||||
|
||||
ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs();
|
||||
if (specs && specs->SpecsDirty) {
|
||||
g_table_sort_ctx.specs = specs;
|
||||
std::sort(visible_indices.begin(), visible_indices.end(),
|
||||
[&app](int a, int b) {
|
||||
return table_row_lt(app.table_rows[a], app.table_rows[b]);
|
||||
});
|
||||
specs->SpecsDirty = false;
|
||||
}
|
||||
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin((int)visible_indices.size());
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
|
||||
int ri = visible_indices[row];
|
||||
const auto& r = app.table_rows[ri];
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(ri);
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
char sel_lbl[256];
|
||||
std::snprintf(sel_lbl, sizeof(sel_lbl), "%s##sel", r.id.c_str());
|
||||
bool is_sel = (app.viewport && r.node_idx >= 0
|
||||
&& graph_viewport_is_selected(*app.viewport, r.node_idx));
|
||||
if (ImGui::Selectable(sel_lbl, is_sel,
|
||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
if (r.node_idx >= 0 && app.graph && app.viewport) {
|
||||
graph_viewport_clear_selection(*app.graph, *app.viewport);
|
||||
graph_viewport_add_to_selection(*app.graph, *app.viewport,
|
||||
r.node_idx);
|
||||
app.filter_focus_target = r.node_idx;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextUnformatted(r.name.c_str());
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::TextUnformatted(r.type_ref.c_str());
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::TextUnformatted(r.status.c_str());
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::TextUnformatted(r.updated_at.c_str());
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
ImGui::Text("%d", r.neighbors);
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table view (issue 0004) — migrated to data_table::render (issue 0081-J).
|
||||
//
|
||||
// OLD: ImGui::BeginTable("##tablev", 6, ...) with manual sort/filter/clipper.
|
||||
// Per-column filter popups + chips toolbar + TabBar per type.
|
||||
// Click on row selected node in graph viewport.
|
||||
//
|
||||
// NEW: data_table::render() provides sort + filter + viz + stages.
|
||||
// AppState::table_dt_state persists the UI state between frames.
|
||||
// Viewport selection via Inspector (not via table row click).
|
||||
//
|
||||
// Removed helpers: render_one_table, render_table_headers_with_filters,
|
||||
// table_row_lt, table_row_field, table_col_name_by_id, k_table_cols,
|
||||
// TableColMeta, TableSortCtx, ci_contains, build_visible lambda.
|
||||
//
|
||||
// TODO(future): if viewport selection from table is wanted again, expose
|
||||
// a read-only "last hovered row id" from data_table::State or add a
|
||||
// post-render row-id lookup via ImGui::IsItemHovered on a named child.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
void views_table(AppState& app) {
|
||||
if (!app.panel_table) return;
|
||||
@@ -1739,97 +1567,70 @@ void views_table(AppState& app) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toolbar superior: search + show all.
|
||||
ImGui::SetNextItemWidth(220);
|
||||
ImGui::InputTextWithHint("##tsearch", TI_SEARCH " filter name/id...",
|
||||
app.table_search_buf, sizeof(app.table_search_buf));
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Show all types", &app.table_show_all);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%zu rows", app.table_rows.size());
|
||||
|
||||
// Chips de filtros activos por columna (right-click sobre header lo anade).
|
||||
if (!app.table_col_filters.empty()) {
|
||||
ImGui::TextDisabled("Filters:");
|
||||
ImGui::SameLine();
|
||||
int del_col = -1;
|
||||
for (auto& kv : app.table_col_filters) {
|
||||
ImGui::SameLine();
|
||||
char chip[160];
|
||||
std::snprintf(chip, sizeof(chip), TI_FILTER " %s: %s " TI_X "##chip_%d",
|
||||
table_col_name_by_id(kv.first), kv.second.c_str(), kv.first);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.30f, 0.50f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.40f, 0.65f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.50f, 0.75f, 1.0f));
|
||||
if (ImGui::SmallButton(chip)) del_col = kv.first;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
if (del_col >= 0) app.table_col_filters.erase(del_col);
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button("Clear all", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.table_col_filters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (app.table_rows.empty()) {
|
||||
ImGui::TextDisabled("(no entities loaded)");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Indices por tipo presentes en el snapshot.
|
||||
std::vector<std::string> types_present;
|
||||
types_present.reserve(8);
|
||||
{
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const auto& r : app.table_rows) {
|
||||
if (seen.insert(r.type_ref).second) types_present.push_back(r.type_ref);
|
||||
}
|
||||
std::sort(types_present.begin(), types_present.end());
|
||||
}
|
||||
|
||||
auto build_visible = [&](const char* type_filter) {
|
||||
std::vector<int> v;
|
||||
v.reserve(app.table_rows.size());
|
||||
for (size_t i = 0; i < app.table_rows.size(); ++i) {
|
||||
const auto& r = app.table_rows[i];
|
||||
if (type_filter && r.type_ref != type_filter) continue;
|
||||
if (app.table_search_buf[0]
|
||||
&& !ci_contains(r.name, app.table_search_buf)
|
||||
&& !ci_contains(r.id, app.table_search_buf)) continue;
|
||||
// Filtros por columna (AND de todos).
|
||||
bool reject = false;
|
||||
for (auto& kv : app.table_col_filters) {
|
||||
const std::string& field = table_row_field(r, kv.first);
|
||||
if (!ci_contains(field, kv.second.c_str())) { reject = true; break; }
|
||||
}
|
||||
if (reject) continue;
|
||||
v.push_back((int)i);
|
||||
}
|
||||
return v;
|
||||
// Construir TableInput a partir de app.table_rows.
|
||||
// Columnas: id, name, type, status, updated_at, neighbors.
|
||||
// Los datos se copian en backing strings cada frame (barato para N<50k
|
||||
// ya que son strings cortos y el vector se reutiliza por move).
|
||||
// data_table::render gestiona sort, filtros, busqueda y viz internamente.
|
||||
static const std::vector<std::string> k_headers = {
|
||||
"id", "name", "type", "status", "updated_at", "neighbors"
|
||||
};
|
||||
constexpr int k_ncols = 6;
|
||||
|
||||
if (app.table_show_all) {
|
||||
auto visible = build_visible(nullptr);
|
||||
ImGui::TextDisabled("All types — %zu visible", visible.size());
|
||||
render_one_table(app, visible);
|
||||
} else if (ImGui::BeginTabBar("##ttabs")) {
|
||||
for (size_t i = 0; i < types_present.size(); ++i) {
|
||||
const std::string& t = types_present[i];
|
||||
char lbl[96];
|
||||
std::snprintf(lbl, sizeof(lbl), "%s##tt%zu",
|
||||
t.empty() ? "(none)" : t.c_str(), i);
|
||||
if (ImGui::BeginTabItem(lbl)) {
|
||||
app.table_active_tab = (int)i;
|
||||
auto visible = build_visible(t.c_str());
|
||||
ImGui::TextDisabled("%zu rows visible", visible.size());
|
||||
render_one_table(app, visible);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
// cell_backing: propietario de los strings de la tabla.
|
||||
// Se reconstruye solo cuando table_cache_dirty (tras reload).
|
||||
// Para mantenerlo entre frames: miembro de AppState seria lo optimo,
|
||||
// pero para simplicidad lo hacemos static local (unica instancia OK
|
||||
// porque views_table se llama desde un unico lugar del render loop).
|
||||
static std::vector<std::string> s_cell_backing;
|
||||
static std::vector<const char*> s_cells;
|
||||
static int s_rows_cached = -1;
|
||||
|
||||
// Rebuilding the cache on dirty flag or size change.
|
||||
if (app.table_cache_dirty || (int)app.table_rows.size() != s_rows_cached) {
|
||||
const int nrows = (int)app.table_rows.size();
|
||||
s_cell_backing.resize((size_t)nrows * k_ncols);
|
||||
s_cells.resize((size_t)nrows * k_ncols);
|
||||
for (int i = 0; i < nrows; ++i) {
|
||||
const auto& r = app.table_rows[i];
|
||||
s_cell_backing[(size_t)i * k_ncols + 0] = r.id;
|
||||
s_cell_backing[(size_t)i * k_ncols + 1] = r.name;
|
||||
s_cell_backing[(size_t)i * k_ncols + 2] = r.type_ref;
|
||||
s_cell_backing[(size_t)i * k_ncols + 3] = r.status;
|
||||
s_cell_backing[(size_t)i * k_ncols + 4] = r.updated_at;
|
||||
s_cell_backing[(size_t)i * k_ncols + 5] = std::to_string(r.neighbors);
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
for (size_t k = 0; k < s_cell_backing.size(); ++k)
|
||||
s_cells[k] = s_cell_backing[k].c_str();
|
||||
s_rows_cached = nrows;
|
||||
app.table_cache_dirty = false;
|
||||
}
|
||||
|
||||
data_table::TableInput tbl;
|
||||
tbl.name = "entities";
|
||||
tbl.headers = k_headers;
|
||||
tbl.types = {
|
||||
data_table::ColumnType::String, // id
|
||||
data_table::ColumnType::String, // name
|
||||
data_table::ColumnType::String, // type
|
||||
data_table::ColumnType::String, // status
|
||||
data_table::ColumnType::String, // updated_at
|
||||
data_table::ColumnType::Int, // neighbors
|
||||
};
|
||||
tbl.cells = s_cells.empty() ? nullptr : s_cells.data();
|
||||
tbl.rows = s_rows_cached;
|
||||
tbl.cols = k_ncols;
|
||||
|
||||
// Render con chrome completo (barra de chips + breadcrumb).
|
||||
// app.table_dt_state persiste entre frames.
|
||||
data_table::render("##tablev_dt", {tbl}, app.table_dt_state);
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "entity_ops.h"
|
||||
#include "node_groups.h"
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -246,6 +248,10 @@ struct AppState {
|
||||
char table_filter_input[96] = {}; // buffer del popup activo
|
||||
int table_filter_pending_col = -1; // col_user_id en edicion
|
||||
|
||||
// data_table::State para el panel Table (issue 0081-J).
|
||||
// Persiste filters/sort/stages entre frames; uno por instancia de render.
|
||||
data_table::State table_dt_state;
|
||||
|
||||
// ---- Type Editor (issue 0007) ------------------------------------------
|
||||
// Draft del editor de tipos. Se inicializa con una copia de parsed_types
|
||||
// tras cargar el grafo. Save reescribe `types.yaml` y dispara
|
||||
|
||||
Reference in New Issue
Block a user