tabs: render schedules as a single table with tilted agent columns (closes #535)
Replaces the per-schedule card layout with one table:
| # | src | next | every | owner | body | …agents… | actions |
|---|-----|------|-------|-------|------|----------|---------|
| 5 | op | 5m | 10m | mara | "…" | ✓ ✓ . ✓ | ↯ ✎ ✕ |
Each schedule is one <tr> in the tbody. Agent columns are dynamic —
operator + manager + live containers + any "extra" name that appears
as a target on some schedule but isn't a current container (same
membership rule buildTargetChips uses, so the table + new/edit forms
agree on what's addressable). Per-agent cells:
- active target → <button>✓</button> that cancels just that target
on click (replaces the per-row ✕ from the old targets sub-table)
- cancelled target → muted ✕ glyph (no button; re-add flows through
the edit form's targets multi-select)
- not a target → empty cell
Agent column headers tilt -45° via CSS so each column reads as 28px
of horizontal real estate instead of the full word width. Standard
rotated-header pattern: 95px-tall <th> with position:relative, inner
<div> positioned absolute at bottom-left, transform rotates about
left-bottom.
Body cell truncates with ellipsis + full-text title. Description used
to be a separate visible block on the card layout; the table folds
it into the body cell's title to keep row height tight. If mara wants
description visible in-table it's a small follow-up — easier to
iterate on a rejection.
Edit form expands inline into a colspan'd row underneath the schedule
row it edits (instead of inside the card). Wrapper drops the form's
background so it reads as a row extension.
No backend changes; everything renders from existing schedulesState
+ containersState.
This commit is contained in:
parent
78a503d8b9
commit
42a2503834
2 changed files with 277 additions and 141 deletions
|
|
@ -1759,60 +1759,112 @@ body.flow-shell .tabbar .tab.active.tab-link {
|
||||||
}
|
}
|
||||||
.schedule-interval-preview-oneshot { color: var(--muted); font-style: italic; }
|
.schedule-interval-preview-oneshot { color: var(--muted); font-style: italic; }
|
||||||
|
|
||||||
.schedules {
|
/* #535 — schedules-as-table. One row per schedule, attribute columns
|
||||||
display: flex;
|
on the left, one ✓/✕ column per agent in the middle, actions
|
||||||
flex-direction: column;
|
column on the right. Agent column headers tilt -45° so a stack of
|
||||||
gap: 0.5em;
|
short agent names fits in ~28px each. */
|
||||||
}
|
.schedules-table {
|
||||||
.schedule-row {
|
|
||||||
padding: 0.6em 0.9em;
|
|
||||||
background: rgba(24, 24, 37, 0.55);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4em;
|
|
||||||
}
|
|
||||||
.schedule-row.schedule-cancelled { opacity: 0.55; }
|
|
||||||
.schedule-head {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4em;
|
|
||||||
}
|
|
||||||
.schedule-description {
|
|
||||||
color: var(--muted);
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
.schedule-body {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0.5em 0.7em;
|
|
||||||
}
|
|
||||||
.schedule-targets-table {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
.schedule-targets-table th,
|
.schedules-table thead th {
|
||||||
.schedule-targets-table td {
|
vertical-align: bottom;
|
||||||
padding: 0.25em 0.6em;
|
padding: 0.3em 0.5em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.schedule-targets-table th {
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.schedules-table-id { width: 3em; }
|
||||||
|
.schedules-table-body-th { min-width: 12em; }
|
||||||
|
.schedules-table-actions-th { width: 7em; }
|
||||||
|
/* Tilted agent column headers (#535). Header cell is narrow (~28px)
|
||||||
|
and tall (~90px); the inner <div> rotates -45° about its bottom-left
|
||||||
|
corner, with a translate to slide the text up alongside the cell
|
||||||
|
border. The inner <span> carries the actual baseline so the
|
||||||
|
underline (border-bottom on the cell) aligns with the rotated text. */
|
||||||
|
.schedules-table-agent-th {
|
||||||
|
width: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
height: 95px;
|
||||||
|
padding: 0 !important;
|
||||||
|
vertical-align: bottom;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-transform: none !important;
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.schedules-table-agent-th > div {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 14px;
|
||||||
|
transform-origin: left bottom;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
font-family: ui-monospace, 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.schedules-table-agent-th > div > span {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 4px 1px;
|
||||||
|
}
|
||||||
|
.schedules-table tbody td {
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.schedules-table-row-cancelled td { opacity: 0.55; }
|
||||||
|
.schedules-table-body-cell {
|
||||||
|
max-width: 30em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: ui-monospace, 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.schedules-table-check {
|
||||||
|
width: 28px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.3em 0 !important;
|
||||||
|
}
|
||||||
|
.schedules-table-check-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--green);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.schedules-table-check-btn:hover:not(:disabled) {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
.schedules-table-check-btn:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.schedules-table-check-cancelled {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.schedules-table-actions {
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.schedules-table-actions .btn-inline-small {
|
||||||
|
margin-left: 0.3em;
|
||||||
|
}
|
||||||
|
/* Edit form expands inline into a colspan'd row underneath the
|
||||||
|
schedule it edits. Wrapper drops the form's normal background so it
|
||||||
|
reads as "an extension of the row above" instead of a free-floating
|
||||||
|
card. */
|
||||||
|
.schedules-table-edit-row > td {
|
||||||
|
padding: 0.5em 0.8em 0.8em !important;
|
||||||
|
background: rgba(24, 24, 37, 0.4);
|
||||||
}
|
}
|
||||||
.schedule-targets-table tr:last-child td { border-bottom: 0; }
|
|
||||||
.schedule-target-cancelled td { opacity: 0.5; }
|
|
||||||
.btn-inline-small {
|
.btn-inline-small {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
padding: 0.1em 0.45em;
|
padding: 0.1em 0.45em;
|
||||||
|
|
|
||||||
|
|
@ -2313,6 +2313,11 @@ window.marked = marked;
|
||||||
if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = originalLabel; }
|
if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = originalLabel; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// #535: schedules render as a single table — one row per schedule,
|
||||||
|
// attribute columns + one ✓/✕ column per agent (tilted 45° header
|
||||||
|
// so a row of agents takes ~28px each instead of full word width),
|
||||||
|
// actions column on the right. Edit form expands into a colspan'd
|
||||||
|
// row underneath when its row's `✎` is toggled on.
|
||||||
function renderSchedulesList() {
|
function renderSchedulesList() {
|
||||||
const liveRoot = $('schedules-section');
|
const liveRoot = $('schedules-section');
|
||||||
if (!liveRoot) return;
|
if (!liveRoot) return;
|
||||||
|
|
@ -2321,118 +2326,186 @@ window.marked = marked;
|
||||||
root.append(el('p', { class: 'empty' }, 'no schedules queued'));
|
root.append(el('p', { class: 'empty' }, 'no schedules queued'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ul = el('ul', { class: 'schedules' });
|
const agents = schedulesTableAgentSet();
|
||||||
// Active schedules first (still firing), then cancelled tail.
|
const table = el('table', { class: 'schedules-table' });
|
||||||
|
table.append(renderSchedulesTableHead(agents));
|
||||||
|
const tbody = el('tbody', {});
|
||||||
const sorted = schedulesState.slice().sort((a, b) => {
|
const sorted = schedulesState.slice().sort((a, b) => {
|
||||||
const aDone = a.cancelled_at_unix ? 1 : 0;
|
const aDone = a.cancelled_at_unix ? 1 : 0;
|
||||||
const bDone = b.cancelled_at_unix ? 1 : 0;
|
const bDone = b.cancelled_at_unix ? 1 : 0;
|
||||||
if (aDone !== bDone) return aDone - bDone;
|
if (aDone !== bDone) return aDone - bDone;
|
||||||
return a.next_fire_at_unix - b.next_fire_at_unix;
|
return a.next_fire_at_unix - b.next_fire_at_unix;
|
||||||
});
|
});
|
||||||
for (const s of sorted) ul.append(renderScheduleCard(s));
|
for (const s of sorted) {
|
||||||
root.append(ul);
|
tbody.append(renderScheduleRow(s, agents));
|
||||||
});
|
if (editingSchedules.has(s.id) && !s.cancelled_at_unix) {
|
||||||
}
|
tbody.append(renderScheduleEditRow(s, agents));
|
||||||
function renderScheduleCard(s) {
|
|
||||||
const cancelled = !!s.cancelled_at_unix;
|
|
||||||
const li = el('li', { class: 'schedule-row' + (cancelled ? ' schedule-cancelled' : '') });
|
|
||||||
const head = el('div', { class: 'schedule-head' });
|
|
||||||
head.append(
|
|
||||||
el('span', { class: 'meta' }, '#' + s.id), ' ',
|
|
||||||
el('span', { class: 'rqe-source rqe-source-' + (s.source && s.source.kind === 'approval' ? 'approval' : 'manual') },
|
|
||||||
s.source && s.source.kind === 'approval' ? 'approval' : 'operator'),
|
|
||||||
);
|
|
||||||
if (cancelled) {
|
|
||||||
head.append(' ', el('span', { class: 'badge badge-muted', title: 'cancelled ' + new Date(s.cancelled_at_unix * 1000).toISOString() }, 'cancelled'));
|
|
||||||
} else {
|
|
||||||
const dueIn = s.next_fire_at_unix - Math.floor(Date.now() / 1000);
|
|
||||||
const dueLabel = dueIn <= 0
|
|
||||||
? `overdue ${fmtAgo(s.next_fire_at_unix)}`
|
|
||||||
: `next fire in ${fmtDuration(dueIn)}`;
|
|
||||||
head.append(' ', el('span', {
|
|
||||||
class: 'meta',
|
|
||||||
title: new Date(s.next_fire_at_unix * 1000).toISOString(),
|
|
||||||
}, dueLabel));
|
|
||||||
}
|
|
||||||
if (s.interval_seconds) {
|
|
||||||
head.append(' ', el('span',
|
|
||||||
{ class: 'badge badge-muted', title: 'recurring' },
|
|
||||||
`↻ every ${fmtDuration(s.interval_seconds)}`));
|
|
||||||
} else {
|
|
||||||
head.append(' ', el('span', { class: 'badge badge-muted', title: 'one-shot' }, 'one-shot'));
|
|
||||||
}
|
|
||||||
head.append(' ', el('span', { class: 'meta' }, '· owner ' + s.owner));
|
|
||||||
li.append(head);
|
|
||||||
|
|
||||||
if (s.description) {
|
|
||||||
li.append(el('div', { class: 'schedule-description' }, s.description));
|
|
||||||
}
|
|
||||||
const body = el('div', { class: 'schedule-body' });
|
|
||||||
appendLinkified(body, s.body);
|
|
||||||
li.append(body);
|
|
||||||
|
|
||||||
// Targets table — one row per recipient with cancel button +
|
|
||||||
// last-fire metadata.
|
|
||||||
const targets = s.targets || [];
|
|
||||||
if (targets.length) {
|
|
||||||
const table = el('table', { class: 'schedule-targets-table' });
|
|
||||||
const thead = el('thead', {}, el('tr', {},
|
|
||||||
el('th', {}, 'target'),
|
|
||||||
el('th', {}, 'last fire'),
|
|
||||||
el('th', {}, 'last result'),
|
|
||||||
el('th', {}, ''),
|
|
||||||
));
|
|
||||||
table.append(thead);
|
|
||||||
const tbody = el('tbody', {});
|
|
||||||
for (const t of targets) {
|
|
||||||
const tCancelled = !!t.cancelled_at_unix;
|
|
||||||
const tr = el('tr', { class: tCancelled ? 'schedule-target-cancelled' : '' });
|
|
||||||
tr.append(el('td', {}, t.target));
|
|
||||||
tr.append(el('td', { class: 'meta' },
|
|
||||||
t.last_fired_at_unix
|
|
||||||
? fmtAgo(t.last_fired_at_unix) + ' ago'
|
|
||||||
: '—'));
|
|
||||||
tr.append(el('td', { class: 'meta' }, t.last_result || '—'));
|
|
||||||
const actionTd = el('td', {});
|
|
||||||
if (!cancelled && !tCancelled) {
|
|
||||||
const btn = el('button', { type: 'button', class: 'btn btn-deny btn-inline-small' }, '✕');
|
|
||||||
btn.title = 'cancel just this target';
|
|
||||||
btn.addEventListener('click', () => cancelScheduleTargets(s.id, [t.target]));
|
|
||||||
actionTd.append(btn);
|
|
||||||
} else if (tCancelled) {
|
|
||||||
actionTd.append(el('span', { class: 'meta' }, '✕ cancelled'));
|
|
||||||
}
|
}
|
||||||
tr.append(actionTd);
|
|
||||||
tbody.append(tr);
|
|
||||||
}
|
}
|
||||||
table.append(tbody);
|
table.append(tbody);
|
||||||
li.append(table);
|
root.append(table);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The set of agent columns in the schedules table: operator + manager
|
||||||
|
// first, then live containers (sorted), then any extra names that
|
||||||
|
// appear as a schedule target but aren't in the live container list
|
||||||
|
// (operator typo, container destroyed mid-schedule, etc.) — same
|
||||||
|
// membership rule as `buildTargetChips` so the table and the new-/
|
||||||
|
// edit-form chip boxes agree on what's addressable.
|
||||||
|
function schedulesTableAgentSet() {
|
||||||
|
const seen = new Set();
|
||||||
|
const out = [];
|
||||||
|
const push = (n) => { if (!seen.has(n)) { seen.add(n); out.push(n); } };
|
||||||
|
push('operator');
|
||||||
|
push('manager');
|
||||||
|
const containerNames = Array.from(containersState.values())
|
||||||
|
.map((c) => c.name)
|
||||||
|
.filter((n) => n !== 'manager' && n !== 'operator')
|
||||||
|
.sort();
|
||||||
|
for (const n of containerNames) push(n);
|
||||||
|
for (const s of schedulesState) {
|
||||||
|
for (const t of s.targets || []) push(t.target);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function renderSchedulesTableHead(agents) {
|
||||||
|
const thead = el('thead', {});
|
||||||
|
const headerRow = el('tr', {});
|
||||||
|
headerRow.append(
|
||||||
|
el('th', { class: 'schedules-table-id' }, '#'),
|
||||||
|
el('th', {}, 'src'),
|
||||||
|
el('th', {}, 'next'),
|
||||||
|
el('th', {}, 'every'),
|
||||||
|
el('th', {}, 'owner'),
|
||||||
|
el('th', { class: 'schedules-table-body-th' }, 'body'),
|
||||||
|
);
|
||||||
|
for (const a of agents) {
|
||||||
|
headerRow.append(el('th', { class: 'schedules-table-agent-th', title: a },
|
||||||
|
el('div', {}, el('span', {}, a))));
|
||||||
|
}
|
||||||
|
headerRow.append(el('th', { class: 'schedules-table-actions-th' }, ''));
|
||||||
|
thead.append(headerRow);
|
||||||
|
return thead;
|
||||||
|
}
|
||||||
|
function renderScheduleRow(s, agents) {
|
||||||
|
const cancelled = !!s.cancelled_at_unix;
|
||||||
|
const tr = el('tr', {
|
||||||
|
class: 'schedules-table-row' + (cancelled ? ' schedules-table-row-cancelled' : ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
tr.append(el('td', { class: 'meta schedules-table-id' }, '#' + s.id));
|
||||||
|
|
||||||
|
const srcKind = s.source && s.source.kind === 'approval' ? 'approval' : 'manual';
|
||||||
|
tr.append(el('td', {},
|
||||||
|
el('span', { class: 'rqe-source rqe-source-' + srcKind },
|
||||||
|
srcKind === 'approval' ? 'approval' : 'operator')));
|
||||||
|
|
||||||
|
// "next" cell — relative due-in for active schedules, "cancelled"
|
||||||
|
// for cancelled ones. Both carry the absolute ISO in the title.
|
||||||
|
const nextCell = el('td', { class: 'meta' });
|
||||||
|
if (cancelled) {
|
||||||
|
nextCell.title = 'cancelled ' + new Date(s.cancelled_at_unix * 1000).toISOString();
|
||||||
|
nextCell.textContent = 'cancelled';
|
||||||
|
} else {
|
||||||
|
const dueIn = s.next_fire_at_unix - Math.floor(Date.now() / 1000);
|
||||||
|
nextCell.title = new Date(s.next_fire_at_unix * 1000).toISOString();
|
||||||
|
nextCell.textContent = dueIn <= 0
|
||||||
|
? 'overdue ' + fmtAgo(s.next_fire_at_unix)
|
||||||
|
: fmtDuration(dueIn);
|
||||||
|
}
|
||||||
|
tr.append(nextCell);
|
||||||
|
|
||||||
|
tr.append(el('td', { class: 'meta' },
|
||||||
|
s.interval_seconds ? '↻ ' + fmtDuration(s.interval_seconds) : 'one-shot'));
|
||||||
|
|
||||||
|
tr.append(el('td', { class: 'meta' }, s.owner));
|
||||||
|
|
||||||
|
// Body cell — truncates with ellipsis; full body + description (if
|
||||||
|
// any) on hover. Description used to be a separate visible block on
|
||||||
|
// the card layout; the table compresses it into the title to keep
|
||||||
|
// row height tight. Mara nit-flag candidate if she actually wants
|
||||||
|
// it visible in-table.
|
||||||
|
const bodyCell = el('td', { class: 'schedules-table-body-cell' });
|
||||||
|
const bodyText = s.body || '';
|
||||||
|
bodyCell.title = (s.description ? s.description + '\n\n' : '') + bodyText;
|
||||||
|
bodyCell.textContent = bodyText;
|
||||||
|
tr.append(bodyCell);
|
||||||
|
|
||||||
|
// Per-agent target cells. Three states:
|
||||||
|
// - active target → ✓ button that cancels just that target on
|
||||||
|
// click (same affordance as the per-row ✕ on the old layout's
|
||||||
|
// targets table)
|
||||||
|
// - cancelled target → muted ✕ glyph (no button — backend
|
||||||
|
// re-add flows through the edit form's targets multi-select)
|
||||||
|
// - not a target → empty cell
|
||||||
|
const targetByName = new Map();
|
||||||
|
for (const t of s.targets || []) targetByName.set(t.target, t);
|
||||||
|
for (const a of agents) {
|
||||||
|
const t = targetByName.get(a);
|
||||||
|
const td = el('td', { class: 'schedules-table-check' });
|
||||||
|
if (!t) {
|
||||||
|
// empty — no button, no glyph
|
||||||
|
} else if (t.cancelled_at_unix) {
|
||||||
|
td.title = 'cancelled — '
|
||||||
|
+ (t.last_fired_at_unix
|
||||||
|
? 'last fired ' + fmtAgo(t.last_fired_at_unix) + ' ago'
|
||||||
|
: 'never fired')
|
||||||
|
+ (t.last_result ? ' · ' + t.last_result : '');
|
||||||
|
td.append(el('span', { class: 'schedules-table-check-cancelled' }, '✕'));
|
||||||
|
} else {
|
||||||
|
const lastFireDesc = t.last_fired_at_unix
|
||||||
|
? 'last fired ' + fmtAgo(t.last_fired_at_unix) + ' ago'
|
||||||
|
: 'never fired';
|
||||||
|
const lastResultDesc = t.last_result ? ' · ' + t.last_result : '';
|
||||||
|
const checkBtn = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'schedules-table-check-btn',
|
||||||
|
}, '✓');
|
||||||
|
checkBtn.title = lastFireDesc + lastResultDesc
|
||||||
|
+ (cancelled ? '' : '\nclick to cancel this target');
|
||||||
|
if (cancelled) {
|
||||||
|
checkBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
checkBtn.addEventListener('click', () => cancelScheduleTargets(s.id, [a]));
|
||||||
|
}
|
||||||
|
td.append(checkBtn);
|
||||||
|
}
|
||||||
|
tr.append(td);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Actions cell — fire / edit / cancel-all. Glyph-only to fit a
|
||||||
|
// compact column; the buttons keep their existing colour classes
|
||||||
|
// so the visual cue (mauve = fire, yellow = edit, red = cancel)
|
||||||
|
// carries over from the card layout.
|
||||||
|
const actionsCell = el('td', { class: 'schedules-table-actions' });
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
const actions = el('div', { class: 'schedule-actions' });
|
|
||||||
// Fire-now (#467): out-of-band manual pulse. Recurring schedules
|
|
||||||
// get an extra fan-out without disturbing `next_fire_at`; one-shots
|
|
||||||
// are consumed (cancelled after fan-out), per damocles's design.
|
|
||||||
// Disabled when every target is cancelled — there's nothing to
|
|
||||||
// fire to. Backend: POST /api/schedules/{id}/fire-now.
|
|
||||||
const activeTargets = (s.targets || []).filter((t) => !t.cancelled_at_unix);
|
const activeTargets = (s.targets || []).filter((t) => !t.cancelled_at_unix);
|
||||||
const fireBtn = el('button', { type: 'button', class: 'btn btn-fire-now' }, '↯ fire now');
|
|
||||||
const isOneShot = !s.interval_seconds;
|
const isOneShot = !s.interval_seconds;
|
||||||
|
const fireBtn = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'btn btn-fire-now btn-inline-small',
|
||||||
|
}, '↯');
|
||||||
fireBtn.title = isOneShot
|
fireBtn.title = isOneShot
|
||||||
? 'fire this schedule once, immediately (one-shot — schedule is consumed after the manual fire)'
|
? 'fire once — one-shot, consumed after the manual fire'
|
||||||
: 'fire this schedule once, immediately (recurring — next regularly-scheduled fire is unaffected)';
|
: 'fire once now — recurring, next regular fire unaffected';
|
||||||
if (!activeTargets.length) {
|
if (!activeTargets.length) {
|
||||||
fireBtn.disabled = true;
|
fireBtn.disabled = true;
|
||||||
fireBtn.title = 'every target is cancelled — nothing to fire';
|
fireBtn.title = 'every target is cancelled — nothing to fire';
|
||||||
}
|
}
|
||||||
fireBtn.addEventListener('click', () => fireScheduleNow(s.id, isOneShot, activeTargets.map((t) => t.target), fireBtn));
|
fireBtn.addEventListener('click', () =>
|
||||||
actions.append(fireBtn);
|
fireScheduleNow(s.id, isOneShot, activeTargets.map((t) => t.target), fireBtn));
|
||||||
const editBtn = el('button', { type: 'button', class: 'btn btn-edit-schedule' },
|
actionsCell.append(fireBtn);
|
||||||
editingSchedules.has(s.id) ? '✎ close edit' : '✎ edit');
|
|
||||||
editBtn.title = 'edit body / description / interval / next-fire (targets stay immutable)';
|
const editingThis = editingSchedules.has(s.id);
|
||||||
|
const editBtn = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'btn btn-edit-schedule btn-inline-small',
|
||||||
|
}, editingThis ? '✎×' : '✎');
|
||||||
|
editBtn.title = editingThis
|
||||||
|
? 'close edit'
|
||||||
|
: 'edit body / description / interval / next-fire / targets';
|
||||||
editBtn.addEventListener('click', () => {
|
editBtn.addEventListener('click', () => {
|
||||||
if (editingSchedules.has(s.id)) {
|
if (editingThis) {
|
||||||
editingSchedules.delete(s.id);
|
editingSchedules.delete(s.id);
|
||||||
scheduleEditCarry.delete(s.id);
|
scheduleEditCarry.delete(s.id);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2440,17 +2513,28 @@ window.marked = marked;
|
||||||
}
|
}
|
||||||
renderSchedulesList();
|
renderSchedulesList();
|
||||||
});
|
});
|
||||||
actions.append(editBtn);
|
actionsCell.append(editBtn);
|
||||||
const cancelAll = el('button', { type: 'button', class: 'btn btn-deny' }, '✕ cancel all');
|
|
||||||
cancelAll.title = 'cancel the whole schedule';
|
const cancelBtn = el('button', {
|
||||||
cancelAll.addEventListener('click', () => cancelScheduleAll(s.id));
|
type: 'button',
|
||||||
actions.append(cancelAll);
|
class: 'btn btn-deny btn-inline-small',
|
||||||
li.append(actions);
|
}, '✕');
|
||||||
if (editingSchedules.has(s.id)) {
|
cancelBtn.title = 'cancel the whole schedule';
|
||||||
li.append(renderScheduleEditForm(s));
|
cancelBtn.addEventListener('click', () => cancelScheduleAll(s.id));
|
||||||
}
|
actionsCell.append(cancelBtn);
|
||||||
}
|
}
|
||||||
return li;
|
tr.append(actionsCell);
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
function renderScheduleEditRow(s, agents) {
|
||||||
|
// colspan = 6 attribute cols + N agent cols + 1 actions col
|
||||||
|
const colCount = 7 + agents.length;
|
||||||
|
const tr = el('tr', { class: 'schedules-table-edit-row' });
|
||||||
|
const td = el('td', { colspan: String(colCount) });
|
||||||
|
td.append(renderScheduleEditForm(s));
|
||||||
|
tr.append(td);
|
||||||
|
return tr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #474 — inline edit form. Renders inside the schedule row when the
|
// #474 — inline edit form. Renders inside the schedule row when the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue