feat(alerts): Steward's own confirm dialog, a verdict-first rules list, and a rule page in three sections (#5314, #5315, #5317)
Confirm dialog (#5314): - Every data-confirm form now asks in one native <dialog> (base.html + static/confirm.js) instead of window.confirm. - The question becomes the heading and what follows it the detail; the act's button is named after the form's own button; Cancel has the focus and Esc backs out. Optional data-confirm-title/-detail/-action/-dismiss attributes. - All 27 existing uses work as they are. Cancelling a run says 'Keep running'. Alerts list (#5315): - A verdict line (what is firing, how many rules, where notifications go, or 'notifies nobody yet' with a link) replaces the empty Active card. - Each rule's name opens it; rows show 'last fired' and fold fire/clear into the condition; History and Disable stay; Delete leaves the list. - .docker-verdict becomes the shared .page-verdict. Rule page (#5317): - Named for the rule (title and crumb), with when it last fired and how many changes of state it holds. - Three sections: what it watches, when it fires (checks side by side, seconds echoed as a duration), what happens (where notifications go, no-data, the playbook, whose fields stay folded until ticked). - Delete at the foot, behind the dialog, saying its history goes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// Steward's own confirm dialog (#5314): a form, or its submit button, with
|
||||
// data-confirm="Question? What follows." asks in the #confirm-dialog that
|
||||
// base.html renders before it submits, never through window.confirm.
|
||||
//
|
||||
// The prompt is attribute text read through dataset and written with
|
||||
// textContent, never parsed as HTML or JavaScript, so a name inside it
|
||||
// cannot run as script (#5099). Optional attributes beside data-confirm:
|
||||
// data-confirm-title the heading (default: the prompt up to its first "? ")
|
||||
// data-confirm-detail the line under it (default: the rest of the prompt)
|
||||
// data-confirm-action the button that goes ahead (default: the submit
|
||||
// button's own label, or the prompt's first word)
|
||||
// data-confirm-dismiss the button that backs out (default: "Cancel")
|
||||
// Cancel has the focus, and Esc backs out.
|
||||
(function () {
|
||||
function label(btn, title) {
|
||||
var own = btn ? (btn.dataset.confirmAction || btn.textContent || btn.value || '').trim() : '';
|
||||
if (own.length > 2) return own; // "×" and other icons say nothing
|
||||
return title.split(' ')[0].replace(/\?$/, '');
|
||||
}
|
||||
|
||||
function ask(form, btn) {
|
||||
var src = btn && btn.dataset.confirm ? btn : form;
|
||||
var prompt = src.dataset.confirm;
|
||||
var cut = prompt.indexOf('? ');
|
||||
var title = src.dataset.confirmTitle || (cut > 0 ? prompt.slice(0, cut + 1) : prompt);
|
||||
var detail = src.dataset.confirmDetail || (cut > 0 ? prompt.slice(cut + 2) : '');
|
||||
var d = document.getElementById('confirm-dialog');
|
||||
d.querySelector('[data-part="title"]').textContent = title;
|
||||
var p = d.querySelector('[data-part="detail"]');
|
||||
p.textContent = detail;
|
||||
p.hidden = !detail;
|
||||
d.querySelector('[data-part="go"]').textContent = src.dataset.confirmAction || label(btn, title);
|
||||
d.querySelector('[data-part="back"]').textContent = src.dataset.confirmDismiss || 'Cancel';
|
||||
d.returnValue = '';
|
||||
d.onclose = function () {
|
||||
if (d.returnValue !== 'go') return;
|
||||
form.dataset.confirmed = '1';
|
||||
form.requestSubmit(btn && btn.form === form ? btn : undefined);
|
||||
};
|
||||
d.showModal();
|
||||
}
|
||||
|
||||
document.addEventListener('submit', function (ev) {
|
||||
var form = ev.target, btn = ev.submitter;
|
||||
if (!((btn && btn.dataset.confirm) || form.dataset.confirm)) return;
|
||||
if (form.dataset.confirmed === '1') { delete form.dataset.confirmed; return; } // the asked-for submit
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation(); // before HTMX sees it: an hx-post form waits for the answer too
|
||||
ask(form, btn);
|
||||
}, true);
|
||||
})();
|
||||
@@ -208,7 +208,8 @@ strong, b, th { font-weight: 500; }
|
||||
.cell-sub { font-size: 0.75rem; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.table td.cell-image { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; color: var(--text-muted); }
|
||||
/* The Docker page's one-line answer, above its tiles (#5298). */
|
||||
.docker-verdict { margin: 0 0 0.75rem; font-size: 0.95rem; }
|
||||
.page-verdict { margin: 0 0 0.75rem; font-size: 0.95rem; }
|
||||
.table tr.row-off > td { opacity: 0.55; }
|
||||
/* A problem section's heading carries its own count, so no tile repeats it (#5214). */
|
||||
.section-count { margin-bottom: 0.6rem; }
|
||||
.section-count > span:first-child { margin-left: 0.25rem; }
|
||||
@@ -373,6 +374,20 @@ a.stat-tile:hover { border-color: var(--border-mid); background: var(--bg-elevat
|
||||
border: 1px solid var(--border); border-radius: var(--fs-radius-md); box-shadow: var(--fs-shadow-3); }
|
||||
.modal::backdrop { background: var(--fs-overlay); }
|
||||
.modal-title { font-family: var(--font-serif); font-size: 1.25rem; font-weight: 500; margin: 0 0 1rem; }
|
||||
/* The confirm dialog (#5314): a question, what follows from it, then Cancel and the act. */
|
||||
.modal-confirm { width: min(440px, calc(100vw - 2rem)); }
|
||||
.modal-confirm .modal-title { margin-bottom: 0.5rem; overflow-wrap: anywhere; }
|
||||
.confirm-detail { color: var(--text-muted); font-size: 0.9rem; margin: 0 0 1.25rem; overflow-wrap: anywhere; }
|
||||
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
|
||||
/* A form page in sections (#5317): capped, left-aligned under its crumbs. */
|
||||
.form-page { max-width: 760px; }
|
||||
.form-section > .section-title { margin-bottom: 0.9rem; }
|
||||
.form-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); gap: 0 1rem; }
|
||||
.form-pair[hidden] { display: none; }
|
||||
.ansible-fields { padding: 0.9rem 1rem 0.25rem; margin-bottom: 0.75rem; border: 1px solid var(--border); border-radius: var(--fs-radius-md); background: var(--bg-elevated); }
|
||||
.form-actions { display: flex; gap: 0.75rem; margin: 0.5rem 0 1.5rem; }
|
||||
.danger-zone { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
|
||||
border-color: color-mix(in srgb, var(--fs-error) 45%, var(--border)); }
|
||||
/* A filled bar for a percentage; the fill takes its grade from status_class. */
|
||||
.meter { height: 8px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
||||
.meter-fill { height: 100%; }
|
||||
|
||||
Reference in New Issue
Block a user