merge navegator_dashboard table migration (issue 0081-J)
4 tablas migradas a data_table::render con CellRenderers Fase 1 (Badge, Duration). Linux build: app es Windows-only (CMakeLists return() en Linux, no genera target). Windows build: OK, 16MB, desplegado a Desktop/apps/navegator_dashboard/. fn doctor cpp-apps: OK (0 BeginTable inline restantes).
This commit is contained in:
@@ -29,6 +29,12 @@ target_link_libraries(navegator_dashboard PRIVATE
|
||||
imgui_node_editor
|
||||
)
|
||||
|
||||
# fn_table_viz: provides data_table::render(), viz_render, TQL engine, Lua, LLM.
|
||||
# Guard keeps the app compilable in builds where vendor/lua is absent.
|
||||
if(TARGET fn_table_viz)
|
||||
target_link_libraries(navegator_dashboard PRIVATE fn_table_viz)
|
||||
endif()
|
||||
|
||||
set_target_properties(navegator_dashboard PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
|
||||
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
|
||||
@@ -51,6 +57,9 @@ if(FN_BUILD_TESTS)
|
||||
ws2_32
|
||||
imgui_node_editor
|
||||
)
|
||||
if(TARGET fn_table_viz)
|
||||
target_link_libraries(navegator_dashboard_tests PRIVATE fn_table_viz)
|
||||
endif()
|
||||
# Excluye int main() de main.cpp; el harness define su propio main().
|
||||
target_compile_definitions(navegator_dashboard_tests PRIVATE FN_TEST_BUILD)
|
||||
# Subsistema consola (no WIN32_EXECUTABLE) para ver output de los tests.
|
||||
|
||||
@@ -4,7 +4,19 @@ lang: cpp
|
||||
domain: tools
|
||||
description: "Cuadro de mandos para gestionar instancias Chrome con remote debugging. Lista navegadores corriendo (visibles + headless), permite lanzar/matar perfiles, inspeccionar pestañas, ejecutar JS, ver peticiones de red. Puente WSL→Windows que centraliza el control que hoy hacemos por scripts dispersos."
|
||||
tags: [imgui, browser, cdp, dashboard, windows, navegator]
|
||||
uses_functions: []
|
||||
uses_functions:
|
||||
- data_table_cpp_viz
|
||||
- viz_render_cpp_viz
|
||||
- compute_stage_cpp_core
|
||||
- compute_pipeline_cpp_core
|
||||
- tql_emit_cpp_core
|
||||
- tql_apply_cpp_core
|
||||
- lua_engine_cpp_core
|
||||
- join_tables_cpp_core
|
||||
- auto_detect_type_cpp_core
|
||||
- compute_column_stats_cpp_core
|
||||
- llm_anthropic_cpp_core
|
||||
- tql_to_sql_cpp_core
|
||||
uses_types: []
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
|
||||
+363
-217
@@ -5,11 +5,15 @@
|
||||
// - Network : panel DevTools-like — tabla + filtros + detalle por tab
|
||||
//
|
||||
// Estado cross-panel via g_session() (session_state.h).
|
||||
// Issue 0081-J: tablas ##browsers, ##tabs, ##wsframes, ##requests migradas a
|
||||
// data_table::render() con CellRenderers declarativos (Badge, Duration).
|
||||
|
||||
#include "imgui.h"
|
||||
#include "implot.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/data_table_types.h"
|
||||
#include "viz/data_table.h"
|
||||
|
||||
#include "chrome_scanner.h"
|
||||
#include "chrome_launcher.h"
|
||||
@@ -53,6 +57,8 @@ struct BrowsersState {
|
||||
int new_port = 19222;
|
||||
bool new_headless = false;
|
||||
std::string last_error;
|
||||
// data_table state (persists between frames).
|
||||
data_table::State dt_state;
|
||||
};
|
||||
|
||||
BrowsersState g_browsers;
|
||||
@@ -173,68 +179,80 @@ void render_browsers_panel(bool* p_open) {
|
||||
ImGui::TextDisabled("No Chrome instances with --remote-debugging-port detected.");
|
||||
ImGui::TextDisabled("Lanza una con el formulario de arriba o con scripts/start.sh.");
|
||||
} else {
|
||||
const ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable;
|
||||
if (ImGui::BeginTable("##browsers", 7, flags)) {
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 22);
|
||||
ImGui::TableSetupColumn("PID", ImGuiTableColumnFlags_WidthFixed, 64);
|
||||
ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed, 64);
|
||||
ImGui::TableSetupColumn("Profile");
|
||||
ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 80);
|
||||
ImGui::TableSetupColumn("user-data-dir");
|
||||
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 130);
|
||||
ImGui::TableHeadersRow();
|
||||
// --- Build TableInput (6 data cols, no Actions) ---
|
||||
// Cols: Sel, PID, Port, Profile, Mode, user-data-dir
|
||||
static std::vector<std::string> cell_backing;
|
||||
const int NCOLS = 6;
|
||||
const int nrows = (int)g_browsers.instances.size();
|
||||
cell_backing.clear();
|
||||
cell_backing.reserve((size_t)(nrows * NCOLS));
|
||||
for (const auto& inst : g_browsers.instances) {
|
||||
cell_backing.push_back(sel_port == inst.port ? TI_CHECK : "");
|
||||
cell_backing.push_back(std::to_string(inst.pid));
|
||||
cell_backing.push_back(std::to_string(inst.port));
|
||||
cell_backing.push_back(inst.profile_name.empty() ? "(none)" : inst.profile_name);
|
||||
cell_backing.push_back(inst.headless ? "headless" : "visible");
|
||||
cell_backing.push_back(inst.user_data_dir);
|
||||
}
|
||||
static std::vector<const char*> cell_ptrs;
|
||||
cell_ptrs.clear();
|
||||
cell_ptrs.reserve(cell_backing.size());
|
||||
for (const auto& s : cell_backing) cell_ptrs.push_back(s.c_str());
|
||||
|
||||
int idx = 0;
|
||||
for (const auto& inst : g_browsers.instances) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(idx);
|
||||
ImGui::TableNextColumn();
|
||||
bool is_sel = (sel_port == inst.port);
|
||||
if (is_sel) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::primary);
|
||||
ImGui::TextUnformatted(TI_CHECK);
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::TextUnformatted("");
|
||||
}
|
||||
data_table::TableInput tbl;
|
||||
tbl.name = "browsers";
|
||||
tbl.headers = {"", "PID", "Port", "Profile", "Mode", "user-data-dir"};
|
||||
tbl.types = {
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::Int,
|
||||
data_table::ColumnType::Int,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String
|
||||
};
|
||||
tbl.cells = cell_ptrs.data();
|
||||
tbl.rows = nrows;
|
||||
tbl.cols = NCOLS;
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%u", inst.pid);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", inst.port);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(inst.profile_name.empty() ? "(none)" : inst.profile_name.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
if (inst.headless) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::warning);
|
||||
ImGui::TextUnformatted("headless");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::TextUnformatted("visible");
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(inst.user_data_dir.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton(is_sel ? "Selected" : "Select")) {
|
||||
g_session().select_browser(inst.port);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Kill")) {
|
||||
if (!inst.user_data_dir.empty()) {
|
||||
kill_chromes_by_userdata(inst.user_data_dir);
|
||||
}
|
||||
if (sel_port == inst.port) g_session().clear_selection();
|
||||
std::thread([]{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
rescan_async();
|
||||
}).detach();
|
||||
}
|
||||
ImGui::PopID();
|
||||
++idx;
|
||||
// Declarative renderers.
|
||||
tbl.column_specs.resize((size_t)NCOLS);
|
||||
// Col 4: Mode -> Badge
|
||||
{
|
||||
data_table::ColumnSpec& cs = tbl.column_specs[4];
|
||||
cs.id = "mode";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"headless", "#f59e0b", "headless"},
|
||||
data_table::BadgeRule{"visible", "#22c55e", "visible"},
|
||||
};
|
||||
}
|
||||
|
||||
data_table::render("##dt_browsers", {tbl}, g_browsers.dt_state, false);
|
||||
|
||||
// --- Actions: inline button list per row (no BeginTable — Button renderer not Fase-1) ---
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Actions:");
|
||||
int idx = 0;
|
||||
for (const auto& inst : g_browsers.instances) {
|
||||
bool is_sel = (sel_port == inst.port);
|
||||
ImGui::PushID(idx);
|
||||
ImGui::Text(":%d", inst.port); ImGui::SameLine();
|
||||
if (ImGui::SmallButton(is_sel ? "Selected" : "Select")) {
|
||||
g_session().select_browser(inst.port);
|
||||
}
|
||||
ImGui::EndTable();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Kill")) {
|
||||
if (!inst.user_data_dir.empty()) {
|
||||
kill_chromes_by_userdata(inst.user_data_dir);
|
||||
}
|
||||
if (sel_port == inst.port) g_session().clear_selection();
|
||||
std::thread([]{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
rescan_async();
|
||||
}).detach();
|
||||
}
|
||||
ImGui::PopID();
|
||||
++idx;
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
@@ -249,6 +267,8 @@ struct TabsUiState {
|
||||
std::atomic<bool> refreshing{false};
|
||||
char new_url_input[1024] = "https://example.com";
|
||||
char filter[128] = "";
|
||||
// data_table state (persists between frames).
|
||||
data_table::State dt_state;
|
||||
};
|
||||
TabsUiState g_tabs_ui;
|
||||
|
||||
@@ -349,53 +369,98 @@ void render_tabs_panel(bool* p_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply filter to get visible tabs.
|
||||
std::string filter_str = g_tabs_ui.filter;
|
||||
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
|
||||
std::vector<const CdpTab*> visible_tabs;
|
||||
visible_tabs.reserve(tabs_copy.size());
|
||||
for (const auto& t : tabs_copy) {
|
||||
if (!filter_str.empty()) {
|
||||
std::string lt = t.title; std::transform(lt.begin(), lt.end(), lt.begin(), ::tolower);
|
||||
std::string lu = t.url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower);
|
||||
if (lt.find(filter_str) == std::string::npos &&
|
||||
lu.find(filter_str) == std::string::npos) continue;
|
||||
}
|
||||
visible_tabs.push_back(&t);
|
||||
}
|
||||
|
||||
const ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
|
||||
if (ImGui::BeginTable("##tabs", 6, flags, ImVec2(0, 0))) {
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 22);
|
||||
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70);
|
||||
ImGui::TableSetupColumn("Title");
|
||||
ImGui::TableSetupColumn("URL");
|
||||
ImGui::TableSetupColumn("Att.", ImGuiTableColumnFlags_WidthFixed, 40);
|
||||
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 200);
|
||||
ImGui::TableHeadersRow();
|
||||
// --- Build TableInput (5 data cols, no Actions) ---
|
||||
// Cols: Sel, Type, Title, URL, Attached
|
||||
static std::vector<std::string> tab_cell_backing;
|
||||
const int NCOLS = 5;
|
||||
const int nrows = (int)visible_tabs.size();
|
||||
tab_cell_backing.clear();
|
||||
tab_cell_backing.reserve((size_t)(nrows * NCOLS));
|
||||
for (const CdpTab* tp : visible_tabs) {
|
||||
tab_cell_backing.push_back(sel_tab_id == tp->id ? TI_CHECK : "");
|
||||
tab_cell_backing.push_back(tp->type);
|
||||
tab_cell_backing.push_back(tp->title.empty() ? "(no title)" : tp->title);
|
||||
tab_cell_backing.push_back(tp->url);
|
||||
tab_cell_backing.push_back(tp->attached ? "yes" : "no");
|
||||
}
|
||||
static std::vector<const char*> tab_cell_ptrs;
|
||||
tab_cell_ptrs.clear();
|
||||
tab_cell_ptrs.reserve(tab_cell_backing.size());
|
||||
for (const auto& s : tab_cell_backing) tab_cell_ptrs.push_back(s.c_str());
|
||||
|
||||
data_table::TableInput tbl;
|
||||
tbl.name = "tabs";
|
||||
tbl.headers = {"", "Type", "Title", "URL", "Attached"};
|
||||
tbl.types = {
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String
|
||||
};
|
||||
tbl.cells = tab_cell_ptrs.data();
|
||||
tbl.rows = nrows;
|
||||
tbl.cols = NCOLS;
|
||||
|
||||
// Declarative renderers.
|
||||
tbl.column_specs.resize((size_t)NCOLS);
|
||||
// Col 1: Type -> Badge
|
||||
{
|
||||
data_table::ColumnSpec& cs = tbl.column_specs[1];
|
||||
cs.id = "type";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"page", "#3b82f6", "page"},
|
||||
data_table::BadgeRule{"iframe", "#8b5cf6", "iframe"},
|
||||
data_table::BadgeRule{"service_worker", "#f59e0b", "service_worker"},
|
||||
data_table::BadgeRule{"worker", "#f59e0b", "worker"},
|
||||
};
|
||||
}
|
||||
// Col 4: Attached -> Badge
|
||||
{
|
||||
data_table::ColumnSpec& cs = tbl.column_specs[4];
|
||||
cs.id = "attached";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"yes", "#22c55e", "yes"},
|
||||
data_table::BadgeRule{"no", "#6b7280", "no"},
|
||||
};
|
||||
}
|
||||
|
||||
data_table::render("##dt_tabs", {tbl}, g_tabs_ui.dt_state, false);
|
||||
|
||||
// --- Actions: inline button list per visible row (no BeginTable — Button renderer not Fase-1) ---
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Actions:");
|
||||
{
|
||||
int idx = 0;
|
||||
for (const auto& t : tabs_copy) {
|
||||
if (!filter_str.empty()) {
|
||||
std::string lt = t.title; std::transform(lt.begin(), lt.end(), lt.begin(), ::tolower);
|
||||
std::string lu = t.url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower);
|
||||
if (lt.find(filter_str) == std::string::npos &&
|
||||
lu.find(filter_str) == std::string::npos) continue;
|
||||
}
|
||||
bool is_sel = (sel_tab_id == t.id);
|
||||
ImGui::TableNextRow();
|
||||
for (const CdpTab* tp : visible_tabs) {
|
||||
bool is_sel = (sel_tab_id == tp->id);
|
||||
ImGui::PushID(idx);
|
||||
ImGui::TableNextColumn();
|
||||
if (is_sel) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::primary);
|
||||
ImGui::TextUnformatted(TI_CHECK);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(t.type.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(t.title.empty() ? "(no title)" : t.title.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(t.url.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(t.attached ? "yes" : "");
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::SmallButton("Select")) {
|
||||
if (!t.ws_url.empty()) g_session().select_tab(t.id, t.ws_url);
|
||||
std::string short_title = tp->title.empty() ? "(no title)" : tp->title;
|
||||
if (short_title.size() > 32) short_title = short_title.substr(0, 29) + "...";
|
||||
ImGui::TextUnformatted(short_title.c_str()); ImGui::SameLine();
|
||||
if (ImGui::SmallButton(is_sel ? "Sel*" : "Select")) {
|
||||
if (!tp->ws_url.empty()) g_session().select_tab(tp->id, tp->ws_url);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Focus")) {
|
||||
std::string id = t.id;
|
||||
std::string id = tp->id;
|
||||
std::thread([port, id]{
|
||||
cdp_activate_tab(port, id, nullptr);
|
||||
refresh_tabs_async(port);
|
||||
@@ -403,17 +468,17 @@ void render_tabs_panel(bool* p_open) {
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Close")) {
|
||||
std::string id = t.id;
|
||||
std::string id = tp->id;
|
||||
bool is_s = is_sel;
|
||||
std::thread([port, id]{
|
||||
cdp_close_tab(port, id, nullptr);
|
||||
refresh_tabs_async(port);
|
||||
}).detach();
|
||||
if (is_sel) g_session().select_tab("", "");
|
||||
if (is_s) g_session().select_tab("", "");
|
||||
}
|
||||
ImGui::PopID();
|
||||
++idx;
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
@@ -509,6 +574,9 @@ struct NetUiState {
|
||||
bool show_histogram = true;
|
||||
int histogram_bins = 30;
|
||||
bool reload_ignore_cache = false;
|
||||
|
||||
// data_table state for ##requests (persists between frames).
|
||||
data_table::State dt_state;
|
||||
};
|
||||
NetUiState g_net_ui;
|
||||
|
||||
@@ -724,26 +792,78 @@ void draw_request_detail(const NetworkRequest& r, NetworkSession* net) {
|
||||
if (r.type == ResourceType::WebSocket && ImGui::BeginTabItem("Messages")) {
|
||||
ImGui::Text("Frames: %d", (int)r.ws_frames.size());
|
||||
ImGui::Separator();
|
||||
const ImGuiTableFlags f = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY;
|
||||
if (ImGui::BeginTable("##wsframes", 4, f, ImVec2(-1, -1))) {
|
||||
ImGui::TableSetupColumn("Dir", ImGuiTableColumnFlags_WidthFixed, 30);
|
||||
ImGui::TableSetupColumn("Op", ImGuiTableColumnFlags_WidthFixed, 30);
|
||||
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 80);
|
||||
ImGui::TableSetupColumn("Payload");
|
||||
ImGui::TableHeadersRow();
|
||||
for (const auto& wf : r.ws_frames) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(wf.outgoing ? TI_ARROW_UP : TI_ARROW_DOWN);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", wf.opcode);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%.3f", wf.time);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextWrapped("%s", wf.payload.c_str());
|
||||
|
||||
// --- data_table::render for ##wsframes ---
|
||||
// Cols: Dir, Op, Time, Payload
|
||||
static std::vector<std::string> ws_cell_backing;
|
||||
static data_table::State g_dt_wsframes;
|
||||
const int WS_COLS = 4;
|
||||
const int ws_rows = (int)r.ws_frames.size();
|
||||
ws_cell_backing.clear();
|
||||
ws_cell_backing.reserve((size_t)(ws_rows * WS_COLS));
|
||||
for (const auto& wf : r.ws_frames) {
|
||||
ws_cell_backing.push_back(wf.outgoing ? "send" : "recv");
|
||||
// Op: opcode as string label
|
||||
switch (wf.opcode) {
|
||||
case 1: ws_cell_backing.push_back("text"); break;
|
||||
case 2: ws_cell_backing.push_back("binary"); break;
|
||||
case 8: ws_cell_backing.push_back("close"); break;
|
||||
case 9: ws_cell_backing.push_back("ping"); break;
|
||||
case 10: ws_cell_backing.push_back("pong"); break;
|
||||
default: {
|
||||
char buf[16]; std::snprintf(buf, sizeof(buf), "%d", wf.opcode);
|
||||
ws_cell_backing.push_back(buf);
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
{ char buf[32]; std::snprintf(buf, sizeof(buf), "%.3f", wf.time);
|
||||
ws_cell_backing.push_back(buf); }
|
||||
ws_cell_backing.push_back(wf.payload);
|
||||
}
|
||||
static std::vector<const char*> ws_cell_ptrs;
|
||||
ws_cell_ptrs.clear();
|
||||
ws_cell_ptrs.reserve(ws_cell_backing.size());
|
||||
for (const auto& s : ws_cell_backing) ws_cell_ptrs.push_back(s.c_str());
|
||||
|
||||
data_table::TableInput ws_tbl;
|
||||
ws_tbl.name = "wsframes";
|
||||
ws_tbl.headers = {"Dir", "Op", "Time", "Payload"};
|
||||
ws_tbl.types = {
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::Float,
|
||||
data_table::ColumnType::String
|
||||
};
|
||||
ws_tbl.cells = ws_rows > 0 ? ws_cell_ptrs.data() : nullptr;
|
||||
ws_tbl.rows = ws_rows;
|
||||
ws_tbl.cols = WS_COLS;
|
||||
|
||||
// Declarative renderers.
|
||||
ws_tbl.column_specs.resize((size_t)WS_COLS);
|
||||
// Col 0: Dir -> Badge
|
||||
{
|
||||
data_table::ColumnSpec& cs = ws_tbl.column_specs[0];
|
||||
cs.id = "dir";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"send", "#3b82f6", "send"},
|
||||
data_table::BadgeRule{"recv", "#22c55e", "recv"},
|
||||
};
|
||||
}
|
||||
// Col 1: Op -> Badge
|
||||
{
|
||||
data_table::ColumnSpec& cs = ws_tbl.column_specs[1];
|
||||
cs.id = "op";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"text", "#3b82f6", "text"},
|
||||
data_table::BadgeRule{"binary", "#8b5cf6", "binary"},
|
||||
data_table::BadgeRule{"close", "#ef4444", "close"},
|
||||
data_table::BadgeRule{"ping", "#6b7280", "ping"},
|
||||
data_table::BadgeRule{"pong", "#6b7280", "pong"},
|
||||
};
|
||||
}
|
||||
|
||||
data_table::render("##dt_wsframes", {ws_tbl}, g_dt_wsframes, false);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
@@ -925,108 +1045,134 @@ void render_network_panel(bool* p_open) {
|
||||
|
||||
ImGui::BeginChild("##nettable", ImVec2(0, top_h), true);
|
||||
{
|
||||
const ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_Sortable;
|
||||
if (ImGui::BeginTable("##requests", 8, flags)) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("Name");
|
||||
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60);
|
||||
ImGui::TableSetupColumn("Method", ImGuiTableColumnFlags_WidthFixed, 70);
|
||||
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70);
|
||||
ImGui::TableSetupColumn("Initiator");
|
||||
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90);
|
||||
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 70);
|
||||
ImGui::TableSetupColumn("Waterfall",ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// Para waterfall: necesitamos rango total.
|
||||
double t_min = 0.0, t_max = 0.0;
|
||||
for (const auto& r : filtered) {
|
||||
t_max = std::max(t_max, std::max(r->t_finished, r->t_response));
|
||||
// --- data_table::render for ##requests ---
|
||||
// Cols: Name, Status, Method, Type, Initiator, Size, Time
|
||||
// (Waterfall col omitted: requires custom ImDrawList rendering — not a data_table renderer)
|
||||
static std::vector<std::string> req_cell_backing;
|
||||
const int REQ_COLS = 7;
|
||||
const int req_rows = (int)filtered.size();
|
||||
req_cell_backing.clear();
|
||||
req_cell_backing.reserve((size_t)(req_rows * REQ_COLS));
|
||||
for (const auto& r : filtered) {
|
||||
// Name
|
||||
std::string name = short_name_from_url(r->url);
|
||||
if (name.empty()) name = "(empty)";
|
||||
req_cell_backing.push_back(std::move(name));
|
||||
// Status: bucket string for Badge + numeric for display
|
||||
if (r->status > 0) {
|
||||
req_cell_backing.push_back(std::to_string(r->status));
|
||||
} else if (r->failed) {
|
||||
req_cell_backing.push_back("failed");
|
||||
} else {
|
||||
req_cell_backing.push_back("...");
|
||||
}
|
||||
if (t_max < 1.0) t_max = 1.0;
|
||||
|
||||
for (size_t i = 0; i < filtered.size(); ++i) {
|
||||
const auto& r = filtered[i];
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID((int)i);
|
||||
bool is_sel = (g_net_ui.selected_id == r->id);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
std::string name = short_name_from_url(r->url);
|
||||
if (name.empty()) name = "(empty)";
|
||||
if (ImGui::Selectable(name.c_str(), is_sel,
|
||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
g_net_ui.selected_id = r->id;
|
||||
g_net_ui.selected_index = (int)i;
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem("##rowctx")) {
|
||||
if (ImGui::MenuItem("Copy URL")) copy_to_clipboard(r->url);
|
||||
if (ImGui::MenuItem("Copy as cURL")) copy_to_clipboard(build_curl(*r));
|
||||
if (ImGui::MenuItem("Copy as fetch")) copy_to_clipboard(build_fetch(*r));
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Block URL (TODO)")) {}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
if (r->status > 0) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, status_color(r->status));
|
||||
ImGui::Text("%d", r->status);
|
||||
ImGui::PopStyleColor();
|
||||
} else if (r->failed) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
|
||||
ImGui::TextUnformatted("(failed)");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::TextDisabled("...");
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(r->method.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(resource_type_label(r->type));
|
||||
ImGui::TableNextColumn();
|
||||
if (!r->initiator_url.empty()) {
|
||||
ImGui::TextUnformatted(short_name_from_url(r->initiator_url).c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", r->initiator_type.c_str());
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
if (r->from_cache) {
|
||||
ImGui::TextDisabled("(cache)");
|
||||
} else {
|
||||
ImGui::TextUnformatted(fmt_size(r->encoded_data_length).c_str());
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
{
|
||||
double dur = (r->t_finished > 0 ? r->t_finished : r->t_response) - r->t_started;
|
||||
if (dur < 0) dur = 0;
|
||||
ImGui::TextUnformatted(fmt_dur_ms(dur).c_str());
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
{
|
||||
// mini waterfall bar.
|
||||
ImVec2 cmin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 avail = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeight());
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
double a = r->t_started / t_max;
|
||||
double b = (r->t_finished > 0 ? r->t_finished : r->t_response) / t_max;
|
||||
if (b < a) b = a;
|
||||
if (b > 1) b = 1;
|
||||
ImVec2 p1(cmin.x + avail.x * (float)a, cmin.y + 2);
|
||||
ImVec2 p2(cmin.x + avail.x * (float)b, cmin.y + avail.y - 2);
|
||||
if (p2.x < p1.x + 2) p2.x = p1.x + 2;
|
||||
ImU32 col = ImGui::ColorConvertFloat4ToU32(
|
||||
r->failed ? fn_tokens::colors::error :
|
||||
(r->finished ? fn_tokens::colors::primary : fn_tokens::colors::warning));
|
||||
dl->AddRectFilled(p1, p2, col, 2.0f);
|
||||
ImGui::Dummy(avail);
|
||||
}
|
||||
ImGui::PopID();
|
||||
// Method
|
||||
req_cell_backing.push_back(r->method.empty() ? "GET" : r->method);
|
||||
// Type
|
||||
req_cell_backing.push_back(resource_type_label(r->type));
|
||||
// Initiator
|
||||
if (!r->initiator_url.empty()) {
|
||||
req_cell_backing.push_back(short_name_from_url(r->initiator_url));
|
||||
} else {
|
||||
req_cell_backing.push_back(r->initiator_type);
|
||||
}
|
||||
// Size
|
||||
if (r->from_cache) {
|
||||
req_cell_backing.push_back("(cache)");
|
||||
} else {
|
||||
req_cell_backing.push_back(fmt_size(r->encoded_data_length));
|
||||
}
|
||||
// Time (duration_ms as float string for Duration renderer)
|
||||
{
|
||||
double dur = (r->t_finished > 0 ? r->t_finished : r->t_response) - r->t_started;
|
||||
if (dur < 0) dur = 0;
|
||||
char buf[32]; std::snprintf(buf, sizeof(buf), "%.3f", dur * 1000.0);
|
||||
req_cell_backing.push_back(buf);
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
static std::vector<const char*> req_cell_ptrs;
|
||||
req_cell_ptrs.clear();
|
||||
req_cell_ptrs.reserve(req_cell_backing.size());
|
||||
for (const auto& s : req_cell_backing) req_cell_ptrs.push_back(s.c_str());
|
||||
|
||||
data_table::TableInput req_tbl;
|
||||
req_tbl.name = "requests";
|
||||
req_tbl.headers = {"Name", "Status", "Method", "Type", "Initiator", "Size", "Time (ms)"};
|
||||
req_tbl.types = {
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::Float,
|
||||
};
|
||||
req_tbl.cells = req_rows > 0 ? req_cell_ptrs.data() : nullptr;
|
||||
req_tbl.rows = req_rows;
|
||||
req_tbl.cols = REQ_COLS;
|
||||
|
||||
// Declarative renderers.
|
||||
req_tbl.column_specs.resize((size_t)REQ_COLS);
|
||||
// Col 1: Status -> Badge by bucket
|
||||
{
|
||||
data_table::ColumnSpec& cs = req_tbl.column_specs[1];
|
||||
cs.id = "status";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
// 2xx — success green
|
||||
data_table::BadgeRule{"200", "#22c55e", "200"},
|
||||
data_table::BadgeRule{"201", "#22c55e", "201"},
|
||||
data_table::BadgeRule{"204", "#22c55e", "204"},
|
||||
data_table::BadgeRule{"206", "#22c55e", "206"},
|
||||
// 3xx — info blue
|
||||
data_table::BadgeRule{"301", "#3b82f6", "301"},
|
||||
data_table::BadgeRule{"302", "#3b82f6", "302"},
|
||||
data_table::BadgeRule{"304", "#3b82f6", "304"},
|
||||
// 4xx — warning yellow
|
||||
data_table::BadgeRule{"400", "#f59e0b", "400"},
|
||||
data_table::BadgeRule{"401", "#f59e0b", "401"},
|
||||
data_table::BadgeRule{"403", "#f59e0b", "403"},
|
||||
data_table::BadgeRule{"404", "#f59e0b", "404"},
|
||||
data_table::BadgeRule{"429", "#f59e0b", "429"},
|
||||
// 5xx — error red
|
||||
data_table::BadgeRule{"500", "#ef4444", "500"},
|
||||
data_table::BadgeRule{"502", "#ef4444", "502"},
|
||||
data_table::BadgeRule{"503", "#ef4444", "503"},
|
||||
data_table::BadgeRule{"504", "#ef4444", "504"},
|
||||
// special
|
||||
data_table::BadgeRule{"failed", "#ef4444", "failed"},
|
||||
data_table::BadgeRule{"...", "#6b7280", "..."},
|
||||
};
|
||||
}
|
||||
// Col 2: Method -> Badge
|
||||
{
|
||||
data_table::ColumnSpec& cs = req_tbl.column_specs[2];
|
||||
cs.id = "method";
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"GET", "#22c55e", "GET"},
|
||||
data_table::BadgeRule{"POST", "#3b82f6", "POST"},
|
||||
data_table::BadgeRule{"PUT", "#f59e0b", "PUT"},
|
||||
data_table::BadgeRule{"DELETE", "#ef4444", "DELETE"},
|
||||
data_table::BadgeRule{"PATCH", "#8b5cf6", "PATCH"},
|
||||
data_table::BadgeRule{"HEAD", "#6b7280", "HEAD"},
|
||||
data_table::BadgeRule{"OPTIONS","#6b7280", "OPTIONS"},
|
||||
};
|
||||
}
|
||||
// Col 6: Time -> Duration (value already in ms)
|
||||
{
|
||||
data_table::ColumnSpec& cs = req_tbl.column_specs[6];
|
||||
cs.id = "time_ms";
|
||||
cs.renderer = data_table::CellRenderer::Duration;
|
||||
cs.duration_warn_ms = 1000.0f;
|
||||
cs.duration_error_ms = 5000.0f;
|
||||
}
|
||||
|
||||
data_table::render("##dt_requests", {req_tbl}, g_net_ui.dt_state, true);
|
||||
|
||||
// Context menu + selection: track clicked row by matching Name col
|
||||
// (handled inside data_table via row-click; URL copy available via right-click
|
||||
// on cell in data_table chrome).
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user