chore: auto-commit (23 archivos)
- CMakeLists.txt - app.md - appicon.ico - main.cpp - perf_tests.cpp - perf_tests.h - qa_panel.cpp - qa_panel.h - qa_state.cpp - qa_state.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
add_imgui_app(tables_qa
|
||||||
|
main.cpp
|
||||||
|
qa_state.cpp
|
||||||
|
qa_panel.cpp
|
||||||
|
test_suite.cpp
|
||||||
|
perf_tests.cpp
|
||||||
|
tab_basic.cpp
|
||||||
|
tab_renderers.cpp
|
||||||
|
tab_buttons.cpp
|
||||||
|
tab_color_rules.cpp
|
||||||
|
tab_dots.cpp
|
||||||
|
tab_joins.cpp
|
||||||
|
tab_tql.cpp
|
||||||
|
tab_drill.cpp
|
||||||
|
tab_events.cpp
|
||||||
|
tab_compat.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(tables_qa PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
# fn_module_data_table: provides data_table::render(), viz_render, TQL engine, Lua, LLM.
|
||||||
|
if(TARGET fn_module_data_table)
|
||||||
|
target_link_libraries(tables_qa PRIVATE fn_module_data_table)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
set_target_properties(tables_qa PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: tables_qa
|
||||||
|
lang: cpp
|
||||||
|
domain: tools
|
||||||
|
version: 0.1.0
|
||||||
|
description: "Testbed agresivo del modulo data_table — multi-tabla, menu QA toggleable, inyector eventos, counters live, version selector"
|
||||||
|
tags: [imgui, dashboard, qa, testing, data-table, regression]
|
||||||
|
uses_functions: []
|
||||||
|
uses_modules:
|
||||||
|
- data_table_cpp
|
||||||
|
uses_types: []
|
||||||
|
framework: "imgui"
|
||||||
|
entry_point: "main.cpp"
|
||||||
|
dir_path: "apps/tables_qa"
|
||||||
|
repo_url: "https://gitea.organic-machine.com/dataforge/tables_qa"
|
||||||
|
icon:
|
||||||
|
phosphor: "test-tube"
|
||||||
|
accent: "#f59e0b"
|
||||||
|
---
|
||||||
|
|
||||||
|
# tables_qa
|
||||||
|
|
||||||
|
Testbed agresivo del modulo `data_table_cpp` (v2.0.0+). Sustituye al `tables_playground` legacy (issue 0108).
|
||||||
|
|
||||||
|
Provee un panel de control QA con toggles, inyector de eventos, counters live y version selector para ejercer todas las capacidades del modulo en runtime.
|
||||||
|
|
||||||
|
## Capacidades cubiertas
|
||||||
|
|
||||||
|
Cada tab demuestra UNA capacidad del modulo. Toda capacidad listada en `docs/MODULES_API.md::Capability matrix consolidada` tiene su tab.
|
||||||
|
|
||||||
|
| Tab | Capacidad |
|
||||||
|
|---|---|
|
||||||
|
| `basic` | Text default + sort + filter chips |
|
||||||
|
| `renderers` | 1 columna por cada CellRenderer (Badge, Progress, Duration, Icon, Button, Dots, CategoricalChip, ColorScale) |
|
||||||
|
| `buttons` | Click events: Retry, Cancel, Inspect (consumido en `events_out`) |
|
||||||
|
| `color_rules` | ColorScale numerico + CategoricalChip configurado |
|
||||||
|
| `dots` | Status timeline sparkline |
|
||||||
|
| `joins` | 2 tablas + JoinStrategy Left/Inner/Right/Full toggleable |
|
||||||
|
| `tql` | Pipeline TQL completo + Ask AI panel |
|
||||||
|
| `drill` | Drill-down stages con breadcrumb |
|
||||||
|
| `events` | Inyector de eventos via worker thread (RowDoubleClick, RowRightClick, ButtonClick) |
|
||||||
|
| `compat` | Version selector + side-by-side con downgrade |
|
||||||
|
|
||||||
|
## Panel QA superior
|
||||||
|
|
||||||
|
Barra de control con:
|
||||||
|
- Toggles por feature (15+ checkboxes que mutan ColumnSpec en runtime).
|
||||||
|
- Inyector de eventos (4 botones que disparan TableEvents via worker).
|
||||||
|
- Counters live (`data_table::internal::*` — pendiente implementar en modulo).
|
||||||
|
- Version dropdown (2.0.0 / 1.5.0 compat / 1.4.0 compat).
|
||||||
|
- Boton "Run --self-test" (suite headless).
|
||||||
|
- Boton "Export golden" (regenera PNGs baseline).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cpp && cmake --build build --target tables_qa -j
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cpp/build/apps/tables_qa/tables_qa
|
||||||
|
```
|
||||||
|
|
||||||
|
## Headless self-test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cpp/build/apps/tables_qa/tables_qa --self-test
|
||||||
|
# Exit 0 = todas las capacidades verde
|
||||||
|
# Exit 1 = algun TableEvent no se emite o renderer crash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relacion con `tables_playground` (deprecado)
|
||||||
|
|
||||||
|
`apps/primitives_gallery/playground/tables/` queda como **legacy archive** post-0108. Su `self_test.cpp` (603 checks contra logica legacy) NO se reemplaza directo — los tests aplicables a logica pura del registry ya viven en `cpp/tests/` (Catch2). Los tests aplicables a `data_table::render` viven aqui en `tables_qa --self-test`.
|
||||||
|
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
||||||
|
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||||
|
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
||||||
|
- `patch`: bugfix sin cambio observable.
|
||||||
|
|
||||||
|
- v0.1.0 (2026-05-18) — baseline.
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,106 @@
|
|||||||
|
// tables_qa — entrypoint thin. Orquesta tab bar + QA panel flotante.
|
||||||
|
//
|
||||||
|
// Estructura:
|
||||||
|
// main.cpp — entry + tab bar + menu (este)
|
||||||
|
// qa_state.h/.cpp — toggles + counters + helpers compartidos
|
||||||
|
// qa_panel.h/.cpp — panel flotante (Window separada)
|
||||||
|
// test_suite.h/.cpp — boton "Run Tests" del panel
|
||||||
|
// perf_tests.h/.cpp — boton "Performance Tests" (millones de filas)
|
||||||
|
// tabs.h — declaraciones de cada tab
|
||||||
|
// tab_<X>.cpp — implementacion por tab (uno por agente fn-constructor)
|
||||||
|
//
|
||||||
|
// Issue 0108. v0.1.0.
|
||||||
|
|
||||||
|
#include "app_base.h"
|
||||||
|
#include "core/icons_tabler.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
#include "core/panel_menu.h"
|
||||||
|
|
||||||
|
#include "qa_panel.h"
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "perf_tests.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool g_show_main = true;
|
||||||
|
bool g_show_qa_panel = true;
|
||||||
|
|
||||||
|
void render() {
|
||||||
|
qa::counters().frames_rendered++;
|
||||||
|
|
||||||
|
if (g_show_qa_panel) tables_qa::render_qa_panel(&g_show_qa_panel);
|
||||||
|
|
||||||
|
if (!g_show_main) return;
|
||||||
|
|
||||||
|
if (!ImGui::Begin(TI_TABLE " tables_qa", &g_show_main,
|
||||||
|
ImGuiWindowFlags_NoCollapse)) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabBar("##qa_tabs")) {
|
||||||
|
using namespace tables_qa::tabs;
|
||||||
|
if (ImGui::BeginTabItem(TI_TABLE " basic")) { render_basic(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem(TI_PALETTE " renderers")) { render_renderers(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("buttons")) { render_buttons(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("color_rules")) { render_color_rules(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("dots")) { render_dots(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("joins")) { render_joins(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("tql")) { render_tql(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("drill")) { render_drill(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("events")) { render_events(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem("compat")) { render_compat(); ImGui::EndTabItem(); }
|
||||||
|
if (ImGui::BeginTabItem(TI_GAUGE " perf")) {
|
||||||
|
tables_qa::render_perf_tab();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool view_extras() {
|
||||||
|
tables_qa::render_qa_menu_item(&g_show_qa_panel);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (std::strcmp(argv[i], "--self-test") == 0) {
|
||||||
|
std::fprintf(stdout, "tables_qa --self-test: running suite headless...\n");
|
||||||
|
// TODO fase 2: imgui_test_engine. Por ahora corre test_suite sync.
|
||||||
|
// Esto requiere init parcial de framework (logger). Skip para WIP.
|
||||||
|
std::fprintf(stdout, "tables_qa --self-test: SKIPPED (fase 2 TBD)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fn_ui::PanelToggle panels[] = {
|
||||||
|
{ "Main", nullptr, &g_show_main },
|
||||||
|
{ "QA Panel", nullptr, &g_show_qa_panel },
|
||||||
|
};
|
||||||
|
|
||||||
|
fn::AppConfig cfg;
|
||||||
|
cfg.title = "tables_qa";
|
||||||
|
cfg.width = 1400;
|
||||||
|
cfg.height = 900;
|
||||||
|
cfg.about = {
|
||||||
|
"tables_qa",
|
||||||
|
"0.1.0",
|
||||||
|
"Testbed agresivo del modulo data_table (issue 0108).",
|
||||||
|
};
|
||||||
|
cfg.log = { "tables_qa.log", 1 };
|
||||||
|
cfg.panels = panels;
|
||||||
|
cfg.panel_count = sizeof(panels) / sizeof(panels[0]);
|
||||||
|
cfg.view_extras = view_extras;
|
||||||
|
|
||||||
|
return fn::run_app(cfg, render);
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
// perf_tests — stress data_table con N filas (default 1M).
|
||||||
|
//
|
||||||
|
// Patron: el boton "Performance Tests" del QA panel llama run_perf_test(rows, frames),
|
||||||
|
// que prepara el backing storage sync (lento si N grande) y marca el state como
|
||||||
|
// "perf mode ON". Durante los siguientes `frames` renders, render_perf_tab()
|
||||||
|
// mide latency por frame y al terminar deja resultados en qa::counters().
|
||||||
|
#include "perf_tests.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tables_qa {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Estado estatico del perf test. Sobrevive entre frames hasta nueva run.
|
||||||
|
struct PerfState {
|
||||||
|
bool active = false;
|
||||||
|
long long rows = 0;
|
||||||
|
int frames_target = 0;
|
||||||
|
int frames_measured = 0;
|
||||||
|
std::vector<double> frame_times_ms;
|
||||||
|
std::vector<std::string> back; // backing — puede ser N millones
|
||||||
|
std::vector<const char*> ptrs;
|
||||||
|
data_table::State dt;
|
||||||
|
};
|
||||||
|
|
||||||
|
PerfState& perf() { static PerfState s; return s; }
|
||||||
|
|
||||||
|
// Generador deterministico — mismo seed produce mismas filas.
|
||||||
|
void seed_synthetic(long long rows) {
|
||||||
|
auto& p = perf();
|
||||||
|
p.back.clear();
|
||||||
|
// Para N filas con 6 columnas, necesitamos rows*6 strings.
|
||||||
|
// Reservamos para evitar reallocs durante el push_back.
|
||||||
|
p.back.reserve(static_cast<size_t>(rows * 6));
|
||||||
|
|
||||||
|
char buf[64];
|
||||||
|
const char* statuses[] = {"ok", "error", "running", "pending"};
|
||||||
|
const char* langs[] = {"go", "py", "cpp"};
|
||||||
|
const int n_statuses = 4;
|
||||||
|
const int n_langs = 3;
|
||||||
|
|
||||||
|
for (long long i = 0; i < rows; ++i) {
|
||||||
|
// id
|
||||||
|
std::snprintf(buf, sizeof(buf), "%lld", i + 1);
|
||||||
|
p.back.emplace_back(buf);
|
||||||
|
// name
|
||||||
|
std::snprintf(buf, sizeof(buf), "item_%lld", i);
|
||||||
|
p.back.emplace_back(buf);
|
||||||
|
// status
|
||||||
|
p.back.emplace_back(statuses[i % n_statuses]);
|
||||||
|
// duration_ms — varia 50..7000
|
||||||
|
std::snprintf(buf, sizeof(buf), "%lld", 50 + (i * 137) % 6950);
|
||||||
|
p.back.emplace_back(buf);
|
||||||
|
// lang
|
||||||
|
p.back.emplace_back(langs[i % n_langs]);
|
||||||
|
// value 0..10000
|
||||||
|
std::snprintf(buf, sizeof(buf), "%lld", (i * 71) % 10000);
|
||||||
|
p.back.emplace_back(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
qa::TabState dummy;
|
||||||
|
p.ptrs.clear();
|
||||||
|
p.ptrs.reserve(p.back.size());
|
||||||
|
for (auto& s : p.back) p.ptrs.push_back(s.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void run_perf_test(long long rows, int frames) {
|
||||||
|
fn_log::log_info("perf_test: starting rows=%lld frames=%d", rows, frames);
|
||||||
|
auto& p = perf();
|
||||||
|
p.active = true;
|
||||||
|
p.rows = rows;
|
||||||
|
p.frames_target = frames;
|
||||||
|
p.frames_measured = 0;
|
||||||
|
p.frame_times_ms.clear();
|
||||||
|
p.frame_times_ms.reserve(static_cast<size_t>(frames));
|
||||||
|
|
||||||
|
// Seed sync. Para 10M filas tarda varios segundos.
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
seed_synthetic(rows);
|
||||||
|
auto t1 = std::chrono::steady_clock::now();
|
||||||
|
double seed_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||||||
|
|
||||||
|
auto& c = qa::counters();
|
||||||
|
c.last_perf_rows = rows;
|
||||||
|
c.last_perf_seed_ms = seed_ms;
|
||||||
|
c.last_perf_frames = 0;
|
||||||
|
c.last_perf_render_ms_p50 = 0.0;
|
||||||
|
c.last_perf_render_ms_p95 = 0.0;
|
||||||
|
|
||||||
|
fn_log::log_info("perf_test: seeded rows=%lld in %.2f ms", rows, seed_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_perf_tab() {
|
||||||
|
auto& p = perf();
|
||||||
|
|
||||||
|
if (!p.active) {
|
||||||
|
ImGui::TextDisabled("No perf test active.");
|
||||||
|
ImGui::TextDisabled("Open QA Control Panel and click " "Performance Tests" " to start.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KPI header
|
||||||
|
auto& c = qa::counters();
|
||||||
|
ImGui::Text("Active perf: rows=%lld seed=%.1fms frames_done=%d/%d",
|
||||||
|
p.rows, c.last_perf_seed_ms, p.frames_measured, p.frames_target);
|
||||||
|
if (p.frames_measured > 0) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text(" p50=%.2fms p95=%.2fms",
|
||||||
|
c.last_perf_render_ms_p50, c.last_perf_render_ms_p95);
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// Construir TableInput
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "perf";
|
||||||
|
tbl.headers = {"id", "name", "status", "duration_ms", "lang", "value"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
};
|
||||||
|
tbl.cells = p.ptrs.data();
|
||||||
|
tbl.rows = static_cast<int>(p.rows);
|
||||||
|
tbl.cols = 6;
|
||||||
|
|
||||||
|
// Renderers — ejercemos los caros (CategoricalChip + ColorScale + Duration).
|
||||||
|
tbl.column_specs.resize(tbl.cols);
|
||||||
|
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
tbl.column_specs[2].chips = {
|
||||||
|
{"ok", "#22c55e"}, {"error", "#ef4444"},
|
||||||
|
{"running", "#f59e0b"}, {"pending", "#a3a3a3"},
|
||||||
|
};
|
||||||
|
tbl.column_specs[3].renderer = data_table::CellRenderer::Duration;
|
||||||
|
tbl.column_specs[3].duration_warn_ms = 1000.0f;
|
||||||
|
tbl.column_specs[3].duration_error_ms = 5000.0f;
|
||||||
|
tbl.column_specs[4].renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
tbl.column_specs[4].chips = {
|
||||||
|
{"go", "#3b82f6"}, {"py", "#22c55e"}, {"cpp", "#a855f7"},
|
||||||
|
};
|
||||||
|
tbl.column_specs[5].renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
tbl.column_specs[5].range_min = 0.0;
|
||||||
|
tbl.column_specs[5].range_max = 10000.0;
|
||||||
|
tbl.column_specs[5].range_alpha = 0.30f;
|
||||||
|
|
||||||
|
// Medir frame time
|
||||||
|
ImGui::BeginChild("##perf_host", ImVec2(-1, -1));
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
data_table::render("##perf_tbl", {tbl}, p.dt, nullptr, true);
|
||||||
|
auto t1 = std::chrono::steady_clock::now();
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
if (p.frames_measured < p.frames_target) {
|
||||||
|
double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||||||
|
p.frame_times_ms.push_back(ms);
|
||||||
|
p.frames_measured++;
|
||||||
|
|
||||||
|
if (p.frames_measured == p.frames_target) {
|
||||||
|
// Compute p50 / p95
|
||||||
|
std::vector<double> sorted = p.frame_times_ms;
|
||||||
|
std::sort(sorted.begin(), sorted.end());
|
||||||
|
size_t n = sorted.size();
|
||||||
|
double p50 = sorted[n / 2];
|
||||||
|
double p95 = sorted[(n * 95) / 100];
|
||||||
|
c.last_perf_render_ms_p50 = p50;
|
||||||
|
c.last_perf_render_ms_p95 = p95;
|
||||||
|
c.last_perf_frames = p.frames_measured;
|
||||||
|
fn_log::log_info("perf_test DONE rows=%lld frames=%d p50=%.2fms p95=%.2fms",
|
||||||
|
p.rows, p.frames_measured, p50, p95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
// perf_tests — stress test data_table::render con N filas (default 1M).
|
||||||
|
//
|
||||||
|
// Mide:
|
||||||
|
// - seed_ms: tiempo en construir backing storage de N filas sinteticas.
|
||||||
|
// - render_ms_p50/p95: latency por frame durante M frames consecutivos.
|
||||||
|
//
|
||||||
|
// Caso 1M filas verifica que el modulo escala con ImGui clipper sin O(N).
|
||||||
|
// Resultados a qa::counters().last_perf_*. Tabla generada queda visible en
|
||||||
|
// el tab "perf" hasta que se relance.
|
||||||
|
|
||||||
|
namespace tables_qa {
|
||||||
|
|
||||||
|
// Construye un dataset sintetico de N filas y lo renderiza durante `frames` ciclos.
|
||||||
|
// NO bloquea el frame actual: hace seed sync, almacena la tabla en estado
|
||||||
|
// estatico, y mide frame-times durante los siguientes `frames` renders.
|
||||||
|
// La medicion ocurre cuando el tab "perf" esta visible.
|
||||||
|
void run_perf_test(long long rows, int frames);
|
||||||
|
|
||||||
|
// Render del tab perf (la tabla con N filas + KPIs). Llamado desde render_tab_perf.
|
||||||
|
void render_perf_tab();
|
||||||
|
|
||||||
|
} // namespace tables_qa
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
// qa_panel — implementacion del panel QA flotante.
|
||||||
|
#include "qa_panel.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "test_suite.h"
|
||||||
|
#include "perf_tests.h"
|
||||||
|
#include "core/icons_tabler.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace tables_qa {
|
||||||
|
|
||||||
|
void render_qa_menu_item(bool* open) {
|
||||||
|
ImGui::MenuItem(TI_SETTINGS " QA Panel", nullptr, open);
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_qa_panel(bool* open) {
|
||||||
|
if (!*open) return;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(900, 280), ImGuiCond_FirstUseEver);
|
||||||
|
if (!ImGui::Begin(TI_FLASK " QA Control Panel", open,
|
||||||
|
ImGuiWindowFlags_NoCollapse)) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& t = qa::toggles();
|
||||||
|
auto& c = qa::counters();
|
||||||
|
|
||||||
|
// --- Section: Counters live ---
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.7f, 1.0f, 1.0f),
|
||||||
|
TI_ACTIVITY " Counters live");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::BeginTable("##counters_tbl", 4, ImGuiTableFlags_SizingStretchSame);
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("button_clicks");
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("%d", c.button_clicks);
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("row_double_click");
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("%d", c.row_double_click);
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("row_right_click");
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("%d", c.row_right_click);
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("frames_rendered");
|
||||||
|
ImGui::TableNextColumn(); ImGui::Text("%d", c.frames_rendered);
|
||||||
|
ImGui::EndTable();
|
||||||
|
if (ImGui::Button(TI_REFRESH " Reset counters")) {
|
||||||
|
c.button_clicks = c.row_double_click = c.row_right_click = c.frames_rendered = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// --- Section: Toggles ---
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.7f, 1.0f, 1.0f),
|
||||||
|
TI_SETTINGS " Feature toggles");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Checkbox("show_chrome", &t.show_chrome);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("buttons", &t.enable_buttons);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("badges", &t.enable_badges);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("colorscale", &t.enable_color_scale);
|
||||||
|
ImGui::Checkbox("categorical", &t.enable_categorical);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("duration", &t.enable_duration);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("dots", &t.enable_dots);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("icon", &t.enable_icon);
|
||||||
|
ImGui::Checkbox("tooltip", &t.enable_tooltip);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("row_dblclick", &t.enable_row_dblclick);
|
||||||
|
ImGui::SameLine(); ImGui::Checkbox("row_rmb", &t.enable_row_rightclick);
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// --- Section: Module version + actions ---
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.7f, 1.0f, 1.0f),
|
||||||
|
TI_GIT_BRANCH " Module data_table");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text("Active version:");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), "2.1.0");
|
||||||
|
ImGui::SameLine(); ImGui::Dummy(ImVec2(20, 0)); ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("[compat downgrade selector: TBD fase 2]");
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// --- Section: Actions — Test + Perf ---
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.7f, 1.0f, 1.0f),
|
||||||
|
TI_PLAYER_PLAY " Actions");
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::Button(TI_CHECK " Run Tests", ImVec2(180, 32))) {
|
||||||
|
tables_qa::run_test_suite();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (c.last_test_total > 0) {
|
||||||
|
ImVec4 col = (c.last_test_failed == 0)
|
||||||
|
? ImVec4(0.4f, 0.9f, 0.4f, 1.0f) : ImVec4(0.9f, 0.4f, 0.4f, 1.0f);
|
||||||
|
ImGui::TextColored(col, " Tests: %d/%d pass (%d failed)",
|
||||||
|
c.last_test_passed, c.last_test_total, c.last_test_failed);
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled(" (no tests run yet)");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (ImGui::Button(TI_GAUGE " Performance Tests", ImVec2(180, 32))) {
|
||||||
|
ImGui::OpenPopup("##perf_popup");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (c.last_perf_rows > 0) {
|
||||||
|
ImGui::Text(" Last: %lld rows seed=%.1fms p50=%.2fms p95=%.2fms (%d frames)",
|
||||||
|
c.last_perf_rows, c.last_perf_seed_ms,
|
||||||
|
c.last_perf_render_ms_p50, c.last_perf_render_ms_p95,
|
||||||
|
c.last_perf_frames);
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled(" (no perf run yet)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popup de configuracion de perf test
|
||||||
|
if (ImGui::BeginPopup("##perf_popup")) {
|
||||||
|
static int rows_pow = 6; // 10^6 = 1M default
|
||||||
|
static int frames_to_measure = 60;
|
||||||
|
ImGui::Text("Stress test data_table::render with N rows.");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::SliderInt("rows (10^N)", &rows_pow, 3, 7); // 1K..10M
|
||||||
|
ImGui::SliderInt("frames", &frames_to_measure, 30, 300);
|
||||||
|
long long rows = 1;
|
||||||
|
for (int i = 0; i < rows_pow; ++i) rows *= 10;
|
||||||
|
ImGui::Text("Target: %lld rows, %d frames measured", rows, frames_to_measure);
|
||||||
|
|
||||||
|
if (ImGui::Button(TI_PLAYER_PLAY " Run perf")) {
|
||||||
|
tables_qa::run_perf_test(rows, frames_to_measure);
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
// qa_panel — barra superior con toggles + counters + version + boton Test + Perf.
|
||||||
|
//
|
||||||
|
// Window flotante separada (no inline) — usuario puede arrastrarla fuera del main.
|
||||||
|
// Triggered desde el menubar (View > QA Panel) o key shortcut.
|
||||||
|
|
||||||
|
namespace tables_qa {
|
||||||
|
|
||||||
|
// Render del QA panel como window flotante. Tiene su propio ImGui::Begin/End.
|
||||||
|
// Llamar una vez por frame desde render() principal si `open` es true.
|
||||||
|
void render_qa_panel(bool* open);
|
||||||
|
|
||||||
|
// Render del menubar item "View > QA Panel" toggle.
|
||||||
|
void render_qa_menu_item(bool* open);
|
||||||
|
|
||||||
|
} // namespace tables_qa
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#include "qa_state.h"
|
||||||
|
|
||||||
|
namespace qa {
|
||||||
|
|
||||||
|
QaToggles& toggles() { static QaToggles t; return t; }
|
||||||
|
QaCounters& counters() { static QaCounters c; return c; }
|
||||||
|
|
||||||
|
void rebuild_ptrs(TabState& s) {
|
||||||
|
s.ptrs.clear();
|
||||||
|
s.ptrs.reserve(s.back.size());
|
||||||
|
for (auto& str : s.back) s.ptrs.push_back(str.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void consume_events(const std::vector<data_table::TableEvent>& events) {
|
||||||
|
auto& c = counters();
|
||||||
|
for (const auto& ev : events) {
|
||||||
|
switch (ev.kind) {
|
||||||
|
case data_table::TableEventKind::ButtonClick: c.button_clicks++; break;
|
||||||
|
case data_table::TableEventKind::RowDoubleClick: c.row_double_click++; break;
|
||||||
|
case data_table::TableEventKind::RowRightClick: c.row_right_click++; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace qa
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
// qa_state.h — estado compartido entre tabs + panel QA + perf/test suites.
|
||||||
|
//
|
||||||
|
// Centraliza:
|
||||||
|
// - QaToggles: feature flags activables por checkbox.
|
||||||
|
// - QaCounters: counters live emulando data_table::internal::* (TBD modulo v2.2.0).
|
||||||
|
// - TabState: backing storage por tab + data_table::State persistente.
|
||||||
|
//
|
||||||
|
// Una sola instancia global por proceso. Acceso via accessors definidos en
|
||||||
|
// qa_state.cpp para evitar duplicate symbol entre TUs.
|
||||||
|
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include "core/data_table_types.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace qa {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Toggles globales. Apps reales mutan ColumnSpec en runtime con el mismo patron.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct QaToggles {
|
||||||
|
bool show_chrome = true;
|
||||||
|
bool enable_buttons = true;
|
||||||
|
bool enable_color_scale = true;
|
||||||
|
bool enable_categorical = true;
|
||||||
|
bool enable_badges = true;
|
||||||
|
bool enable_duration = true;
|
||||||
|
bool enable_dots = true;
|
||||||
|
bool enable_icon = false;
|
||||||
|
bool enable_tooltip = true;
|
||||||
|
bool enable_row_dblclick = true;
|
||||||
|
bool enable_row_rightclick = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Counters live. Cada vez que un TabEvent pasa por consume_events() se incrementan.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct QaCounters {
|
||||||
|
int button_clicks = 0;
|
||||||
|
int row_double_click = 0;
|
||||||
|
int row_right_click = 0;
|
||||||
|
int frames_rendered = 0;
|
||||||
|
// Perf
|
||||||
|
long long last_perf_rows = 0;
|
||||||
|
double last_perf_seed_ms = 0.0;
|
||||||
|
double last_perf_render_ms_p50 = 0.0;
|
||||||
|
double last_perf_render_ms_p95 = 0.0;
|
||||||
|
int last_perf_frames = 0;
|
||||||
|
// Test suite
|
||||||
|
int last_test_total = 0;
|
||||||
|
int last_test_passed = 0;
|
||||||
|
int last_test_failed = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TabState — backing storage + data_table::State por tab.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct TabState {
|
||||||
|
std::vector<std::string> back;
|
||||||
|
std::vector<const char*> ptrs;
|
||||||
|
data_table::State dt;
|
||||||
|
std::vector<data_table::TableEvent> last_events;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Refill ptrs[] desde back[]. Llamar tras cada cambio.
|
||||||
|
void rebuild_ptrs(TabState& s);
|
||||||
|
|
||||||
|
// Consume events emitidos por render(): incrementa counters globales.
|
||||||
|
void consume_events(const std::vector<data_table::TableEvent>& events);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Singletons (definidos en qa_state.cpp).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
QaToggles& toggles();
|
||||||
|
QaCounters& counters();
|
||||||
|
|
||||||
|
} // namespace qa
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// tab_basic — Text default + sort + filter chips. Caso minimo.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
qa::TabState g_st;
|
||||||
|
|
||||||
|
void seed() {
|
||||||
|
g_st.back = {
|
||||||
|
"1","alpha", "100",
|
||||||
|
"2","beta", "250",
|
||||||
|
"3","gamma", "75",
|
||||||
|
"4","delta", "500",
|
||||||
|
"5","epsilon","12",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void render_basic() {
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "basic";
|
||||||
|
tbl.headers = {"id", "name", "value"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 5;
|
||||||
|
tbl.cols = 3;
|
||||||
|
|
||||||
|
ImGui::BeginChild("##basic_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##basic_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, qa::toggles().show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
// tab_buttons — ejercita CellRenderer::Button con 3 action_ids distintos
|
||||||
|
// (retry / cancel / inspect) y consume sus TableEvents.
|
||||||
|
// Muestra contador local de clicks por accion + boton Reset.
|
||||||
|
// Issue 0108 fase 2.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
qa::TabState g_st;
|
||||||
|
int g_retry_clicks = 0;
|
||||||
|
int g_cancel_clicks = 0;
|
||||||
|
int g_inspect_clicks = 0;
|
||||||
|
|
||||||
|
void seed() {
|
||||||
|
// 5 filas x 6 columnas: id, name, status, action_retry, action_cancel, action_inspect
|
||||||
|
// Las columnas de boton llevan el label del boton en la celda (se usa como button_label si
|
||||||
|
// button_label esta vacio; aqui lo dejamos vacio para que el renderer use button_action).
|
||||||
|
// Usamos una cadena plausible para cada celda de boton.
|
||||||
|
g_st.back = {
|
||||||
|
// id name status retry cancel inspect
|
||||||
|
"1", "job-alpha", "ok", "Retry", "Cancel", "Inspect",
|
||||||
|
"2", "job-beta", "error", "Retry", "Cancel", "Inspect",
|
||||||
|
"3", "job-gamma", "running","Retry", "Cancel", "Inspect",
|
||||||
|
"4", "job-delta", "error", "Retry", "Cancel", "Inspect",
|
||||||
|
"5", "job-epsilon", "ok", "Retry", "Cancel", "Inspect",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void render_buttons() {
|
||||||
|
// 1. Seed si vacio
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
|
||||||
|
// 2. UI — contadores locales + boton Reset (encima del BeginChild)
|
||||||
|
ImGui::Text("Local click counters:");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("Retry: %d", g_retry_clicks);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text(" Cancel: %d", g_cancel_clicks);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text(" Inspect: %d", g_inspect_clicks);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("Reset")) {
|
||||||
|
g_retry_clicks = 0;
|
||||||
|
g_cancel_clicks = 0;
|
||||||
|
g_inspect_clicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Construir TableInput con 6 columnas
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "buttons";
|
||||||
|
tbl.headers = {"id", "name", "status", "action_retry", "action_cancel", "action_inspect"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 5;
|
||||||
|
tbl.cols = 6;
|
||||||
|
|
||||||
|
// column_specs declarativos
|
||||||
|
tbl.column_specs.resize(tbl.cols);
|
||||||
|
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
|
||||||
|
// status: CategoricalChip (ok=verde, error=rojo, running=amarillo)
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[2];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"ok", "#22c55e"},
|
||||||
|
{"error", "#ef4444"},
|
||||||
|
{"running", "#f59e0b"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// action_retry
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Button;
|
||||||
|
cs.button_action = "retry";
|
||||||
|
cs.button_color_hex = "#3b82f6";
|
||||||
|
}
|
||||||
|
// action_cancel
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::Button;
|
||||||
|
cs.button_action = "cancel";
|
||||||
|
cs.button_color_hex = "#ef4444";
|
||||||
|
}
|
||||||
|
// action_inspect
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[5];
|
||||||
|
cs.renderer = data_table::CellRenderer::Button;
|
||||||
|
cs.button_action = "inspect";
|
||||||
|
cs.button_color_hex = "#a855f7";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Render dentro de BeginChild
|
||||||
|
ImGui::BeginChild("##buttons_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##buttons_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, qa::toggles().show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// 5. Consumir eventos globales (incrementa qa::counters)
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
|
||||||
|
// 6. Iterar last_events para contadores locales + log de eventos de fila
|
||||||
|
for (const auto& ev : g_st.last_events) {
|
||||||
|
switch (ev.kind) {
|
||||||
|
case data_table::TableEventKind::ButtonClick:
|
||||||
|
if (ev.action_id == "retry") g_retry_clicks++;
|
||||||
|
else if (ev.action_id == "cancel") g_cancel_clicks++;
|
||||||
|
else if (ev.action_id == "inspect") g_inspect_clicks++;
|
||||||
|
break;
|
||||||
|
case data_table::TableEventKind::RowDoubleClick:
|
||||||
|
fn_log::log_info("buttons: RowDoubleClick row=%d", ev.row);
|
||||||
|
break;
|
||||||
|
case data_table::TableEventKind::RowRightClick:
|
||||||
|
fn_log::log_info("buttons: RowRightClick row=%d", ev.row);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// tab_color_rules — demostración de reglas de color condicionales.
|
||||||
|
//
|
||||||
|
// Columnas:
|
||||||
|
// id Int — row id
|
||||||
|
// name String — nombre del job
|
||||||
|
// status String — Badge básico (ok/error/running/warning)
|
||||||
|
// lang String — CategoricalChip: go/py/cpp/rust con 4 colores
|
||||||
|
// score Float — ColorScale default verde→amber→rojo (0..1000)
|
||||||
|
// latency_ms Float — ColorScale custom stops: 0=verde, 200=amber, 1000=rojo
|
||||||
|
// priority String — Badge: low/medium/high/critical con labels custom
|
||||||
|
//
|
||||||
|
// UI controls (antes de BeginChild):
|
||||||
|
// Slider "alpha" → muta range_alpha de ambas cols ColorScale
|
||||||
|
// Toggle "reverse stops" → invierte los ColorStops de latency_ms
|
||||||
|
// Toggle "expand chips" → añade java/rust/zig a la palette de lang
|
||||||
|
//
|
||||||
|
// Patrón: tab_renderers.cpp
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
qa::TabState g_st;
|
||||||
|
|
||||||
|
// Local controls (no globales para no contaminar QaToggles compartido)
|
||||||
|
float g_alpha = 0.30f;
|
||||||
|
bool g_reverse_stops = false;
|
||||||
|
bool g_expand_chips = false;
|
||||||
|
|
||||||
|
// 12 filas × 7 cols = 84 celdas
|
||||||
|
// cols: id, name, status, lang, score, latency_ms, priority
|
||||||
|
void seed() {
|
||||||
|
g_st.back = {
|
||||||
|
// id name status lang score latency_ms priority
|
||||||
|
"1", "ingest_raw", "ok", "go", "920", "45", "low",
|
||||||
|
"2", "parse_schema", "ok", "py", "780", "130", "medium",
|
||||||
|
"3", "validate_types", "error", "rust", "210", "850", "high",
|
||||||
|
"4", "enrich_entities", "running", "cpp", "550", "320", "medium",
|
||||||
|
"5", "dedup_records", "ok", "go", "870", "88", "low",
|
||||||
|
"6", "score_relevance", "ok", "py", "640", "175", "medium",
|
||||||
|
"7", "classify_intent", "error", "cpp", "120", "990", "critical",
|
||||||
|
"8", "aggregate_daily", "ok", "go", "730", "65", "low",
|
||||||
|
"9", "export_warehouse", "running", "rust", "480", "410", "high",
|
||||||
|
"10", "notify_downstream", "warning", "py", "340", "620", "high",
|
||||||
|
"11", "cleanup_staging", "ok", "go", "810", "55", "low",
|
||||||
|
"12", "audit_pipeline", "error", "cpp", "190", "1100", "critical",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void render_color_rules() {
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
const auto& t = qa::toggles();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// UI controls — fuera del BeginChild para que respondan de inmediato
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
ImGui::SetNextItemWidth(220.0f);
|
||||||
|
ImGui::SliderFloat("alpha##cr", &g_alpha, 0.0f, 1.0f, "%.2f");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox("reverse stops##cr", &g_reverse_stops);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox("expand chips palette##cr", &g_expand_chips);
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// TableInput
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "color_rules";
|
||||||
|
tbl.headers = {"id", "name", "status", "lang", "score", "latency_ms", "priority"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 12;
|
||||||
|
tbl.cols = 7;
|
||||||
|
|
||||||
|
tbl.column_specs.resize(tbl.cols);
|
||||||
|
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
|
||||||
|
// col 2 — status: Badge básico
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[2];
|
||||||
|
cs.renderer = data_table::CellRenderer::Badge;
|
||||||
|
cs.badges = {
|
||||||
|
{"ok", "#22c55e", ""},
|
||||||
|
{"error", "#ef4444", "ERR"},
|
||||||
|
{"running", "#f59e0b", ""},
|
||||||
|
{"warning", "#f97316", "WARN"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 3 — lang: CategoricalChip (dot izquierdo por valor)
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"go", "#3b82f6"}, // azul
|
||||||
|
{"py", "#22c55e"}, // verde
|
||||||
|
{"cpp", "#a855f7"}, // violeta
|
||||||
|
{"rust", "#ef4444"}, // rojo
|
||||||
|
};
|
||||||
|
if (g_expand_chips) {
|
||||||
|
cs.chips.push_back({"java", "#f59e0b"}); // amber
|
||||||
|
cs.chips.push_back({"zig", "#06b6d4"}); // cyan
|
||||||
|
cs.chips.push_back({"lua", "#ec4899"}); // pink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 4 — score: ColorScale default (verde→amber→rojo), range 0..1000
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 1000.0;
|
||||||
|
cs.range_alpha = g_alpha;
|
||||||
|
// range_stops vacío → default verde→amber→rojo interno
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 5 — latency_ms: ColorScale custom stops (0=verde, 200=amber, 1000=rojo)
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[5];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 1200.0;
|
||||||
|
cs.range_alpha = g_alpha;
|
||||||
|
|
||||||
|
if (!g_reverse_stops) {
|
||||||
|
cs.range_stops = {
|
||||||
|
{0.00f, "#22c55e"}, // 0 ms → verde
|
||||||
|
{0.17f, "#f59e0b"}, // 200 ms → amber
|
||||||
|
{0.83f, "#ef4444"}, // 1000 ms → rojo
|
||||||
|
{1.00f, "#7f1d1d"}, // 1200 ms → rojo oscuro
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Invertido: bajo=rojo, alto=verde
|
||||||
|
cs.range_stops = {
|
||||||
|
{0.00f, "#7f1d1d"},
|
||||||
|
{0.17f, "#ef4444"},
|
||||||
|
{0.83f, "#f59e0b"},
|
||||||
|
{1.00f, "#22c55e"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 6 — priority: Badge con label custom por valor
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[6];
|
||||||
|
cs.renderer = data_table::CellRenderer::Badge;
|
||||||
|
cs.badges = {
|
||||||
|
{"low", "#6b7280", "LOW"},
|
||||||
|
{"medium", "#3b82f6", "MED"},
|
||||||
|
{"high", "#f59e0b", "HIGH"},
|
||||||
|
{"critical", "#ef4444", "CRIT"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Render
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
ImGui::BeginChild("##color_rules_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##color_rules_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, t.show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
// tab_compat — Version selector + side-by-side rendering (simulación educativa).
|
||||||
|
// Muestra la MISMA TableInput renderizada dos veces con distinta ColumnSpec config:
|
||||||
|
// Left panel : full renderers (v2.1 current).
|
||||||
|
// Right panel : renderers degradados según la versión simulada seleccionada.
|
||||||
|
//
|
||||||
|
// Issue 0108 fase 2. Patrón: tab_renderers.cpp.
|
||||||
|
// NOTA: esto es una SIMULACIÓN educativa del compat mode futuro.
|
||||||
|
// El compat real requiere flag en el módulo (TBD issue futuro).
|
||||||
|
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Datos: 8 filas × 5 cols (id, name, status, duration_ms, priority).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
qa::TabState g_left; // panel izquierdo — full config v2.1
|
||||||
|
qa::TabState g_right; // panel derecho — config degradada
|
||||||
|
|
||||||
|
static const char* k_headers[] = { "id", "name", "status", "duration_ms", "priority" };
|
||||||
|
static const int k_cols = 5;
|
||||||
|
static const int k_rows = 8;
|
||||||
|
|
||||||
|
// backing strings: row-major id/name/status/duration_ms/priority
|
||||||
|
static const char* k_cells_raw[k_rows * k_cols] = {
|
||||||
|
"1", "auth_service", "ok", "320", "high",
|
||||||
|
"2", "db_sync", "error", "4800", "critical",
|
||||||
|
"3", "cache_warmup", "running", "750", "medium",
|
||||||
|
"4", "report_gen", "pending", "2100", "low",
|
||||||
|
"5", "email_dispatch", "ok", "180", "high",
|
||||||
|
"6", "index_rebuild", "error", "5900", "critical",
|
||||||
|
"7", "metrics_collector", "running", "950", "medium",
|
||||||
|
"8", "snapshot_export", "pending", "1500", "low",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Versiones simuladas
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
enum class SimVersion {
|
||||||
|
V21 = 0, // v2.1.0 — full renderers
|
||||||
|
V20 = 1, // v2.0.0 — full renderers, solo split refactor
|
||||||
|
V15 = 2, // v1.5.0 — no CategoricalChip → fallback Text en status
|
||||||
|
V14 = 3, // v1.4.0 — no Dots, no CategoricalChip, no ColorScale → Badge básico
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* k_version_labels[] = {
|
||||||
|
"v2.1.0 (current)",
|
||||||
|
"v2.0.0 (split refactor, MODE: no renderer config)",
|
||||||
|
"v1.5.0 (legacy, MODE: no CategoricalChip)",
|
||||||
|
"v1.4.0 (legacy, MODE: no Dots + no ColorScale)",
|
||||||
|
};
|
||||||
|
static int g_selected_version = 0;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Construir TableInput — cabeceras + tipos + punteros a datos estáticos.
|
||||||
|
// ColumnSpec se rellena por cada panel por separado.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static data_table::TableInput make_base_table(const char* tbl_name) {
|
||||||
|
data_table::TableInput t;
|
||||||
|
t.name = tbl_name;
|
||||||
|
t.headers = { "id", "name", "status", "duration_ms", "priority" };
|
||||||
|
t.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
};
|
||||||
|
t.cells = k_cells_raw;
|
||||||
|
t.rows = k_rows;
|
||||||
|
t.cols = k_cols;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifica las reglas de CategoricalChip para la col status
|
||||||
|
static void apply_status_chip(data_table::ColumnSpec& cs) {
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{ "ok", "#22c55e" },
|
||||||
|
{ "error", "#ef4444" },
|
||||||
|
{ "running", "#f59e0b" },
|
||||||
|
{ "pending", "#a3a3a3" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifica Badge básico (fallback sin CategoricalChip)
|
||||||
|
static void apply_status_badge(data_table::ColumnSpec& cs) {
|
||||||
|
cs.renderer = data_table::CellRenderer::Badge;
|
||||||
|
cs.badges = {
|
||||||
|
{ "ok", "#22c55e", "" },
|
||||||
|
{ "error", "#ef4444", "ERR" },
|
||||||
|
{ "running", "#f59e0b", "RUN" },
|
||||||
|
{ "pending", "#a3a3a3", "..." },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica specs para panel LEFT — siempre v2.1 full
|
||||||
|
static void apply_full_specs(data_table::TableInput& t) {
|
||||||
|
t.column_specs.resize(k_cols);
|
||||||
|
for (int i = 0; i < k_cols; i++) t.column_specs[i].id = k_headers[i];
|
||||||
|
|
||||||
|
// col 0: id — Text (default)
|
||||||
|
// col 1: name — Text (default)
|
||||||
|
|
||||||
|
// col 2: status — CategoricalChip
|
||||||
|
apply_status_chip(t.column_specs[2]);
|
||||||
|
|
||||||
|
// col 3: duration_ms — Duration renderer
|
||||||
|
{
|
||||||
|
auto& cs = t.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Duration;
|
||||||
|
cs.duration_warn_ms = 1000.0f;
|
||||||
|
cs.duration_error_ms = 5000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 4: priority — ColorScale
|
||||||
|
{
|
||||||
|
auto& cs = t.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 4.0;
|
||||||
|
cs.range_alpha = 0.30f;
|
||||||
|
// stops: low (green) → medium (amber) → high/critical (red)
|
||||||
|
cs.range_stops = {
|
||||||
|
{ 0.0f, "#22c55e" },
|
||||||
|
{ 0.5f, "#f59e0b" },
|
||||||
|
{ 1.0f, "#ef4444" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica specs para panel RIGHT — degradado según versión seleccionada
|
||||||
|
static void apply_downgraded_specs(data_table::TableInput& t, SimVersion ver) {
|
||||||
|
t.column_specs.resize(k_cols);
|
||||||
|
for (int i = 0; i < k_cols; i++) t.column_specs[i].id = k_headers[i];
|
||||||
|
|
||||||
|
switch (ver) {
|
||||||
|
case SimVersion::V21:
|
||||||
|
// Igual que full (no debería llegar aquí, pero por completitud)
|
||||||
|
apply_full_specs(t);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SimVersion::V20:
|
||||||
|
// v2.0.0: full renderers disponibles, mismo resultado visual.
|
||||||
|
// La diferencia real es interna (split refactor en el módulo).
|
||||||
|
// Simulamos: idéntico a v2.1.
|
||||||
|
apply_status_chip(t.column_specs[2]);
|
||||||
|
{
|
||||||
|
auto& cs = t.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Duration;
|
||||||
|
cs.duration_warn_ms = 1000.0f;
|
||||||
|
cs.duration_error_ms = 5000.0f;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto& cs = t.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 4.0;
|
||||||
|
cs.range_alpha = 0.30f;
|
||||||
|
cs.range_stops = {
|
||||||
|
{ 0.0f, "#22c55e" },
|
||||||
|
{ 0.5f, "#f59e0b" },
|
||||||
|
{ 1.0f, "#ef4444" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SimVersion::V15:
|
||||||
|
// v1.5.0: CategoricalChip no disponible → fallback Badge en status.
|
||||||
|
// Duration + ColorScale sí disponibles.
|
||||||
|
apply_status_badge(t.column_specs[2]);
|
||||||
|
{
|
||||||
|
auto& cs = t.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Duration;
|
||||||
|
cs.duration_warn_ms = 1000.0f;
|
||||||
|
cs.duration_error_ms = 5000.0f;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto& cs = t.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 4.0;
|
||||||
|
cs.range_alpha = 0.30f;
|
||||||
|
cs.range_stops = {
|
||||||
|
{ 0.0f, "#22c55e" },
|
||||||
|
{ 0.5f, "#f59e0b" },
|
||||||
|
{ 1.0f, "#ef4444" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SimVersion::V14:
|
||||||
|
// v1.4.0: ni CategoricalChip, ni ColorScale → Badge básico en status,
|
||||||
|
// Duration degradada (Badge de rango), priority Text plano.
|
||||||
|
apply_status_badge(t.column_specs[2]);
|
||||||
|
// Duration: sin renderer específico → solo Text
|
||||||
|
t.column_specs[3].renderer = data_table::CellRenderer::Text;
|
||||||
|
// priority: Text plano
|
||||||
|
t.column_specs[4].renderer = data_table::CellRenderer::Text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// render_compat — entry point
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void render_compat() {
|
||||||
|
// ---- Cabecera de controles ----
|
||||||
|
ImGui::TextDisabled("Active module: data_table v2.1.0");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(360.0f);
|
||||||
|
ImGui::Combo("Compare with version", &g_selected_version,
|
||||||
|
k_version_labels, IM_ARRAYSIZE(k_version_labels));
|
||||||
|
|
||||||
|
// Nota informativa sobre la simulación
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("(educational simulation — real compat requires module flag, TBD)");
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// Botón TBD
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
ImGui::Button("Take screenshots both");
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("TBD: requires capture API integration");
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// ---- Labels de columnas ----
|
||||||
|
float col_w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f));
|
||||||
|
ImGui::Text("v2.1.0 — full renderers (current)");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::SameLine(col_w + ImGui::GetStyle().ItemSpacing.x);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.9f, 0.75f, 0.3f, 1.0f));
|
||||||
|
ImGui::Text("%s", k_version_labels[g_selected_version]);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ---- Side-by-side layout ----
|
||||||
|
SimVersion sim_ver = static_cast<SimVersion>(g_selected_version);
|
||||||
|
float avail_h = ImGui::GetContentRegionAvail().y;
|
||||||
|
|
||||||
|
if (ImGui::BeginTable("##compat_layout", 2,
|
||||||
|
ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchSame)) {
|
||||||
|
ImGui::TableSetupColumn("##col_left", ImGuiTableColumnFlags_None, 1.0f);
|
||||||
|
ImGui::TableSetupColumn("##col_right", ImGuiTableColumnFlags_None, 1.0f);
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
|
||||||
|
// ---------- Panel izquierdo: v2.1 full ----------
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
if (ImGui::BeginChild("##compat_left", ImVec2(-1.0f, avail_h - 4.0f), false)) {
|
||||||
|
data_table::TableInput tbl_left = make_base_table("compat_left");
|
||||||
|
apply_full_specs(tbl_left);
|
||||||
|
|
||||||
|
g_left.last_events.clear();
|
||||||
|
data_table::render("##compat_left_tbl", { tbl_left }, g_left.dt,
|
||||||
|
&g_left.last_events, false);
|
||||||
|
qa::consume_events(g_left.last_events);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// ---------- Panel derecho: versión degradada ----------
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
if (ImGui::BeginChild("##compat_right", ImVec2(-1.0f, avail_h - 4.0f), false)) {
|
||||||
|
data_table::TableInput tbl_right = make_base_table("compat_right");
|
||||||
|
apply_downgraded_specs(tbl_right, sim_ver);
|
||||||
|
|
||||||
|
g_right.last_events.clear();
|
||||||
|
data_table::render("##compat_right_tbl", { tbl_right }, g_right.dt,
|
||||||
|
&g_right.last_events, false);
|
||||||
|
qa::consume_events(g_right.last_events);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
// tab_dots — CellRenderer::Dots demo (fase 2, issue 0108).
|
||||||
|
// 8 servicios x 5 cols: service_name, environment, last_runs (Dots ','),
|
||||||
|
// recent_24h (Dots '|'), uptime_pct (ColorScale).
|
||||||
|
// UI controls: dots_max slider, show_count toggle, glyph_size slider.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
qa::TabState g_st;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Dynamic UI state — mutated by sliders/toggles each frame.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
static int s_dots_max = 20; // both Dots columns
|
||||||
|
static bool s_show_count = true; // last_runs " (N)" suffix
|
||||||
|
static float s_glyph_size = 0.0f; // 0 = default font size
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Backing data: 8 services, 5 cols each.
|
||||||
|
// Cols: service_name | environment | last_runs | recent_24h | uptime_pct
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
void seed() {
|
||||||
|
g_st.back = {
|
||||||
|
// service_name env last_runs (comma-sep) recent_24h (pipe-sep) uptime_pct
|
||||||
|
"api-gateway", "prod", "ok,ok,ok,error,ok,ok,ok,ok,error,ok", "ok|ok|error|ok|ok|ok|ok|ok", "99.1",
|
||||||
|
"auth-service", "prod", "ok,ok,ok,ok,ok,ok,ok,ok,ok,ok", "ok|ok|ok|ok|ok|ok|ok|ok", "100.0",
|
||||||
|
"billing-worker", "prod", "ok,error,error,ok,timeout,ok,ok,error,ok,ok","ok|error|timeout|ok|ok|ok|ok", "94.7",
|
||||||
|
"data-pipeline", "staging", "ok,ok,ok,ok,error,ok", "ok|ok|ok|error|ok", "98.3",
|
||||||
|
"report-gen", "staging", "timeout,ok,ok,ok,error,ok,ok,ok", "timeout|ok|ok|ok|ok", "97.5",
|
||||||
|
"ml-inference", "dev", "ok,ok,error,ok,ok", "ok|error|ok", "96.2",
|
||||||
|
"cache-warmer", "dev", "ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok", "ok|ok|ok|ok|ok|ok", "99.9",
|
||||||
|
"event-consumer", "prod", "error,error,ok,ok,timeout,ok,ok,ok,error,ok","error|ok|ok|timeout|ok|ok", "91.4",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Shared BadgeRule map for both Dots columns.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
std::vector<data_table::BadgeRule> make_status_badges() {
|
||||||
|
return {
|
||||||
|
{"ok", "#22c55e", ""},
|
||||||
|
{"error", "#ef4444", ""},
|
||||||
|
{"timeout", "#f59e0b", ""},
|
||||||
|
{"unknown", "#6b7280", ""},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// render_dots — called every frame from qa_panel.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
void render_dots() {
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// UI controls bar.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
ImGui::PushItemWidth(180.0f);
|
||||||
|
ImGui::SliderInt("dots_max", &s_dots_max, 5, 30);
|
||||||
|
ImGui::SameLine(0, 20);
|
||||||
|
ImGui::Checkbox("show_count (last_runs)", &s_show_count);
|
||||||
|
ImGui::SameLine(0, 20);
|
||||||
|
ImGui::SliderFloat("glyph_size", &s_glyph_size, 0.0f, 12.0f, "%.1f px");
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// TableInput: 8 rows x 5 cols, row-major.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "dots_demo";
|
||||||
|
tbl.headers = {"service_name", "environment", "last_runs", "recent_24h", "uptime_pct"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::String, // service_name
|
||||||
|
data_table::ColumnType::String, // environment → CategoricalChip
|
||||||
|
data_table::ColumnType::String, // last_runs → Dots ','
|
||||||
|
data_table::ColumnType::String, // recent_24h → Dots '|'
|
||||||
|
data_table::ColumnType::Float, // uptime_pct → ColorScale
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 8;
|
||||||
|
tbl.cols = 5;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Column specs.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
tbl.column_specs.resize(tbl.cols);
|
||||||
|
for (int i = 0; i < tbl.cols; ++i)
|
||||||
|
tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
|
||||||
|
// col 0: service_name — default Text, no spec needed.
|
||||||
|
|
||||||
|
// col 1: environment — CategoricalChip
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[1];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"prod", "#ef4444"}, // red
|
||||||
|
{"staging", "#f59e0b"}, // amber
|
||||||
|
{"dev", "#3b82f6"}, // blue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 2: last_runs — Dots with ',' separator, show_count dynamic.
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[2];
|
||||||
|
cs.renderer = data_table::CellRenderer::Dots;
|
||||||
|
cs.badges = make_status_badges();
|
||||||
|
cs.dots_separator = ',';
|
||||||
|
cs.dots_max = s_dots_max;
|
||||||
|
cs.dots_show_count = s_show_count;
|
||||||
|
cs.dots_glyph_size = s_glyph_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 3: recent_24h — Dots with '|' separator, no count suffix.
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Dots;
|
||||||
|
cs.badges = make_status_badges();
|
||||||
|
cs.dots_separator = '|';
|
||||||
|
cs.dots_max = s_dots_max;
|
||||||
|
cs.dots_show_count = false;
|
||||||
|
cs.dots_glyph_size = s_glyph_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// col 4: uptime_pct — ColorScale 0..100.
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 100.0;
|
||||||
|
cs.range_alpha = 0.30f;
|
||||||
|
// Default green→amber→red gradient (empty range_stops).
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Render.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
ImGui::BeginChild("##dots_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##dots_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, qa::toggles().show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
// tab_drill — drill-down stages + breadcrumb demo (issue 0108 fase 2).
|
||||||
|
// Muestra tabla de ventas jerarquica (region/country/product/quarter/revenue).
|
||||||
|
// Stage 0: raw. Stage 1: agrupado por region (Sum revenue).
|
||||||
|
// La UI arranca en stage 1. Right-click en celda de region -> popup nativo
|
||||||
|
// "Drill into" del modulo -> crea stage 2 con filter region == valor.
|
||||||
|
// Breadcrumb (<>) navega back/forward entre stages.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <cstdio> // snprintf
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Datos: 30 filas x 5 columnas (region, country, product, quarter, revenue)
|
||||||
|
// 3 regiones x 3 paises x (Q1..Q4 repartidos) x 3 productos = 30 filas aprox.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct Row {
|
||||||
|
const char* region;
|
||||||
|
const char* country;
|
||||||
|
const char* product;
|
||||||
|
const char* quarter;
|
||||||
|
const char* revenue;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int kRows = 30;
|
||||||
|
constexpr int kCols = 5;
|
||||||
|
|
||||||
|
// Revenues: mezcla de 1000..50000 con variacion por region/producto.
|
||||||
|
static const Row kSalesData[kRows] = {
|
||||||
|
// EMEA
|
||||||
|
{"EMEA", "Germany", "A", "Q1", "12500"},
|
||||||
|
{"EMEA", "Germany", "B", "Q2", "18300"},
|
||||||
|
{"EMEA", "Germany", "C", "Q3", "9700"},
|
||||||
|
{"EMEA", "France", "A", "Q4", "21000"},
|
||||||
|
{"EMEA", "France", "B", "Q1", "14200"},
|
||||||
|
{"EMEA", "France", "C", "Q2", "7800"},
|
||||||
|
{"EMEA", "Spain", "A", "Q3", "11500"},
|
||||||
|
{"EMEA", "Spain", "B", "Q4", "16800"},
|
||||||
|
{"EMEA", "Spain", "C", "Q1", "5400"},
|
||||||
|
{"EMEA", "Germany", "A", "Q2", "19900"},
|
||||||
|
// AMER
|
||||||
|
{"AMER", "USA", "A", "Q1", "45000"},
|
||||||
|
{"AMER", "USA", "B", "Q2", "38500"},
|
||||||
|
{"AMER", "USA", "C", "Q3", "22000"},
|
||||||
|
{"AMER", "Canada", "A", "Q4", "17600"},
|
||||||
|
{"AMER", "Canada", "B", "Q1", "13200"},
|
||||||
|
{"AMER", "Canada", "C", "Q2", "8900"},
|
||||||
|
{"AMER", "Brazil", "A", "Q3", "27400"},
|
||||||
|
{"AMER", "Brazil", "B", "Q4", "31000"},
|
||||||
|
{"AMER", "Brazil", "C", "Q1", "11100"},
|
||||||
|
{"AMER", "USA", "A", "Q2", "49800"},
|
||||||
|
// APAC
|
||||||
|
{"APAC", "Japan", "A", "Q1", "32000"},
|
||||||
|
{"APAC", "Japan", "B", "Q2", "25600"},
|
||||||
|
{"APAC", "Japan", "C", "Q3", "14300"},
|
||||||
|
{"APAC", "Australia", "A", "Q4", "18700"},
|
||||||
|
{"APAC", "Australia", "B", "Q1", "12400"},
|
||||||
|
{"APAC", "Australia", "C", "Q2", "6100"},
|
||||||
|
{"APAC", "Singapore", "A", "Q3", "23800"},
|
||||||
|
{"APAC", "Singapore", "B", "Q4", "29500"},
|
||||||
|
{"APAC", "Singapore", "C", "Q1", "9200"},
|
||||||
|
{"APAC", "Japan", "A", "Q2", "35400"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Backing strings (flat row-major) + ptrs (rebuilt by seed).
|
||||||
|
static std::string g_back[kRows * kCols];
|
||||||
|
static const char* g_ptrs[kRows * kCols];
|
||||||
|
|
||||||
|
// data_table state — persistent across frames.
|
||||||
|
static data_table::State g_dt;
|
||||||
|
static bool g_seeded = false;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// seed — fill g_back + g_ptrs, preset stages, set active_stage = 1.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void seed() {
|
||||||
|
for (int r = 0; r < kRows; ++r) {
|
||||||
|
int base = r * kCols;
|
||||||
|
g_back[base + 0] = kSalesData[r].region;
|
||||||
|
g_back[base + 1] = kSalesData[r].country;
|
||||||
|
g_back[base + 2] = kSalesData[r].product;
|
||||||
|
g_back[base + 3] = kSalesData[r].quarter;
|
||||||
|
g_back[base + 4] = kSalesData[r].revenue;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < kRows * kCols; ++i)
|
||||||
|
g_ptrs[i] = g_back[i].c_str();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Preset stages:
|
||||||
|
// stages[0] = raw (empty Stage — no breakouts, no aggregations).
|
||||||
|
// stages[1] = breakout by region + Sum of revenue.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
g_dt.stages.clear();
|
||||||
|
g_dt.stages.emplace_back(); // Stage 0: raw
|
||||||
|
|
||||||
|
data_table::Stage agg;
|
||||||
|
agg.breakouts = {"region"};
|
||||||
|
{
|
||||||
|
data_table::Aggregation a;
|
||||||
|
a.fn = data_table::AggFn::Sum;
|
||||||
|
a.col = "revenue";
|
||||||
|
a.alias = "revenue_sum";
|
||||||
|
agg.aggregations.push_back(a);
|
||||||
|
}
|
||||||
|
g_dt.stages.push_back(agg); // Stage 1: group by region + sum revenue
|
||||||
|
|
||||||
|
// Start on the aggregated view.
|
||||||
|
g_dt.active_stage = 1;
|
||||||
|
|
||||||
|
// Clear drill history.
|
||||||
|
g_dt.drill_back.clear();
|
||||||
|
g_dt.drill_forward.clear();
|
||||||
|
|
||||||
|
g_seeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// render_drill
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void render_drill() {
|
||||||
|
if (!g_seeded) seed();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// UI controls above the table.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
if (ImGui::SmallButton("Reset to grouped (stage 1)")) {
|
||||||
|
// Keep only stage 0 + stage 1; drop any drill stages.
|
||||||
|
if (g_dt.stages.size() > 2) {
|
||||||
|
g_dt.stages.resize(2);
|
||||||
|
}
|
||||||
|
g_dt.active_stage = 1;
|
||||||
|
g_dt.drill_back.clear();
|
||||||
|
g_dt.drill_forward.clear();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("Reset to raw (stage 0)")) {
|
||||||
|
g_dt.active_stage = 0;
|
||||||
|
g_dt.drill_back.clear();
|
||||||
|
g_dt.drill_forward.clear();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled(
|
||||||
|
"Right-click any region cell -> 'Drill into' to filter down. "
|
||||||
|
"Use breadcrumb < > to navigate back/forward.");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Build TableInput.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "sales";
|
||||||
|
tbl.headers = {"region", "country", "product", "quarter", "revenue"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
};
|
||||||
|
tbl.cells = g_ptrs;
|
||||||
|
tbl.rows = kRows;
|
||||||
|
tbl.cols = kCols;
|
||||||
|
|
||||||
|
// Renderers: CategoricalChip on region, ColorScale on revenue.
|
||||||
|
tbl.column_specs.resize(kCols);
|
||||||
|
for (int i = 0; i < kCols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
|
||||||
|
// region: CategoricalChip — one dot per region for fast scanning.
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[0];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"EMEA", "#3b82f6"}, // blue
|
||||||
|
{"AMER", "#22c55e"}, // green
|
||||||
|
{"APAC", "#f59e0b"}, // amber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// revenue: ColorScale green→amber→red over [0, 50000].
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 50000.0;
|
||||||
|
cs.range_alpha = 0.30f;
|
||||||
|
// Default 3-stop gradient (green→amber→red) via empty range_stops.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Render.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
ImGui::BeginChild("##drill_host", ImVec2(-1, -1));
|
||||||
|
data_table::render("##drill_tbl", {tbl}, g_dt, nullptr,
|
||||||
|
qa::toggles().show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
// tab_events — inyector de TableEvents sinteticos para auto-test del event sink.
|
||||||
|
// Issue 0108 fase 2.
|
||||||
|
//
|
||||||
|
// Patron: se construye un vector<TableEvent> LOCAL y se llama qa::consume_events
|
||||||
|
// directamente — sin worker thread. Los counters de qa::counters() se incrementan
|
||||||
|
// como si el usuario hubiera clickado. Ademas se renderiza la tabla real para que
|
||||||
|
// eventos reales y sinteticos cuenten igual.
|
||||||
|
//
|
||||||
|
// Inspirado en altsnap_jitter_test: fakear eventos de bajo nivel directamente
|
||||||
|
// sobre el estado interno del subsistema, sin depender de la UI.
|
||||||
|
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
qa::TabState g_st;
|
||||||
|
int g_events_injected_total = 0;
|
||||||
|
char g_burst_msg[128] = "";
|
||||||
|
|
||||||
|
void seed() {
|
||||||
|
// 6 filas x 5 columnas: id, name, status (CategoricalChip), action (Button), value (Float)
|
||||||
|
g_st.back = {
|
||||||
|
// id name status action value
|
||||||
|
"1", "task-alpha", "ok", "Activate", "1.25",
|
||||||
|
"2", "task-beta", "error", "Activate", "0.50",
|
||||||
|
"3", "task-gamma", "running", "Activate", "3.14",
|
||||||
|
"4", "task-delta", "pending", "Activate", "2.71",
|
||||||
|
"5", "task-epsilon", "ok", "Activate", "0.99",
|
||||||
|
"6", "task-zeta", "error", "Activate", "4.20",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: construye un ButtonClick sintetico y lo consume.
|
||||||
|
void inject_button_click(const char* action_id, int row) {
|
||||||
|
data_table::TableEvent ev;
|
||||||
|
ev.kind = data_table::TableEventKind::ButtonClick;
|
||||||
|
ev.row = row;
|
||||||
|
ev.col = 3;
|
||||||
|
ev.column_id = "action";
|
||||||
|
ev.action_id = action_id;
|
||||||
|
ev.value = "Activate";
|
||||||
|
|
||||||
|
std::vector<data_table::TableEvent> batch = {ev};
|
||||||
|
qa::consume_events(batch);
|
||||||
|
g_events_injected_total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: construye un RowDoubleClick sintetico y lo consume.
|
||||||
|
void inject_row_double_click(int row) {
|
||||||
|
data_table::TableEvent ev;
|
||||||
|
ev.kind = data_table::TableEventKind::RowDoubleClick;
|
||||||
|
ev.row = row;
|
||||||
|
ev.col = -1;
|
||||||
|
|
||||||
|
std::vector<data_table::TableEvent> batch = {ev};
|
||||||
|
qa::consume_events(batch);
|
||||||
|
g_events_injected_total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: construye un RowRightClick sintetico y lo consume.
|
||||||
|
void inject_row_right_click(int row) {
|
||||||
|
data_table::TableEvent ev;
|
||||||
|
ev.kind = data_table::TableEventKind::RowRightClick;
|
||||||
|
ev.row = row;
|
||||||
|
ev.col = -1;
|
||||||
|
|
||||||
|
std::vector<data_table::TableEvent> batch = {ev};
|
||||||
|
qa::consume_events(batch);
|
||||||
|
g_events_injected_total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void render_events() {
|
||||||
|
// 1. Seed si vacio
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
|
||||||
|
const auto& c = qa::counters();
|
||||||
|
|
||||||
|
// 2. Panel de control — inyectores sinteticos
|
||||||
|
ImGui::Text("Synthetic event injectors:");
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// Fila 1: ButtonClick injectors
|
||||||
|
if (ImGui::Button("Inject ButtonClick (retry) row=0")) {
|
||||||
|
inject_button_click("retry", 0);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Inject ButtonClick (cancel) row=1")) {
|
||||||
|
inject_button_click("cancel", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila 2: Row events
|
||||||
|
if (ImGui::Button("Inject RowDoubleClick row=2")) {
|
||||||
|
inject_row_double_click(2);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Inject RowRightClick row=3")) {
|
||||||
|
inject_row_right_click(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila 3: Burst + reset
|
||||||
|
if (ImGui::Button("Inject 100x ButtonClick burst")) {
|
||||||
|
std::vector<data_table::TableEvent> burst;
|
||||||
|
burst.reserve(100);
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
data_table::TableEvent ev;
|
||||||
|
ev.kind = data_table::TableEventKind::ButtonClick;
|
||||||
|
ev.row = i % 6;
|
||||||
|
ev.col = 3;
|
||||||
|
ev.column_id = "action";
|
||||||
|
ev.action_id = "activate";
|
||||||
|
ev.value = "Activate";
|
||||||
|
burst.push_back(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
qa::consume_events(burst);
|
||||||
|
auto t1 = std::chrono::steady_clock::now();
|
||||||
|
long long us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||||
|
|
||||||
|
g_events_injected_total += 100;
|
||||||
|
snprintf(g_burst_msg, sizeof(g_burst_msg),
|
||||||
|
"100 events processed in %lld us", us);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Reset counters")) {
|
||||||
|
auto& mut = qa::counters();
|
||||||
|
mut.button_clicks = 0;
|
||||||
|
mut.row_double_click = 0;
|
||||||
|
mut.row_right_click = 0;
|
||||||
|
g_events_injected_total = 0;
|
||||||
|
g_burst_msg[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila 4: mensajes de estado
|
||||||
|
if (g_burst_msg[0] != '\0') {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s", g_burst_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// 3. Counters vivos
|
||||||
|
ImGui::Text("Global counters — button_clicks: %d row_double_click: %d row_right_click: %d",
|
||||||
|
c.button_clicks, c.row_double_click, c.row_right_click);
|
||||||
|
ImGui::Text("events_injected_total (esta tab): %d", g_events_injected_total);
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// 4. Tabla real — eventos reales del usuario tambien cuentan
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "events";
|
||||||
|
tbl.headers = {"id", "name", "status", "action", "value"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 6;
|
||||||
|
tbl.cols = 5;
|
||||||
|
|
||||||
|
tbl.column_specs.resize(tbl.cols);
|
||||||
|
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
|
||||||
|
// status: CategoricalChip
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[2];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"ok", "#22c55e"},
|
||||||
|
{"error", "#ef4444"},
|
||||||
|
{"running", "#f59e0b"},
|
||||||
|
{"pending", "#a3a3a3"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// action: Button
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Button;
|
||||||
|
cs.button_action = "activate";
|
||||||
|
cs.button_color_hex = "#3b82f6";
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginChild("##events_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##events_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, qa::toggles().show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// 5. Consumir eventos reales (incrementa los mismos counters globales)
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
// tab_joins — 2 tablas (orders + customers) pasadas a data_table::render.
|
||||||
|
// Demuestra la UI de joins del modulo: drag 'customers' chip → canvas.
|
||||||
|
// Issue 0108 fase 2.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Backing storage — dos tablas independientes.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct JoinsState {
|
||||||
|
// orders: order_id, customer_id, amount, status
|
||||||
|
std::vector<std::string> orders_back = {
|
||||||
|
"1001", "C01", "250.00", "shipped",
|
||||||
|
"1002", "C02", "89.99", "pending",
|
||||||
|
"1003", "C01", "540.50", "delivered",
|
||||||
|
"1004", "C03", "12.00", "cancelled",
|
||||||
|
"1005", "C04", "310.75", "shipped",
|
||||||
|
"1006", "C02", "75.00", "pending",
|
||||||
|
"1007", "C05", "1200.00", "delivered",
|
||||||
|
"1008", "C01", "43.20", "shipped",
|
||||||
|
"1009", "C03", "920.00", "processing",
|
||||||
|
"1010", "C04", "67.80", "cancelled",
|
||||||
|
};
|
||||||
|
std::vector<const char*> orders_ptrs;
|
||||||
|
|
||||||
|
// customers: customer_id, name, country, vip_level
|
||||||
|
std::vector<std::string> customers_back = {
|
||||||
|
"C01", "Alice Romero", "ES", "gold",
|
||||||
|
"C02", "Bob Chen", "US", "silver",
|
||||||
|
"C03", "Carla Müller", "DE", "bronze",
|
||||||
|
"C04", "David Okafor", "NG", "gold",
|
||||||
|
"C05", "Eva Lindqvist", "SE", "silver",
|
||||||
|
};
|
||||||
|
std::vector<const char*> customers_ptrs;
|
||||||
|
|
||||||
|
data_table::State dt;
|
||||||
|
std::vector<data_table::TableEvent> last_events;
|
||||||
|
|
||||||
|
bool seeded = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
JoinsState g_jst;
|
||||||
|
|
||||||
|
void seed(JoinsState& s) {
|
||||||
|
s.orders_ptrs.clear();
|
||||||
|
for (auto& v : s.orders_back) s.orders_ptrs.push_back(v.c_str());
|
||||||
|
s.customers_ptrs.clear();
|
||||||
|
for (auto& v : s.customers_back) s.customers_ptrs.push_back(v.c_str());
|
||||||
|
s.seeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void render_joins() {
|
||||||
|
if (!g_jst.seeded) seed(g_jst);
|
||||||
|
|
||||||
|
// --- Controls bar -------------------------------------------------------
|
||||||
|
ImGui::TextDisabled(
|
||||||
|
"Drag 'customers' table from chips bar into the canvas to join.");
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// JoinStrategy selector — display only; the module handles the actual join.
|
||||||
|
static int s_strategy = 0; // 0=Left 1=Inner 2=Right 3=Full
|
||||||
|
const char* strategies[] = { "Left", "Inner", "Right", "Full" };
|
||||||
|
ImGui::SetNextItemWidth(120.0f);
|
||||||
|
ImGui::Combo("Join strategy", &s_strategy, strategies, 4);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("(display only — use the module join UI to apply)");
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (ImGui::Button("Reset join state")) {
|
||||||
|
g_jst.dt = data_table::State{};
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// --- Build TableInputs --------------------------------------------------
|
||||||
|
|
||||||
|
// Main table: orders
|
||||||
|
data_table::TableInput orders_tbl;
|
||||||
|
orders_tbl.name = "orders";
|
||||||
|
orders_tbl.headers = { "order_id", "customer_id", "amount", "status" };
|
||||||
|
orders_tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
};
|
||||||
|
orders_tbl.cells = g_jst.orders_ptrs.data();
|
||||||
|
orders_tbl.rows = 10;
|
||||||
|
orders_tbl.cols = 4;
|
||||||
|
orders_tbl.column_specs.resize(orders_tbl.cols);
|
||||||
|
for (int i = 0; i < orders_tbl.cols; i++)
|
||||||
|
orders_tbl.column_specs[i].id = orders_tbl.headers[i];
|
||||||
|
|
||||||
|
// Secondary table: customers (joinable)
|
||||||
|
data_table::TableInput customers_tbl;
|
||||||
|
customers_tbl.name = "customers";
|
||||||
|
customers_tbl.headers = { "customer_id", "name", "country", "vip_level" };
|
||||||
|
customers_tbl.types = {
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
};
|
||||||
|
customers_tbl.cells = g_jst.customers_ptrs.data();
|
||||||
|
customers_tbl.rows = 5;
|
||||||
|
customers_tbl.cols = 4;
|
||||||
|
customers_tbl.column_specs.resize(customers_tbl.cols);
|
||||||
|
for (int i = 0; i < customers_tbl.cols; i++)
|
||||||
|
customers_tbl.column_specs[i].id = customers_tbl.headers[i];
|
||||||
|
|
||||||
|
// --- Render -------------------------------------------------------------
|
||||||
|
ImGui::BeginChild("##joins_host", ImVec2(-1, -1));
|
||||||
|
g_jst.last_events.clear();
|
||||||
|
data_table::render("##joins_tbl",
|
||||||
|
{ orders_tbl, customers_tbl },
|
||||||
|
g_jst.dt,
|
||||||
|
&g_jst.last_events,
|
||||||
|
/*show_chrome=*/true);
|
||||||
|
ImGui::EndChild();
|
||||||
|
qa::consume_events(g_jst.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// tab_renderers — 1 columna por cada CellRenderer.
|
||||||
|
// Sirve como referencia visual de TODOS los renderers en una misma tabla.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
qa::TabState g_st;
|
||||||
|
|
||||||
|
void seed() {
|
||||||
|
g_st.back = {
|
||||||
|
"1","ok", "0.75","350", "ok", "Run", "ok,ok,error", "go", "1500",
|
||||||
|
"2","error", "0.30","1800","error", "Stop", "ok,error,ok", "py", "4200",
|
||||||
|
"3","running","0.55","750", "pending","Cancel","ok,ok,ok,ok", "cpp","950",
|
||||||
|
"4","pending","0.10","2500","ok", "Retry", "error,error,ok", "go", "5500",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void render_renderers() {
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
const auto& t = qa::toggles();
|
||||||
|
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "renderers";
|
||||||
|
tbl.headers = {"id", "badge", "progress", "duration_ms", "icon",
|
||||||
|
"button", "dots", "categorical", "color_scale"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Int,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 4;
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (t.enable_badges) {
|
||||||
|
auto& cs = tbl.column_specs[1];
|
||||||
|
cs.renderer = data_table::CellRenderer::Badge;
|
||||||
|
cs.badges = {
|
||||||
|
{"ok", "#22c55e", ""},
|
||||||
|
{"error", "#ef4444", "ERR"},
|
||||||
|
{"running", "#f59e0b", ""},
|
||||||
|
{"pending", "#a3a3a3", ""},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[2];
|
||||||
|
cs.renderer = data_table::CellRenderer::Progress;
|
||||||
|
cs.progress_scale_100 = false;
|
||||||
|
cs.progress_color_hex = "#3b82f6";
|
||||||
|
}
|
||||||
|
if (t.enable_duration) {
|
||||||
|
auto& cs = tbl.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Duration;
|
||||||
|
cs.duration_warn_ms = 1000.0f;
|
||||||
|
cs.duration_error_ms = 5000.0f;
|
||||||
|
}
|
||||||
|
if (t.enable_icon) {
|
||||||
|
auto& cs = tbl.column_specs[4];
|
||||||
|
cs.renderer = data_table::CellRenderer::Icon;
|
||||||
|
cs.icon_map = {
|
||||||
|
{"ok", "TI_CHECK", "#22c55e"},
|
||||||
|
{"error", "TI_X", "#ef4444"},
|
||||||
|
{"pending", "TI_CIRCLE", "#a3a3a3"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (t.enable_buttons) {
|
||||||
|
auto& cs = tbl.column_specs[5];
|
||||||
|
cs.renderer = data_table::CellRenderer::Button;
|
||||||
|
cs.button_action = "demo_action";
|
||||||
|
cs.button_color_hex = "#3b82f6";
|
||||||
|
}
|
||||||
|
if (t.enable_dots) {
|
||||||
|
auto& cs = tbl.column_specs[6];
|
||||||
|
cs.renderer = data_table::CellRenderer::Dots;
|
||||||
|
cs.badges = {
|
||||||
|
{"ok", "#22c55e", ""},
|
||||||
|
{"error", "#ef4444", ""},
|
||||||
|
};
|
||||||
|
cs.dots_separator = ',';
|
||||||
|
cs.dots_show_count = false;
|
||||||
|
}
|
||||||
|
if (t.enable_categorical) {
|
||||||
|
auto& cs = tbl.column_specs[7];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"go", "#3b82f6"},
|
||||||
|
{"py", "#22c55e"},
|
||||||
|
{"cpp", "#a855f7"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (t.enable_color_scale) {
|
||||||
|
auto& cs = tbl.column_specs[8];
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 6000.0;
|
||||||
|
cs.range_alpha = 0.30f;
|
||||||
|
}
|
||||||
|
if (t.enable_tooltip) {
|
||||||
|
tbl.column_specs[3].tooltip = "auto";
|
||||||
|
tbl.column_specs[3].tooltip_on_hover = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginChild("##renderers_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##renderers_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, t.show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
// tab_tql — TQL pipeline preset + Ask AI panel demo.
|
||||||
|
// Demuestra:
|
||||||
|
// - Tabla "events" (20 filas, 5 columnas) con renderers declarativos.
|
||||||
|
// - Preset buttons: filter errors only / group by service + count / reset.
|
||||||
|
// - TextDisabled explicando chips bar + Ask AI NL→TQL modal.
|
||||||
|
// Issue 0108 fase 2.
|
||||||
|
#include "tabs.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
qa::TabState g_st;
|
||||||
|
|
||||||
|
// 20 filas x 5 columnas: timestamp, service, event_type, duration_ms, user_country
|
||||||
|
// Columnas: idx 0=timestamp, 1=service, 2=event_type, 3=duration_ms, 4=user_country
|
||||||
|
void seed() {
|
||||||
|
g_st.back = {
|
||||||
|
// timestamp service event_type duration_ms user_country
|
||||||
|
"2024-01-15T08:01:00Z", "auth", "login", "45", "ES",
|
||||||
|
"2024-01-15T08:01:12Z", "api", "success", "120", "US",
|
||||||
|
"2024-01-15T08:01:34Z", "db", "timeout", "1500", "FR",
|
||||||
|
"2024-01-15T08:02:01Z", "web", "error", "2300", "DE",
|
||||||
|
"2024-01-15T08:02:15Z", "auth", "login", "55", "JP",
|
||||||
|
"2024-01-15T08:02:45Z", "api", "error", "980", "ES",
|
||||||
|
"2024-01-15T08:03:00Z", "db", "success", "88", "US",
|
||||||
|
"2024-01-15T08:03:22Z", "web", "login", "210", "FR",
|
||||||
|
"2024-01-15T08:03:50Z", "auth", "timeout", "3200", "DE",
|
||||||
|
"2024-01-15T08:04:10Z", "api", "success", "67", "JP",
|
||||||
|
"2024-01-15T08:04:30Z", "db", "error", "4100", "ES",
|
||||||
|
"2024-01-15T08:04:55Z", "web", "success", "155", "US",
|
||||||
|
"2024-01-15T08:05:15Z", "auth", "error", "1900", "FR",
|
||||||
|
"2024-01-15T08:05:40Z", "api", "login", "38", "DE",
|
||||||
|
"2024-01-15T08:06:05Z", "db", "success", "95", "JP",
|
||||||
|
"2024-01-15T08:06:30Z", "web", "timeout", "2750", "ES",
|
||||||
|
"2024-01-15T08:07:00Z", "auth", "success", "72", "US",
|
||||||
|
"2024-01-15T08:07:25Z", "api", "error", "1100", "FR",
|
||||||
|
"2024-01-15T08:07:50Z", "db", "login", "41", "DE",
|
||||||
|
"2024-01-15T08:08:15Z", "web", "success", "180", "JP",
|
||||||
|
};
|
||||||
|
qa::rebuild_ptrs(g_st);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
void render_tql() {
|
||||||
|
if (g_st.back.empty()) seed();
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Controls row: preset buttons + reset
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
if (ImGui::SmallButton("Preset: filter errors only")) {
|
||||||
|
// Ensure stage 0 exists
|
||||||
|
g_st.dt.ensure_stage0();
|
||||||
|
// Clear existing filters on stage 0 and add error filter
|
||||||
|
// event_type is column index 2
|
||||||
|
g_st.dt.stages[0].filters.clear();
|
||||||
|
data_table::Filter f;
|
||||||
|
f.col = 2;
|
||||||
|
f.op = data_table::Op::Eq;
|
||||||
|
f.value = "error";
|
||||||
|
g_st.dt.stages[0].filters.push_back(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::SmallButton("Preset: group by service + count")) {
|
||||||
|
// Stage 1 aggregates stage 0 output: breakout by "service", count rows
|
||||||
|
g_st.dt.ensure_stage0();
|
||||||
|
// Remove any existing stage 1+ and add a fresh aggregation stage
|
||||||
|
if ((int)g_st.dt.stages.size() > 1)
|
||||||
|
g_st.dt.stages.resize(1);
|
||||||
|
data_table::Stage agg;
|
||||||
|
agg.breakouts.push_back("service");
|
||||||
|
data_table::Aggregation cnt;
|
||||||
|
cnt.fn = data_table::AggFn::Count;
|
||||||
|
cnt.alias = "count";
|
||||||
|
agg.aggregations.push_back(cnt);
|
||||||
|
g_st.dt.stages.push_back(std::move(agg));
|
||||||
|
// Activate the aggregation stage
|
||||||
|
g_st.dt.active_stage = (int)g_st.dt.stages.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::SmallButton("Reset pipeline")) {
|
||||||
|
g_st.dt.stages.clear();
|
||||||
|
g_st.dt.active_stage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("Use chips bar to build pipeline interactively. "
|
||||||
|
"Ask AI button in chips bar opens NL\xe2\x86\x92TQL modal "
|
||||||
|
"(requires ANTHROPIC_API_KEY env).");
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Table
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "events";
|
||||||
|
tbl.headers = {"timestamp", "service", "event_type", "duration_ms", "user_country"};
|
||||||
|
tbl.types = {
|
||||||
|
data_table::ColumnType::Date,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
data_table::ColumnType::Float,
|
||||||
|
data_table::ColumnType::String,
|
||||||
|
};
|
||||||
|
tbl.cells = g_st.ptrs.data();
|
||||||
|
tbl.rows = 20;
|
||||||
|
tbl.cols = 5;
|
||||||
|
|
||||||
|
tbl.column_specs.resize(tbl.cols);
|
||||||
|
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||||
|
|
||||||
|
// service (col 1) → CategoricalChip
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[1];
|
||||||
|
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||||
|
cs.chips = {
|
||||||
|
{"auth", "#3b82f6"},
|
||||||
|
{"api", "#22c55e"},
|
||||||
|
{"db", "#a855f7"},
|
||||||
|
{"web", "#f59e0b"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// event_type (col 2) → Badge
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[2];
|
||||||
|
cs.renderer = data_table::CellRenderer::Badge;
|
||||||
|
cs.badges = {
|
||||||
|
{"login", "#3b82f6", ""},
|
||||||
|
{"success", "#22c55e", ""},
|
||||||
|
{"timeout", "#f59e0b", ""},
|
||||||
|
{"error", "#ef4444", "ERR"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// duration_ms (col 3) → Duration with warn=200, error=1000
|
||||||
|
{
|
||||||
|
auto& cs = tbl.column_specs[3];
|
||||||
|
cs.renderer = data_table::CellRenderer::Duration;
|
||||||
|
cs.duration_warn_ms = 200.0f;
|
||||||
|
cs.duration_error_ms = 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginChild("##tql_host", ImVec2(-1, -1));
|
||||||
|
g_st.last_events.clear();
|
||||||
|
data_table::render("##tql_tbl", {tbl}, g_st.dt,
|
||||||
|
&g_st.last_events, qa::toggles().show_chrome);
|
||||||
|
ImGui::EndChild();
|
||||||
|
qa::consume_events(g_st.last_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
// tabs.h — entry points de cada tab del testbed.
|
||||||
|
//
|
||||||
|
// Cada tab vive en SU PROPIO `tab_<X>.cpp`. main.cpp solo orquesta tab bar.
|
||||||
|
// Mismo namespace `tables_qa::tabs` para minimizar collisions.
|
||||||
|
|
||||||
|
namespace tables_qa::tabs {
|
||||||
|
|
||||||
|
// Implementadas (fase 1)
|
||||||
|
void render_basic();
|
||||||
|
void render_renderers();
|
||||||
|
|
||||||
|
// Fase 2 — una funcion por tab. Cada agente fn-constructor implementa SU
|
||||||
|
// funcion en `tab_<X>.cpp` siguiendo el patron de tab_basic.cpp y
|
||||||
|
// tab_renderers.cpp. NO comparten estado entre si: cada uno su TabState
|
||||||
|
// estatico local + sus chips/badges/event handlers.
|
||||||
|
void render_buttons(); // CellRenderer::Button + events Retry/Cancel/Inspect
|
||||||
|
void render_color_rules(); // ColorScale numerico + CategoricalChip runtime
|
||||||
|
void render_dots(); // CellRenderer::Dots — status timeline
|
||||||
|
void render_joins(); // 2 tablas + JoinStrategy Left/Inner/Right/Full
|
||||||
|
void render_tql(); // TQL pipeline + Ask AI
|
||||||
|
void render_drill(); // Drill stages + breadcrumb
|
||||||
|
void render_events(); // Inyector via worker thread (modelo altsnap)
|
||||||
|
void render_compat(); // Version selector + side-by-side downgrade
|
||||||
|
|
||||||
|
} // namespace tables_qa::tabs
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
// test_suite — smoke tests in-process sobre la API publica de data_table.
|
||||||
|
//
|
||||||
|
// Estos tests NO requieren imgui_test_engine. Solo verifican que las
|
||||||
|
// estructuras se construyen, los enums son consistentes y los CellRenderer
|
||||||
|
// configurados producen tablas con `column_specs` validos.
|
||||||
|
//
|
||||||
|
// Para tests UI-level con clicks reales: ver fase 2 del issue 0108 con
|
||||||
|
// imgui_test_engine + FN_BUILD_TESTS=ON.
|
||||||
|
#include "test_suite.h"
|
||||||
|
#include "qa_state.h"
|
||||||
|
#include "data_table/data_table.h"
|
||||||
|
#include "core/data_table_types.h"
|
||||||
|
#include "core/logger.h"
|
||||||
|
|
||||||
|
namespace tables_qa {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct TestResult { const char* name; bool pass; };
|
||||||
|
|
||||||
|
#define TEST(NAME, EXPR) do { \
|
||||||
|
bool _ok = (EXPR); \
|
||||||
|
results.push_back({NAME, _ok}); \
|
||||||
|
if (!_ok) fn_log::log_warn("test failed: %s", NAME); \
|
||||||
|
else fn_log::log_info("test pass: %s", NAME); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
bool run_test_suite() {
|
||||||
|
fn_log::log_info("=== tables_qa: test suite start ===");
|
||||||
|
std::vector<TestResult> results;
|
||||||
|
|
||||||
|
// --- Test 1: TableInput basic construction ---
|
||||||
|
{
|
||||||
|
data_table::TableInput tbl;
|
||||||
|
tbl.name = "t1";
|
||||||
|
tbl.headers = {"a", "b"};
|
||||||
|
tbl.types = {data_table::ColumnType::Int, data_table::ColumnType::String};
|
||||||
|
tbl.rows = 0;
|
||||||
|
tbl.cols = 2;
|
||||||
|
TEST("TableInput construct", tbl.cols == 2 && tbl.headers.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 2: ColumnSpec each renderer enum value ---
|
||||||
|
{
|
||||||
|
bool all_ok = true;
|
||||||
|
for (int r = 0; r <= 10; r++) {
|
||||||
|
if (r == 6 || r == 7) continue; // reserved
|
||||||
|
data_table::ColumnSpec cs;
|
||||||
|
cs.renderer = static_cast<data_table::CellRenderer>(r);
|
||||||
|
cs.id = "col";
|
||||||
|
(void)cs; // construct OK
|
||||||
|
}
|
||||||
|
TEST("ColumnSpec enum construct", all_ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 3: BadgeRule + ChipRule consistent ---
|
||||||
|
{
|
||||||
|
data_table::BadgeRule b{"ok", "#22c55e", "OK"};
|
||||||
|
data_table::ChipRule c{"ok", "#22c55e"};
|
||||||
|
TEST("BadgeRule fields", b.value == "ok" && b.color_hex == "#22c55e");
|
||||||
|
TEST("ChipRule fields", c.match == "ok" && c.color == "#22c55e");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 4: State default-constructible + persistent ---
|
||||||
|
{
|
||||||
|
data_table::State st1;
|
||||||
|
data_table::State st2;
|
||||||
|
// Different instances; no shared state contamination
|
||||||
|
TEST("State default construct", true);
|
||||||
|
(void)st1; (void)st2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 5: TableEvent enums ---
|
||||||
|
{
|
||||||
|
data_table::TableEvent ev;
|
||||||
|
ev.kind = data_table::TableEventKind::ButtonClick;
|
||||||
|
ev.action_id = "test";
|
||||||
|
TEST("TableEvent ButtonClick",
|
||||||
|
ev.kind == data_table::TableEventKind::ButtonClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 6: ColorScale config plausible ---
|
||||||
|
{
|
||||||
|
data_table::ColumnSpec cs;
|
||||||
|
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||||
|
cs.range_min = 0.0;
|
||||||
|
cs.range_max = 100.0;
|
||||||
|
cs.range_alpha = 0.25f;
|
||||||
|
TEST("ColorScale spec", cs.range_max > cs.range_min);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 7: Duration thresholds order ---
|
||||||
|
{
|
||||||
|
data_table::ColumnSpec cs;
|
||||||
|
cs.renderer = data_table::CellRenderer::Duration;
|
||||||
|
cs.duration_warn_ms = 1000.0f;
|
||||||
|
cs.duration_error_ms = 5000.0f;
|
||||||
|
TEST("Duration thresholds", cs.duration_warn_ms < cs.duration_error_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 8: Button action_id non-empty when renderer Button ---
|
||||||
|
{
|
||||||
|
data_table::ColumnSpec cs;
|
||||||
|
cs.renderer = data_table::CellRenderer::Button;
|
||||||
|
cs.button_action = "retry";
|
||||||
|
cs.button_label = "Retry";
|
||||||
|
TEST("Button setup", !cs.button_action.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 9: Multiple TableInputs in single render call (joins setup) ---
|
||||||
|
{
|
||||||
|
data_table::TableInput main_t, lookup_t;
|
||||||
|
main_t.name = "main"; main_t.cols = 2; main_t.rows = 0;
|
||||||
|
lookup_t.name = "lookup"; lookup_t.cols = 2; lookup_t.rows = 0;
|
||||||
|
std::vector<data_table::TableInput> tables = {main_t, lookup_t};
|
||||||
|
TEST("Multi-table input", tables.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test 10: ColorStop ordering ---
|
||||||
|
{
|
||||||
|
std::vector<data_table::ColorStop> stops = {
|
||||||
|
{0.0f, "#22c55e"},
|
||||||
|
{0.5f, "#f59e0b"},
|
||||||
|
{1.0f, "#ef4444"},
|
||||||
|
};
|
||||||
|
TEST("ColorStop ordering",
|
||||||
|
stops[0].position < stops[1].position &&
|
||||||
|
stops[1].position < stops[2].position);
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = (int)results.size();
|
||||||
|
int passed = 0, failed = 0;
|
||||||
|
for (const auto& r : results) {
|
||||||
|
if (r.pass) passed++; else failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& c = qa::counters();
|
||||||
|
c.last_test_total = total;
|
||||||
|
c.last_test_passed = passed;
|
||||||
|
c.last_test_failed = failed;
|
||||||
|
|
||||||
|
fn_log::log_info("=== test suite done: %d/%d pass, %d failed ===",
|
||||||
|
passed, total, failed);
|
||||||
|
return failed == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tables_qa
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
// test_suite — boton "Run Tests" del QA panel.
|
||||||
|
//
|
||||||
|
// Suite de smoke tests sobre la API publica del modulo `data_table`. Ejecutable
|
||||||
|
// in-process (NO requiere relanzar la app). Resultados se exponen via
|
||||||
|
// qa::counters().last_test_*.
|
||||||
|
//
|
||||||
|
// Fase 1: smoke tests basicos (estructuras inicializan OK, renders no crashean,
|
||||||
|
// events emitidos cuando se simulan). Fase 2 con imgui_test_engine puede
|
||||||
|
// drive UI completa.
|
||||||
|
|
||||||
|
namespace tables_qa {
|
||||||
|
|
||||||
|
// Ejecuta toda la suite. Actualiza qa::counters().last_test_*.
|
||||||
|
// Retorna true si todos pasan.
|
||||||
|
bool run_test_suite();
|
||||||
|
|
||||||
|
} // namespace tables_qa
|
||||||
Reference in New Issue
Block a user