Auto-commit local changes before build (2026-01-04 12:34:15)
This commit is contained in:
parent
843e01e1e6
commit
a1b8dfe5cf
@ -1 +1 @@
|
|||||||
v20260104-04-reports-html-jobs-table-fix
|
v20260104-05-reports-html-jobs-columns-selection
|
||||||
|
|||||||
@ -192,6 +192,7 @@ def build_report_columns_meta():
|
|||||||
"views": [
|
"views": [
|
||||||
{"key": "summary", "label": "Summary"},
|
{"key": "summary", "label": "Summary"},
|
||||||
{"key": "snapshot", "label": "Snapshot"},
|
{"key": "snapshot", "label": "Snapshot"},
|
||||||
|
{"key": "jobs", "label": "Jobs"},
|
||||||
],
|
],
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"summary": [
|
"summary": [
|
||||||
@ -220,6 +221,18 @@ def build_report_columns_meta():
|
|||||||
"remark",
|
"remark",
|
||||||
"reviewed_at",
|
"reviewed_at",
|
||||||
],
|
],
|
||||||
|
"jobs": [
|
||||||
|
"object_name",
|
||||||
|
"job_name",
|
||||||
|
"backup_software",
|
||||||
|
"backup_type",
|
||||||
|
"run_at",
|
||||||
|
"status",
|
||||||
|
"missed",
|
||||||
|
"override_applied",
|
||||||
|
"ticket_number",
|
||||||
|
"remark",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
@ -238,35 +251,35 @@ def build_report_columns_meta():
|
|||||||
{
|
{
|
||||||
"name": "Job Information",
|
"name": "Job Information",
|
||||||
"items": [
|
"items": [
|
||||||
{"key": "job_name", "label": "Job name", "views": ["snapshot"]},
|
{"key": "job_name", "label": "Job name", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "job_id", "label": "Job ID", "views": ["snapshot"]},
|
{"key": "job_id", "label": "Job ID", "views": ["snapshot"]},
|
||||||
{"key": "backup_software", "label": "Job type", "views": ["snapshot"]},
|
{"key": "backup_software", "label": "Software", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "backup_type", "label": "Repository / Target", "views": ["snapshot"]},
|
{"key": "backup_type", "label": "Type", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "customer_name", "label": "Customer", "views": ["snapshot", "summary"]},
|
{"key": "customer_name", "label": "Customer", "views": ["snapshot", "summary", "jobs"]},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Status",
|
"name": "Status",
|
||||||
"items": [
|
"items": [
|
||||||
{"key": "status", "label": "Last run status", "views": ["snapshot"]},
|
{"key": "status", "label": "Status", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "missed", "label": "Missed", "views": ["snapshot"]},
|
{"key": "missed", "label": "Missed", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "override_applied", "label": "Override applied", "views": ["snapshot"]},
|
{"key": "override_applied", "label": "Override", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "run_id", "label": "Run ID", "views": ["snapshot"]},
|
{"key": "run_id", "label": "Run ID", "views": ["snapshot"]},
|
||||||
{"key": "ticket_number", "label": "Ticket number", "views": ["snapshot"]},
|
{"key": "ticket_number", "label": "Ticket", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "remark", "label": "Remark", "views": ["snapshot"]},
|
{"key": "remark", "label": "Remark", "views": ["snapshot", "jobs"]},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Time",
|
"name": "Time",
|
||||||
"items": [
|
"items": [
|
||||||
{"key": "run_at", "label": "Start time", "views": ["snapshot"]},
|
{"key": "run_at", "label": "Last run", "views": ["snapshot", "jobs"]},
|
||||||
{"key": "reviewed_at", "label": "Reviewed at", "views": ["snapshot"]},
|
{"key": "reviewed_at", "label": "Reviewed at", "views": ["snapshot"]},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Object",
|
"name": "Object",
|
||||||
"items": [
|
"items": [
|
||||||
{"key": "object_name", "label": "Object name", "views": ["snapshot", "summary"]},
|
{"key": "object_name", "label": "Object", "views": ["snapshot", "summary", "jobs"]},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1103,6 +1116,68 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
|
|||||||
include_customers = html_content in ("customers", "both")
|
include_customers = html_content in ("customers", "both")
|
||||||
include_jobs = html_content in ("jobs", "both")
|
include_jobs = html_content in ("jobs", "both")
|
||||||
|
|
||||||
|
cols_meta = build_report_columns_meta()
|
||||||
|
label_map: dict[str, str] = {}
|
||||||
|
allowed_by_view: dict[str, set[str]] = {}
|
||||||
|
for g in (cols_meta.get("groups") or []):
|
||||||
|
for it in (g.get("items") or []):
|
||||||
|
k = (it.get("key") or "").strip()
|
||||||
|
if not k:
|
||||||
|
continue
|
||||||
|
label_map[k] = (it.get("label") or k)
|
||||||
|
for v in (it.get("views") or []):
|
||||||
|
allowed_by_view.setdefault(v, set()).add(k)
|
||||||
|
|
||||||
|
def _selected_cols(view_key: str) -> list[str]:
|
||||||
|
cols = []
|
||||||
|
rc_cols = rc.get("columns") if isinstance(rc, dict) else None
|
||||||
|
if isinstance(rc_cols, dict):
|
||||||
|
v = rc_cols.get(view_key)
|
||||||
|
if isinstance(v, list):
|
||||||
|
cols = [str(x).strip() for x in v if str(x).strip()]
|
||||||
|
|
||||||
|
if not cols:
|
||||||
|
d = (cols_meta.get("defaults") or {}).get(view_key)
|
||||||
|
if isinstance(d, list):
|
||||||
|
cols = [str(x).strip() for x in d if str(x).strip()]
|
||||||
|
|
||||||
|
allowed = allowed_by_view.get(view_key) or set()
|
||||||
|
cols = [c for c in cols if c in allowed]
|
||||||
|
if not cols:
|
||||||
|
cols = [c for c in (cols_meta.get("defaults") or {}).get(view_key, []) if c in allowed]
|
||||||
|
return cols
|
||||||
|
|
||||||
|
def _td(value: str, cls: str = "") -> str:
|
||||||
|
cl = f" class='{cls}'" if cls else ""
|
||||||
|
return f"<td{cl}>{value}</td>"
|
||||||
|
|
||||||
|
def _th(label: str, cls: str = "") -> str:
|
||||||
|
cl = f" class='{cls}'" if cls else ""
|
||||||
|
return f"<th{cl}>{label}</th>"
|
||||||
|
|
||||||
|
def _render_table(view_key: str, rows: list[dict]) -> tuple[str, str]:
|
||||||
|
cols = _selected_cols(view_key)
|
||||||
|
|
||||||
|
th = []
|
||||||
|
for k in cols:
|
||||||
|
cls = "text-end" if k in ("missed", "override_applied") else ""
|
||||||
|
th.append(_th(_esc(label_map.get(k) or k), cls))
|
||||||
|
|
||||||
|
tr = []
|
||||||
|
for r in rows:
|
||||||
|
tds = []
|
||||||
|
for k in cols:
|
||||||
|
if k in ("missed", "override_applied"):
|
||||||
|
v = "1" if bool(r.get(k)) else "0"
|
||||||
|
tds.append(_td(_esc(v), "text-end"))
|
||||||
|
elif k == "run_at":
|
||||||
|
tds.append(_td(_esc(r.get(k) or ""), "text-muted small"))
|
||||||
|
else:
|
||||||
|
tds.append(_td(_esc(r.get(k) or "")))
|
||||||
|
tr.append("<tr>" + "".join(tds) + "</tr>")
|
||||||
|
|
||||||
|
return "".join(th), "\n".join(tr)
|
||||||
|
|
||||||
# Snapshot preview table can be requested explicitly via view=snapshot.
|
# Snapshot preview table can be requested explicitly via view=snapshot.
|
||||||
want_snapshot_table = (view or "summary").strip().lower() == "snapshot"
|
want_snapshot_table = (view or "summary").strip().lower() == "snapshot"
|
||||||
|
|
||||||
@ -1157,22 +1232,7 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
job_row_html = []
|
jobs_th, jobs_tr = _render_table("jobs", jobs_rows)
|
||||||
for j in jobs_rows:
|
|
||||||
job_row_html.append(
|
|
||||||
"<tr>"
|
|
||||||
f"<td>{_esc(j['object_name'])}</td>"
|
|
||||||
f"<td>{_esc(j['job_name'])}</td>"
|
|
||||||
f"<td>{_esc(j['backup_software'])}</td>"
|
|
||||||
f"<td>{_esc(j['backup_type'])}</td>"
|
|
||||||
f"<td class='text-muted small'>{_esc(j['run_at'])}</td>"
|
|
||||||
f"<td>{_esc(j['status'])}</td>"
|
|
||||||
f"<td class='text-end'>{'1' if j['missed'] else '0'}</td>"
|
|
||||||
f"<td class='text-end'>{'1' if j['override_applied'] else '0'}</td>"
|
|
||||||
f"<td>{_esc(j['ticket_number'])}</td>"
|
|
||||||
f"<td>{_esc(j['remark'])}</td>"
|
|
||||||
"</tr>"
|
|
||||||
)
|
|
||||||
|
|
||||||
jobs_table_html = """<div class="row g-3 mb-3">
|
jobs_table_html = """<div class="row g-3 mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -1185,21 +1245,10 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover">
|
<table class="table table-sm table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>{jobs_th}</tr>
|
||||||
<th>Object</th>
|
|
||||||
<th>Job</th>
|
|
||||||
<th>Software</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Last run</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th class="text-end">Missed</th>
|
|
||||||
<th class="text-end">Override</th>
|
|
||||||
<th>Ticket</th>
|
|
||||||
<th>Remark</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
""" + "\n".join(job_row_html) + """</tbody>
|
""" + jobs_tr + """</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1252,23 +1301,7 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
row_html = []
|
snap_th, snap_tr = _render_table("snapshot", snap_rows)
|
||||||
for s in snap_rows:
|
|
||||||
row_html.append(
|
|
||||||
"<tr>"
|
|
||||||
f"<td>{_esc(s['object_name'])}</td>"
|
|
||||||
f"<td>{_esc(s['customer_name'])}</td>"
|
|
||||||
f"<td>{_esc(s['job_name'])}</td>"
|
|
||||||
f"<td>{_esc(s['backup_software'])}</td>"
|
|
||||||
f"<td>{_esc(s['backup_type'])}</td>"
|
|
||||||
f"<td class='text-muted small'>{_esc(s['run_at'])}</td>"
|
|
||||||
f"<td>{_esc(s['status'])}</td>"
|
|
||||||
f"<td class='text-end'>{'1' if s['missed'] else '0'}</td>"
|
|
||||||
f"<td class='text-end'>{'1' if s['override_applied'] else '0'}</td>"
|
|
||||||
f"<td>{_esc(s['ticket_number'])}</td>"
|
|
||||||
f"<td>{_esc(s['remark'])}</td>"
|
|
||||||
"</tr>"
|
|
||||||
)
|
|
||||||
|
|
||||||
snapshot_table_html = """<div class="row g-3">
|
snapshot_table_html = """<div class="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -1281,22 +1314,10 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-striped">
|
<table class="table table-sm table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>{snap_th}</tr>
|
||||||
<th>Object</th>
|
|
||||||
<th>Customer</th>
|
|
||||||
<th>Job</th>
|
|
||||||
<th>Software</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Run at</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th class="text-end">Missed</th>
|
|
||||||
<th class="text-end">Override</th>
|
|
||||||
<th>Ticket</th>
|
|
||||||
<th>Remark</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
""" + "\n".join(row_html) + """</tbody>
|
""" + snap_tr + """</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -166,6 +166,7 @@
|
|||||||
<div class="btn-group" role="group" aria-label="Report content view selector">
|
<div class="btn-group" role="group" aria-label="Report content view selector">
|
||||||
<button type="button" class="btn btn-outline-secondary active" id="rep_cols_tab_summary">Summary</button>
|
<button type="button" class="btn btn-outline-secondary active" id="rep_cols_tab_summary">Summary</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" id="rep_cols_tab_snapshot">Snapshot</button>
|
<button type="button" class="btn btn-outline-secondary" id="rep_cols_tab_snapshot">Snapshot</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="rep_cols_tab_jobs">Jobs</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small" id="rep_cols_hint">Select columns for the summary view.</div>
|
<div class="text-muted small" id="rep_cols_hint">Select columns for the summary view.</div>
|
||||||
</div>
|
</div>
|
||||||
@ -274,13 +275,14 @@
|
|||||||
// --- Report content / column selector ---
|
// --- Report content / column selector ---
|
||||||
var repColsView = 'summary';
|
var repColsView = 'summary';
|
||||||
var repColsMeta = window.__reportColumnsMeta || null;
|
var repColsMeta = window.__reportColumnsMeta || null;
|
||||||
var repColsSelected = { summary: [], snapshot: [] };
|
var repColsSelected = { summary: [], snapshot: [], jobs: [] };
|
||||||
|
|
||||||
if (isEdit && initialReport && initialReport.report_config && initialReport.report_config.columns) {
|
if (isEdit && initialReport && initialReport.report_config && initialReport.report_config.columns) {
|
||||||
var cols = initialReport.report_config.columns;
|
var cols = initialReport.report_config.columns;
|
||||||
repColsSelected = {
|
repColsSelected = {
|
||||||
summary: Array.isArray(cols.summary) ? cols.summary.slice() : [],
|
summary: Array.isArray(cols.summary) ? cols.summary.slice() : [],
|
||||||
snapshot: Array.isArray(cols.snapshot) ? cols.snapshot.slice() : [],
|
snapshot: Array.isArray(cols.snapshot) ? cols.snapshot.slice() : [],
|
||||||
|
jobs: Array.isArray(cols.jobs) ? cols.jobs.slice() : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,23 +394,20 @@
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
function colsHintText(viewKey) {
|
function colsHintText(viewKey) {
|
||||||
return viewKey === 'snapshot'
|
if (viewKey === 'snapshot') return 'Select columns for the snapshot view.';
|
||||||
? 'Select columns for the snapshot view.'
|
if (viewKey === 'jobs') return 'Select columns for the jobs view (HTML/PDF).';
|
||||||
: 'Select columns for the summary view.';
|
return 'Select columns for the summary view.';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setColsView(viewKey) {
|
function setColsView(viewKey) {
|
||||||
repColsView = (viewKey === 'snapshot') ? 'snapshot' : 'summary';
|
repColsView = (viewKey === 'snapshot' || viewKey === 'jobs') ? viewKey : 'summary';
|
||||||
|
|
||||||
var a = qs('rep_cols_tab_summary');
|
var a = qs('rep_cols_tab_summary');
|
||||||
var b = qs('rep_cols_tab_snapshot');
|
var b = qs('rep_cols_tab_snapshot');
|
||||||
if (repColsView === 'summary') {
|
var c = qs('rep_cols_tab_jobs');
|
||||||
a.classList.add('active');
|
a.classList.toggle('active', repColsView === 'summary');
|
||||||
b.classList.remove('active');
|
b.classList.toggle('active', repColsView === 'snapshot');
|
||||||
} else {
|
c.classList.toggle('active', repColsView === 'jobs');
|
||||||
b.classList.add('active');
|
|
||||||
a.classList.remove('active');
|
|
||||||
}
|
|
||||||
qs('rep_cols_hint').textContent = colsHintText(repColsView);
|
qs('rep_cols_hint').textContent = colsHintText(repColsView);
|
||||||
|
|
||||||
renderColsAvailable();
|
renderColsAvailable();
|
||||||
@ -435,7 +434,7 @@
|
|||||||
|
|
||||||
function ensureDefaultsFromMeta() {
|
function ensureDefaultsFromMeta() {
|
||||||
if (!repColsMeta || !repColsMeta.defaults) return;
|
if (!repColsMeta || !repColsMeta.defaults) return;
|
||||||
['summary', 'snapshot'].forEach(function (v) {
|
['summary', 'snapshot', 'jobs'].forEach(function (v) {
|
||||||
if (!repColsSelected[v] || !repColsSelected[v].length) {
|
if (!repColsSelected[v] || !repColsSelected[v].length) {
|
||||||
repColsSelected[v] = (repColsMeta.defaults[v] || []).slice();
|
repColsSelected[v] = (repColsMeta.defaults[v] || []).slice();
|
||||||
}
|
}
|
||||||
@ -612,6 +611,7 @@
|
|||||||
ensureDefaultsFromMeta();
|
ensureDefaultsFromMeta();
|
||||||
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
|
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
|
||||||
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
|
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
|
||||||
|
qs('rep_cols_tab_jobs').addEventListener('click', function () { setColsView('jobs'); });
|
||||||
setColsView('summary');
|
setColsView('summary');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -635,6 +635,7 @@
|
|||||||
// bind tabs once metadata is ready
|
// bind tabs once metadata is ready
|
||||||
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
|
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
|
||||||
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
|
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
|
||||||
|
qs('rep_cols_tab_jobs').addEventListener('click', function () { setColsView('jobs'); });
|
||||||
|
|
||||||
setColsView('summary');
|
setColsView('summary');
|
||||||
})
|
})
|
||||||
|
|||||||
@ -217,6 +217,15 @@
|
|||||||
- Restored the jobs table below the charts in HTML reports for single-customer selections.
|
- Restored the jobs table below the charts in HTML reports for single-customer selections.
|
||||||
- Ensured the latest snapshot per job is displayed correctly in the HTML output.
|
- Ensured the latest snapshot per job is displayed correctly in the HTML output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v20260104-05-reports-html-jobs-columns-selection
|
||||||
|
|
||||||
|
- Added configurable column selection for the Jobs table in report create/edit views.
|
||||||
|
- Exposed all Jobs table columns as selectable options instead of using fixed/hardcoded columns.
|
||||||
|
- Ensured the HTML report Jobs table only renders columns explicitly selected in the report configuration.
|
||||||
|
- Aligned Jobs table rendering logic with Snapshot and Summary column selection behavior.
|
||||||
|
|
||||||
================================================================================================================================================
|
================================================================================================================================================
|
||||||
|
|
||||||
## v0.1.15
|
## v0.1.15
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user