absorb jobs_actions into main Jobs table via Button renderer (Fase 2, issue 0081-J)
- 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.
This commit is contained in:
+58
-103
@@ -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<const JobRow*> 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<std::string> 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<std::string> s_cell_backing;
|
||||
static std::vector<const char*> s_cells;
|
||||
static int s_rows_cached = -1;
|
||||
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.
|
||||
@@ -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<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();
|
||||
|
||||
Reference in New Issue
Block a user