// views_jobs.cpp — Jobs panel (issue 0026). // // Migration history (issue 0081-J): // v1: ImGui::BeginTable inline with 6 columns (status, enricher, target, // progress, time, ##actions). // v2: data_table::render for 5 data columns (status, enricher, target, // progress, time) with declarative CellRenderer (Badge, Progress, // Duration). Action buttons (Cancel/Delete) rendered in a separate // small ImGui::BeginTable below — option (a) from issue 0081-J spec. // v3 (Phase 2, issue 0081-J): Cancel/Delete absorbed into the main // data_table as Button-renderer virtual columns (col 5 = "cancel", // col 6 = "delete"). The separate ##jobs_actions_tbl BeginTable is // removed. ButtonClick events dispatched post-render via events_out. // job_id resolved by row index from s_frow_ids parallel vector. #include "views.h" #include "jobs.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "core/data_table_types.h" #include "viz/data_table.h" #include "imgui.h" #include #include #include #include #include namespace ge { namespace { // Cache de la lista de jobs. Se refresca cada N frames para no abrir SQLite // en cada frame. ~10 Hz es suficiente para una progress bar fluida. struct JobsCache { std::vector rows; int last_frame_refresh = -100; int filter_idx = 0; // 0=all 1=active 2=done 3=error }; JobsCache g_jobs_cache; ImVec4 status_color(const std::string& s) { if (s == "running") return ImVec4(0.36f, 0.78f, 1.0f, 1.0f); if (s == "done") return ImVec4(0.40f, 0.85f, 0.55f, 1.0f); if (s == "error") return ImVec4(0.95f, 0.45f, 0.45f, 1.0f); if (s == "cancelled") return ImVec4(0.65f, 0.65f, 0.65f, 1.0f); if (s == "queued") return ImVec4(0.85f, 0.78f, 0.45f, 1.0f); return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); } bool filter_match(const std::string& status, int idx) { switch (idx) { case 0: return true; case 1: return status == "queued" || status == "running"; case 2: return status == "done"; case 3: return status == "error" || status == "cancelled"; default: return true; } } // Duration helper: computes elapsed ms for running jobs (finished_at=0). long long job_duration_ms(const JobRow& r) { if (r.started_at <= 0) return 0; long long end = r.finished_at > 0 ? r.finished_at : std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); long long ms = end - r.started_at; return ms < 0 ? 0 : ms; } } // namespace void views_jobs(AppState& app) { if (!app.panel_jobs) return; if (!ImGui::Begin("Jobs", &app.panel_jobs)) { ImGui::End(); return; } // Refresh cache cada ~10 frames (~6 Hz a 60fps). int frame = ImGui::GetFrameCount(); if (frame - g_jobs_cache.last_frame_refresh > 10) { jobs_list(&g_jobs_cache.rows, 200); g_jobs_cache.last_frame_refresh = frame; } // Header: counters + filtro. JobCounters c = jobs_counters(); ImGui::TextColored(status_color("running"), "%s", TI_PLAYER_PLAY); ImGui::SameLine(); ImGui::Text("%d", c.running); ImGui::SameLine(0, 16); ImGui::TextColored(status_color("queued"), "%s", TI_HOURGLASS); ImGui::SameLine(); ImGui::Text("%d", c.queued); ImGui::SameLine(0, 16); ImGui::TextColored(status_color("done"), "%s", TI_CHECK); ImGui::SameLine(); ImGui::Text("%d", c.done); ImGui::SameLine(0, 16); ImGui::TextColored(status_color("error"), "%s", TI_ALERT_CIRCLE); ImGui::SameLine(); ImGui::Text("%d", c.error + c.cancelled); ImGui::SameLine(0, 24); const char* filter_labels[] = { "All", "Active", "Done", "Errors" }; ImGui::SetNextItemWidth(100); ImGui::Combo("##jobs_filter", &g_jobs_cache.filter_idx, filter_labels, IM_ARRAYSIZE(filter_labels)); ImGui::Separator(); // ------------------------------------------------------------------------- // Main data table via data_table::render (issue 0081-J migration). // // Columns: // 0: status — Badge renderer (queued/running/done/error/cancelled) // 1: enricher — Text (default) // 2: target — Text (default) // 3: progress — Progress renderer, 0..1 scale // 4: time_ms — Duration renderer, warn=1000ms error=10000ms // 5: cancel — Button renderer; value="Cancel" if active, "" otherwise // 6: delete — Button renderer; value="Delete" if terminal, "" otherwise // // Rows are filtered by g_jobs_cache.filter_idx BEFORE populating // cells, so data_table only sees the subset the user wants. // // Cell backing: static vectors rebuilt when cache refreshes. // s_frow_ids: parallel vector of job ids for O(1) ButtonClick dispatch. // ------------------------------------------------------------------------- static const std::vector k_headers = { "status", "enricher", "target", "progress", "time_ms", "cancel", "delete" }; constexpr int k_ncols = 7; static std::vector s_cell_backing; static std::vector s_cells; static std::vector s_frow_ids; // job id per filtered row static int s_rows_cached = -1; static int s_filter_cached = -1; // Rebuild when cache was refreshed or filter changed. bool needs_rebuild = (s_rows_cached != (int)g_jobs_cache.rows.size()) || (s_filter_cached != g_jobs_cache.filter_idx) || (frame - g_jobs_cache.last_frame_refresh <= 10); // just refreshed if (needs_rebuild) { // Collect filtered rows. std::vector frows; frows.reserve(g_jobs_cache.rows.size()); for (const auto& r : g_jobs_cache.rows) { if (filter_match(r.status, g_jobs_cache.filter_idx)) frows.push_back(&r); } const int nrows = (int)frows.size(); s_cell_backing.resize((size_t)nrows * k_ncols); s_cells.resize((size_t)nrows * k_ncols); s_frow_ids.resize((size_t)nrows); for (int i = 0; i < nrows; ++i) { const JobRow& r = *frows[(size_t)i]; const size_t base = (size_t)i * k_ncols; s_frow_ids[(size_t)i] = r.id; // status s_cell_backing[base + 0] = r.status; // enricher s_cell_backing[base + 1] = r.enricher_id; // target: node_name > node_id > "(global)" if (!r.node_name.empty()) s_cell_backing[base + 2] = r.node_name; else if (!r.node_id.empty()) s_cell_backing[base + 2] = r.node_id; else s_cell_backing[base + 2] = "(global)"; // progress: float 0..1 -> string (data_table Progress renderer // parses via strtof; value is in [0,1]). char pbuf[16]; std::snprintf(pbuf, sizeof(pbuf), "%.4f", (float)r.progress); s_cell_backing[base + 3] = pbuf; // time_ms: duration in milliseconds as float string. char tbuf[24]; long long ms = job_duration_ms(r); std::snprintf(tbuf, sizeof(tbuf), "%lld", ms); s_cell_backing[base + 4] = tbuf; // cancel: show button only for active jobs. if (r.status == "queued" || r.status == "running") s_cell_backing[base + 5] = "Cancel"; else s_cell_backing[base + 5] = ""; // delete: show button only for terminal jobs. if (r.status == "done" || r.status == "error" || r.status == "cancelled") s_cell_backing[base + 6] = "Delete"; else s_cell_backing[base + 6] = ""; } for (size_t k = 0; k < s_cell_backing.size(); ++k) s_cells[k] = s_cell_backing[k].c_str(); s_rows_cached = (int)g_jobs_cache.rows.size(); s_filter_cached = g_jobs_cache.filter_idx; } // Build column specs with declarative renderers. static const std::vector k_col_specs = []() { std::vector specs(k_ncols); // Col 0: status — Badge specs[0].id = "status"; specs[0].renderer = data_table::CellRenderer::Badge; specs[0].badges = { { "running", "#3b82f6", "Running" }, { "done", "#22c55e", "Done" }, { "error", "#ef4444", "Error" }, { "cancelled", "#9ca3af", "Cancelled" }, { "queued", "#f59e0b", "Queued" }, }; // Col 1: enricher — Text (default) specs[1].id = "enricher"; specs[1].renderer = data_table::CellRenderer::Text; // Col 2: target — Text (default) specs[2].id = "target"; specs[2].renderer = data_table::CellRenderer::Text; // Col 3: progress — Progress bar, values are 0..1 specs[3].id = "progress"; specs[3].renderer = data_table::CellRenderer::Progress; specs[3].progress_scale_100 = false; // Col 4: time_ms — Duration, warn=1000ms error=10000ms specs[4].id = "time_ms"; specs[4].renderer = data_table::CellRenderer::Duration; specs[4].duration_warn_ms = 1000.0f; specs[4].duration_error_ms = 10000.0f; // Col 5: cancel — Button, amber; only shown when cell value is non-empty. specs[5].id = "cancel"; specs[5].renderer = data_table::CellRenderer::Button; specs[5].button_action = "cancel_job"; specs[5].button_color_hex = "#f59e0b"; // Col 6: delete — Button, red; only shown when cell value is non-empty. specs[6].id = "delete"; specs[6].renderer = data_table::CellRenderer::Button; specs[6].button_action = "delete_job"; specs[6].button_color_hex = "#ef4444"; return specs; }(); // Compute available height for main table. float avail_h = ImGui::GetContentRegionAvail().y - 4.0f; if (avail_h < 80.0f) avail_h = 80.0f; ImGui::BeginChild("##jobs_data", ImVec2(0.0f, avail_h), ImGuiChildFlags_None); // Recompute actual filtered row count. int nrows_data = 0; for (const auto& r : g_jobs_cache.rows) { if (filter_match(r.status, g_jobs_cache.filter_idx)) ++nrows_data; } if (nrows_data == 0) { ImGui::TextDisabled("(no jobs match current filter)"); } else { data_table::TableInput tbl; tbl.name = "jobs"; tbl.headers = k_headers; tbl.types = { data_table::ColumnType::String, // status data_table::ColumnType::String, // enricher data_table::ColumnType::String, // target data_table::ColumnType::Float, // progress data_table::ColumnType::Float, // time_ms data_table::ColumnType::String, // cancel (virtual) data_table::ColumnType::String, // delete (virtual) }; tbl.cells = s_cells.empty() ? nullptr : s_cells.data(); tbl.rows = nrows_data; tbl.cols = k_ncols; tbl.column_specs = k_col_specs; std::vector events; data_table::render("##jobs_dt", {tbl}, app.jobs_dt_state, &events); // Dispatch button events. row index maps 1:1 to s_frow_ids. for (const auto& e : events) { if (e.kind != data_table::TableEventKind::ButtonClick) continue; if (e.row < 0 || e.row >= (int)s_frow_ids.size()) continue; const std::string& job_id = s_frow_ids[(size_t)e.row]; if (e.action_id == "cancel_job") { jobs_cancel(job_id.c_str()); g_jobs_cache.last_frame_refresh = -100; // invalidate immediately } else if (e.action_id == "delete_job") { jobs_delete(job_id.c_str()); g_jobs_cache.last_frame_refresh = -100; } } } ImGui::EndChild(); ImGui::End(); } } // namespace ge