35ac7d9a24
- Remove ##jobs_actions_tbl BeginTable (the separate actions mini-table). - Add 2 virtual columns to main data_table: cancel (col 5) + delete (col 6). cancel = "Cancel" only for queued/running jobs; delete = "Delete" only for done/error/cancelled. - ColumnSpec: CellRenderer::Button, button_action cancel_job/delete_job, amber/red colors. - Maintain s_frow_ids parallel vector (job id per filtered row) for O(1) ButtonClick dispatch. - Dispatch loop after render: ButtonClick -> jobs_cancel/jobs_delete + cache invalidation. - te_rows (##te_rows) NOT migrated: table has Selectable AllowOverlap + right-click context menus + Promote/Demote SmallButton with AllowOverlap — requires RowContextMenu renderer hook not yet in data_table v1.2.0. Deferred to Phase 3 (TextInput + context-menu hook). Build: Linux + Windows clean. pytest 125/125 passed.
319 lines
12 KiB
C++
319 lines
12 KiB
C++
// 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 <cfloat>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<JobRow> 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::milliseconds>(
|
|
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<std::string> k_headers = {
|
|
"status", "enricher", "target", "progress", "time_ms", "cancel", "delete"
|
|
};
|
|
constexpr int k_ncols = 7;
|
|
|
|
static std::vector<std::string> s_cell_backing;
|
|
static std::vector<const char*> s_cells;
|
|
static std::vector<std::string> 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<const JobRow*> 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<data_table::ColumnSpec> k_col_specs = []() {
|
|
std::vector<data_table::ColumnSpec> 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<data_table::TableEvent> 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
|