diff --git a/views_jobs.cpp b/views_jobs.cpp index c0cb8f9..5a99462 100644 --- a/views_jobs.cpp +++ b/views_jobs.cpp @@ -7,11 +7,11 @@ // 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. -// Rationale: data_table::State does not expose selected_row_idx yet -// (Phase 2 feature); a dedicated actions bar keeps all action logic -// isolated and works with any filter state in the main table. -// TODO(0081-J Phase 2): when data_table::State gets selected_row_idx, -// migrate actions to option (b) toolbar per selected row. +// 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" @@ -111,96 +111,6 @@ void views_jobs(AppState& app) { ImGui::Separator(); - // ------------------------------------------------------------------------- - // Actions mini-table (option a — Fase 1; TODO: migrate to selected-row - // toolbar when data_table::State exposes selected_row_idx in Phase 2). - // - // Shows Cancel/Delete buttons for every row that passes the current filter. - // Fixed-height child so it doesn't push the data_table below the panel. - // We pre-compute the list of actionable rows (active or terminal). - // ------------------------------------------------------------------------- - - // Collect visible rows for actions. - std::vector visible; - visible.reserve(g_jobs_cache.rows.size()); - for (const auto& r : g_jobs_cache.rows) { - if (filter_match(r.status, g_jobs_cache.filter_idx)) - visible.push_back(&r); - } - - // Actions bar height: up to 5 rows visible, ~22px each + header ~22px. - // Only render if there are any rows with actionable buttons. - bool any_actionable = false; - for (const auto* rp : visible) { - if (rp->status == "queued" || rp->status == "running" || - rp->status == "done" || rp->status == "error" || - rp->status == "cancelled") { - any_actionable = true; - break; - } - } - - if (any_actionable && !visible.empty()) { - int visible_rows = (int)visible.size(); - int capped = visible_rows < 5 ? visible_rows : 5; - float row_h = ImGui::GetTextLineHeightWithSpacing(); - float child_h = row_h * (float)(capped + 1) + ImGui::GetStyle().ItemSpacing.y * 2.0f; - child_h = child_h < 120.0f ? child_h : 120.0f; // hard cap - - ImGui::TextDisabled("Actions"); - if (ImGui::BeginChild("##jobs_actions", ImVec2(0.0f, child_h), - ImGuiChildFlags_Borders)) { - ImGuiTableFlags aflags = ImGuiTableFlags_Borders | - ImGuiTableFlags_RowBg | - ImGuiTableFlags_SizingStretchProp | - ImGuiTableFlags_ScrollY; - if (ImGui::BeginTable("##jobs_actions_tbl", 3, aflags)) { - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Job", ImGuiTableColumnFlags_WidthStretch, 3.0f); - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 70.0f); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 64.0f); - ImGui::TableHeadersRow(); - - for (const auto* rp : visible) { - const JobRow& r = *rp; - ImGui::PushID(r.id.c_str()); - ImGui::TableNextRow(); - - // Job label (enricher + target truncated). - ImGui::TableSetColumnIndex(0); - std::string label = r.enricher_id; - if (!r.node_name.empty()) label += " / " + r.node_name; - else if (!r.node_id.empty()) label += " / " + r.node_id; - if (label.size() > 40) label = label.substr(0, 38) + "…"; - ImGui::TextUnformatted(label.c_str()); - - // Status badge. - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(status_color(r.status), "%s", r.status.c_str()); - - // Action button. - ImGui::TableSetColumnIndex(2); - if (r.status == "queued" || r.status == "running") { - if (ImGui::SmallButton("Cancel")) { - jobs_cancel(r.id.c_str()); - // Invalidate cache immediately. - g_jobs_cache.last_frame_refresh = -100; - } - } else { - if (ImGui::SmallButton("Delete")) { - jobs_delete(r.id.c_str()); - g_jobs_cache.last_frame_refresh = -100; - } - } - ImGui::PopID(); - } - ImGui::EndTable(); - } - } - ImGui::EndChild(); - ImGui::Spacing(); - } - // ------------------------------------------------------------------------- // Main data table via data_table::render (issue 0081-J migration). // @@ -210,21 +120,25 @@ void views_jobs(AppState& app) { // 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" + "status", "enricher", "target", "progress", "time_ms", "cancel", "delete" }; - constexpr int k_ncols = 5; + constexpr int k_ncols = 7; static std::vector s_cell_backing; static std::vector s_cells; - static int s_rows_cached = -1; + 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. @@ -244,11 +158,14 @@ void views_jobs(AppState& app) { 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; @@ -274,6 +191,18 @@ void views_jobs(AppState& app) { 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) @@ -317,6 +246,18 @@ void views_jobs(AppState& app) { 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; }(); @@ -324,16 +265,13 @@ void views_jobs(AppState& app) { float avail_h = ImGui::GetContentRegionAvail().y - 4.0f; if (avail_h < 80.0f) avail_h = 80.0f; - ImGui::TextDisabled("Data"); ImGui::BeginChild("##jobs_data", ImVec2(0.0f, avail_h), ImGuiChildFlags_None); - int nrows_vis = s_rows_cached; // total cached — filtered subset - // Recompute actual filtered row count for the data child. + // 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; } - (void)nrows_vis; if (nrows_data == 0) { ImGui::TextDisabled("(no jobs match current filter)"); @@ -347,13 +285,30 @@ void views_jobs(AppState& app) { 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; - data_table::render("##jobs_dt", {tbl}, app.jobs_dt_state); + 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();