var AssayDetail = (function () {
'use strict';
let panel = null;
let ctx = null;
let activeClose = null;
function getPanel() {
if (!panel) panel = document.getElementById('detail-panel');
return panel;
}
async function showDetail(id, context, opts) {
ctx = context;
var p = (opts && opts.target) || getPanel();
activeClose = (opts && opts.onClose) || closeDetail;
p.innerHTML = '<div class="detail-header"><h2>Loading...</h2>' +
'<button class="detail-close" id="detail-close-btn">×</button></div>';
if (p === getPanel()) p.classList.add('open');
p.querySelector('#detail-close-btn').addEventListener('click', function () {
activeClose && activeClose();
});
try {
var statePromise = ctx
.apiFetch('/workflows/' + encodeURIComponent(id) + '/state')
.catch(function () { return null; });
var [wf, events, children, state] = await Promise.all([
ctx.apiFetch('/workflows/' + encodeURIComponent(id)),
ctx.apiFetch('/workflows/' + encodeURIComponent(id) + '/events'),
ctx.apiFetch('/workflows/' + encodeURIComponent(id) + '/children'),
statePromise,
]);
renderDetail(wf, events || [], children || [], state, p);
} catch (err) {
p.innerHTML =
'<div class="detail-header"><h2>Error</h2>' +
'<button class="detail-close" id="detail-close-btn">×</button></div>' +
'<div class="detail-body"><div class="error-box">' + ctx.escapeHtml(err.message) + '</div></div>';
p.querySelector('#detail-close-btn').addEventListener('click', function () {
activeClose && activeClose();
});
}
}
function renderDetail(wf, events, children, state, targetEl) {
var p = targetEl || getPanel();
var status = (wf.status || 'PENDING').toUpperCase();
var terminal = ctx.isTerminal(status);
var html =
'<div class="detail-header">' +
'<h2 title="' + ctx.escapeHtml(wf.id) + '">' +
ctx.escapeHtml(ctx.truncate(wf.id, 40)) + '</h2>' +
'<button class="detail-close" id="detail-close-btn">×</button>' +
'</div>' +
'<div class="detail-body">';
html +=
'<div class="meta-grid">' +
metaItem('Status', '<span class="badge ' + ctx.badgeClass(status) + '">' + status + '</span>') +
metaItem('Type', ctx.escapeHtml(wf.workflow_type || '-')) +
metaItem('Namespace', ctx.escapeHtml(wf.namespace || '-')) +
metaItem('Queue', ctx.escapeHtml(wf.task_queue || '-')) +
metaItem('Run ID', '<span class="mono" title="' + ctx.escapeHtml(wf.run_id || '') + '">' +
ctx.escapeHtml(ctx.truncate(wf.run_id, 24)) + '</span>') +
metaItem('Created', ctx.formatTime(wf.created_at)) +
metaItem('Claimed By', ctx.escapeHtml(wf.claimed_by || '-')) +
metaItem('Completed', wf.completed_at ? ctx.formatTime(wf.completed_at) : '-') +
'</div>';
var idAttr = ctx.escapeHtml(wf.id);
html += '<div class="action-row" style="margin-bottom: 16px;">';
if (!terminal) {
html +=
'<button class="btn-action btn-signal-detail" data-id="' + idAttr + '">Send signal</button>' +
'<button class="btn-action btn-cancel-detail" data-id="' + idAttr + '">Cancel</button>' +
'<button class="btn-action btn-action-danger btn-terminate-detail" data-id="' + idAttr + '">Terminate</button>' +
'<button class="btn-action btn-continue-detail" data-id="' + idAttr + '">Continue as new</button>';
} else {
html +=
'<button class="btn-action btn-continue-detail" data-id="' + idAttr + '" title="Start a fresh run with the same type + queue">Continue as new</button>';
}
html += '</div>';
var tabs = [
{
id: 'overview',
label: 'Overview',
count: null,
build: function () {
var body = '';
if (wf.input) {
body += '<h4 class="detail-subhead">Input</h4>' +
'<div class="json-viewer">' + ctx.escapeHtml(ctx.formatJson(wf.input)) + '</div>';
}
if (wf.result) {
body += '<h4 class="detail-subhead">Result</h4>' +
'<div class="json-viewer">' + ctx.escapeHtml(ctx.formatJson(wf.result)) + '</div>';
}
if (wf.error) {
body += '<h4 class="detail-subhead" style="color: var(--red);">Error</h4>' +
'<div class="error-box">' + ctx.escapeHtml(wf.error) + '</div>';
}
if (!body) {
body = '<p class="detail-muted">No input, result, or error recorded.</p>';
}
return body;
},
},
{
id: 'state',
label: 'State',
empty: !(state && state.state !== undefined && state.state !== null),
build: function () {
if (!state || state.state === undefined || state.state === null) {
return '<p class="detail-muted">' +
'No live state snapshot. This workflow did not call <code>ctx:register_query</code>.' +
'</p>';
}
var stateJson = typeof state.state === 'string'
? state.state
: JSON.stringify(state.state, null, 2);
return '<div class="json-viewer">' + ctx.escapeHtml(stateJson) + '</div>' +
'<p class="detail-muted" style="margin-top: 6px;">' +
'Snapshot at event seq ' + (state.event_seq || '?') +
(state.created_at ? ' — ' + ctx.formatTime(state.created_at) : '') +
'</p>';
},
},
{
id: 'events',
label: 'Events',
count: events.length,
build: function () {
if (!events.length) return '<p class="detail-muted">No events recorded.</p>';
var out = '<div class="event-timeline">';
for (var i = 0; i < events.length; i++) {
var evt = events[i];
out +=
'<div class="event-item" data-idx="' + i + '">' +
'<div class="event-header">' +
'<span class="event-type">' + ctx.escapeHtml(evt.event_type) + '</span>' +
'<span class="event-time">#' + evt.seq + ' - ' + ctx.formatTime(evt.timestamp) + '</span>' +
'</div>' +
'<div class="event-payload" id="evt-payload-' + i + '">' +
(evt.payload
? '<div class="json-viewer">' + ctx.escapeHtml(ctx.formatJson(evt.payload)) + '</div>'
: '<span style="color: var(--text-muted); font-size: 12px;">No payload</span>') +
'</div>' +
'</div>';
}
return out + '</div>';
},
},
{
id: 'children',
label: 'Children',
count: children.length,
empty: children.length === 0,
build: function () {
if (!children.length) return '<p class="detail-muted">No child workflows.</p>';
var out = '<table class="data-table"><thead><tr>' +
'<th>ID</th><th>Type</th><th>Status</th></tr></thead><tbody>';
for (var j = 0; j < children.length; j++) {
var child = children[j];
var cs = (child.status || 'PENDING').toUpperCase();
out +=
'<tr>' +
'<td><a href="#" class="clickable child-link mono" data-id="' + ctx.escapeHtml(child.id) + '" title="' + ctx.escapeHtml(child.id) + '">' +
ctx.escapeHtml(ctx.truncate(child.id, 28)) + '</a></td>' +
'<td>' + ctx.escapeHtml(child.workflow_type || '-') + '</td>' +
'<td><span class="badge ' + ctx.badgeClass(cs) + '">' + cs + '</span></td>' +
'</tr>';
}
return out + '</tbody></table>';
},
},
{
id: 'attrs',
label: 'Attributes',
empty: !wf.search_attributes,
build: function () {
if (!wf.search_attributes) {
return '<p class="detail-muted">No search attributes set on this run.</p>';
}
return '<div class="json-viewer">' +
ctx.escapeHtml(ctx.formatJson(wf.search_attributes)) +
'</div>';
},
},
];
html += '<div class="detail-tabs">';
html += '<div class="detail-tab-nav" role="tablist">';
for (var t = 0; t < tabs.length; t++) {
var tab = tabs[t];
var active = t === 0 ? ' active' : '';
var dim = tab.empty ? ' dim' : '';
var label = tab.label +
(tab.count != null ? ' <span class="tab-count">(' + tab.count + ')</span>' : '');
html +=
'<button class="detail-tab' + active + dim +
'" data-tab="' + tab.id + '" role="tab">' +
label +
'</button>';
}
html += '</div>';
html += '<div class="detail-tab-panels">';
for (var u = 0; u < tabs.length; u++) {
var active2 = u === 0 ? ' active' : '';
html +=
'<div class="detail-tab-panel' + active2 + '" data-tab="' + tabs[u].id + '" role="tabpanel">' +
tabs[u].build() +
'</div>';
}
html += '</div>';
html += '</div>';
html += '</div>';
p.innerHTML = html;
p.addEventListener('click', handlePanelClick);
}
function handlePanelClick(e) {
if (e.target.closest('#detail-close-btn') || e.target.closest('.detail-close')) {
(activeClose || closeDetail)();
return;
}
var tabBtn = e.target.closest('.detail-tab');
if (tabBtn) {
e.preventDefault();
var container = tabBtn.closest('.detail-tabs');
if (!container) return;
var id = tabBtn.dataset.tab;
var tabs = container.querySelectorAll('.detail-tab');
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
tabBtn.classList.add('active');
var panels = container.querySelectorAll('.detail-tab-panel');
for (var j = 0; j < panels.length; j++) {
panels[j].classList.toggle('active', panels[j].dataset.tab === id);
}
return;
}
var evtItem = e.target.closest('.event-item');
if (evtItem) {
var idx = evtItem.dataset.idx;
var payload = document.getElementById('evt-payload-' + idx);
if (payload) payload.classList.toggle('open');
return;
}
var childLink = e.target.closest('.child-link');
if (childLink) {
e.preventDefault();
showDetail(childLink.dataset.id, ctx);
return;
}
var sigBtn = e.target.closest('.btn-signal-detail');
if (sigBtn) {
handleSignal(sigBtn.dataset.id);
return;
}
var canBtn = e.target.closest('.btn-cancel-detail');
if (canBtn) {
handleCancel(canBtn.dataset.id);
return;
}
var termBtn = e.target.closest('.btn-terminate-detail');
if (termBtn) {
handleTerminate(termBtn.dataset.id);
return;
}
var contBtn = e.target.closest('.btn-continue-detail');
if (contBtn) {
handleContinueAsNew(contBtn.dataset.id);
}
}
async function handleSignal(id) {
var name = prompt('Signal name:');
if (!name) return;
var payloadStr = prompt('Signal payload (JSON, or leave empty):', '');
var payload = null;
if (payloadStr) {
try { payload = JSON.parse(payloadStr); } catch (_) { payload = payloadStr; }
}
try {
await ctx.apiFetch('/workflows/' + encodeURIComponent(id) + '/signal/' + encodeURIComponent(name), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload: payload }),
});
ctx.toast("Signal '" + name + "' sent", 'success');
showDetail(id, ctx);
} catch (err) {
ctx.toast('Signal failed: ' + err.message, 'error');
}
}
async function handleCancel(id) {
if (!confirm('Cancel workflow ' + id + '?')) return;
try {
await ctx.apiFetch('/workflows/' + encodeURIComponent(id) + '/cancel', { method: 'POST' });
ctx.toast('Cancel requested', 'success');
showDetail(id, ctx);
} catch (err) {
ctx.toast('Cancel failed: ' + err.message, 'error');
}
}
async function handleTerminate(id) {
var reason = prompt(
'Terminate workflow ' + id + '?\n\nReason (optional):',
''
);
if (reason === null) return;
var body = reason ? { reason: reason } : {};
try {
await ctx.apiFetch('/workflows/' + encodeURIComponent(id) + '/terminate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
ctx.toast('Terminated', 'success');
showDetail(id, ctx);
} catch (err) {
ctx.toast('Terminate failed: ' + err.message, 'error');
}
}
async function handleContinueAsNew(id) {
var inputStr = prompt(
'Close out ' + id + ' and start a fresh run with the same type + queue.\n\n' +
'New input (JSON, optional):',
''
);
if (inputStr === null) return;
var body = {};
if (inputStr && inputStr.trim()) {
try {
body.input = JSON.parse(inputStr);
} catch (err) {
ctx.toast('Input must be valid JSON', 'error');
return;
}
}
try {
var newRun = await ctx.apiFetch(
'/workflows/' + encodeURIComponent(id) + '/continue-as-new',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}
);
ctx.toast('New run: ' + (newRun && newRun.workflow_id || 'unknown'), 'success');
if (newRun && newRun.workflow_id) {
showDetail(newRun.workflow_id, ctx);
} else {
closeDetail();
}
if (ctx.refreshCurrentView) ctx.refreshCurrentView();
} catch (err) {
ctx.toast('Continue-as-new failed: ' + err.message, 'error');
}
}
function metaItem(label, value) {
return '<div class="meta-item"><label>' + label + '</label><span>' + value + '</span></div>';
}
function closeDetail() {
var p = getPanel();
p.classList.remove('open');
p.removeEventListener('click', handlePanelClick);
setTimeout(function () { p.innerHTML = ''; }, 300);
}
return { showDetail: showDetail, closeDetail: closeDetail };
})();