/* ===================================================================== PULSE — Workflows page (WP-D) --------------------------------------------------------------------- Owns: DOM id prefix `wf-`, action namespace `wf:*`. Consumes the frozen window.Pulse contract from /assets/app.js. ===================================================================== */ 'use strict'; (function () { // Example workflow definition. The Create modal is pre-filled with this // every time it opens (fixes the old bug where the textarea was blanked). var EXAMPLE_DEFINITION = { steps: [ { name: 'Example Step', type: 'execute', targets: ['all'], command: "echo 'Hello from PULSE'" } ] }; // id -> parsed definition object, rebuilt on every list load. var _registry = {}; // id -> raw row from the list response (name/description/etc.). var _rowsById = {}; // workflow id currently targeted by the Run modal. var _pendingWorkflowId = null; function paramBadge(def) { var params = (def && def.params) || []; if (!params.length) return ''; var n = params.length; return ' ' + n + ' param' + (n > 1 ? 's' : '') + ''; } function parseDefinition(raw) { if (raw && typeof raw === 'object') return raw; if (typeof raw === 'string') { try { return JSON.parse(raw); } catch (e) { return {}; } } return {}; } function renderEmptyState() { return ( '
' + '
' + '
No workflows yet
' + '
Workflows let you run multi-step jobs across one or more workers.
' + '' + '
' ); } function renderRow(w) { var def = _registry[w.id] || {}; var isAdmin = window.Pulse.isAdmin; var esc = window.Pulse.esc; var actions = ''; if (isAdmin) { actions += ' ' + ' '; } var createdAt = window.Pulse.fmt.dateTime(w.created_at); return ( '' + '' + esc(w.name) + paramBadge(def) + '' + '' + esc(w.description || 'No description') + '' + '' + esc(w.created_by || 'Unknown') + ' · ' + esc(createdAt) + '' + '
' + actions + '
' + '' ); } function renderList(workflows) { var el = document.getElementById('wf-list'); if (!el) return; if (!workflows.length) { el.innerHTML = renderEmptyState(); return; } var rows = workflows.map(renderRow).join(''); el.innerHTML = '
' + '' + '' + '' + rows + '' + '
NameDescriptionCreatedActions
' + '
'; } function renderLoadError(message) { var el = document.getElementById('wf-list'); if (!el) return; el.innerHTML = ''; } function loadWorkflows() { return window.Pulse.api.get('/api/workflows').then(function (workflows) { var list = workflows || []; _registry = {}; _rowsById = {}; list.forEach(function (w) { _rowsById[w.id] = w; _registry[w.id] = parseDefinition(w.definition); }); renderList(list); }).catch(function (e) { renderLoadError(e && e.message); }); } /* --------------------------------------------------------------- Create modal --------------------------------------------------------------- */ function showFieldError(id, message) { var el = document.getElementById(id); if (!el) return; el.textContent = message || ''; el.hidden = !message; } function openCreateModal() { document.getElementById('wf-create-form').reset(); document.getElementById('wf-create-definition').value = JSON.stringify(EXAMPLE_DEFINITION, null, 2); showFieldError('wf-create-error', ''); window.lt.modal.open('wf-create-modal'); } function submitCreateForm(formEl) { var name = formEl.elements['name'].value.trim(); var description = formEl.elements['description'].value.trim(); var definitionText = formEl.elements['definition'].value; var webhookUrl = formEl.elements['webhook_url'].value.trim() || null; if (!name) { showFieldError('wf-create-error', 'Name is required.'); return; } var definition; try { definition = JSON.parse(definitionText); } catch (e) { showFieldError('wf-create-error', 'Invalid JSON: ' + e.message); return; } showFieldError('wf-create-error', ''); return window.Pulse.api.post('/api/workflows', { name: name, description: description, definition: definition, webhook_url: webhookUrl }).then(function () { window.lt.modal.close('wf-create-modal'); window.Pulse.toast.success('Workflow created'); return loadWorkflows(); }).catch(function (e) { showFieldError('wf-create-error', (e && e.message) || 'Failed to create workflow'); }); } /* --------------------------------------------------------------- Edit modal (admin) --------------------------------------------------------------- */ function openEditModal(workflowId) { showFieldError('wf-edit-error', ''); return window.Pulse.api.get('/api/workflows/' + encodeURIComponent(workflowId)).then(function (wf) { document.getElementById('wf-edit-id').value = wf.id; document.getElementById('wf-edit-name').value = wf.name || ''; document.getElementById('wf-edit-description').value = wf.description || ''; document.getElementById('wf-edit-definition').value = JSON.stringify(parseDefinition(wf.definition), null, 2); document.getElementById('wf-edit-webhook').value = wf.webhook_url || ''; window.lt.modal.open('wf-edit-modal'); }).catch(function (e) { window.Pulse.toast.error((e && e.message) || 'Failed to load workflow'); }); } function submitEditForm(formEl) { var id = formEl.elements['id'].value; var name = formEl.elements['name'].value.trim(); var description = formEl.elements['description'].value.trim(); var definitionText = formEl.elements['definition'].value; var webhookUrl = formEl.elements['webhook_url'].value.trim() || null; if (!name) { showFieldError('wf-edit-error', 'Name is required.'); return; } var definition; try { definition = JSON.parse(definitionText); } catch (e) { showFieldError('wf-edit-error', 'Invalid JSON: ' + e.message); return; } showFieldError('wf-edit-error', ''); return window.Pulse.api.put('/api/workflows/' + encodeURIComponent(id), { name: name, description: description, definition: definition, webhook_url: webhookUrl }).then(function () { window.lt.modal.close('wf-edit-modal'); window.Pulse.toast.success('Workflow saved'); return loadWorkflows(); }).catch(function (e) { showFieldError('wf-edit-error', (e && e.message) || 'Failed to save workflow'); }); } /* --------------------------------------------------------------- Delete (admin) --------------------------------------------------------------- */ function deleteWorkflow(workflowId, workflowName) { return window.Pulse.confirm({ title: 'Delete Workflow', message: 'Delete workflow "' + workflowName + '"? This cannot be undone.', type: 'error', confirmLabel: 'DELETE' }).then(function (ok) { if (!ok) return; return window.Pulse.api.delete('/api/workflows/' + encodeURIComponent(workflowId)).then(function () { window.Pulse.toast.success('Workflow deleted'); return loadWorkflows(); }).catch(function (e) { window.Pulse.toast.error((e && e.message) || 'Failed to delete workflow'); }); }); } /* --------------------------------------------------------------- Run modal (execute): always shown, replaces the old confirm(). --------------------------------------------------------------- */ function paramFieldHtml(p) { var esc = window.Pulse.esc; var label = esc(p.label || p.name); var required = p.required ? ' *' : ''; return ( '
' + '' + '' + '
' ); } function openRunModal(workflowId) { var row = _rowsById[workflowId]; var def = _registry[workflowId] || {}; var paramDefs = def.params || []; _pendingWorkflowId = workflowId; document.getElementById('wf-run-workflow-id').value = workflowId; document.getElementById('wf-run-name').textContent = row ? row.name : 'this workflow'; document.getElementById('wf-run-dryrun').checked = false; var paramsEl = document.getElementById('wf-run-params'); paramsEl.innerHTML = paramDefs.map(paramFieldHtml).join(''); window.lt.modal.open('wf-run-modal'); var first = paramsEl.querySelector('input'); if (first) setTimeout(function () { first.focus(); }, 60); } function submitRunForm() { if (!_pendingWorkflowId) return; var def = _registry[_pendingWorkflowId] || {}; var paramDefs = def.params || []; var params = {}; for (var i = 0; i < paramDefs.length; i++) { var p = paramDefs[i]; var el = document.getElementById('wf-run-param-' + p.name); var val = el ? el.value.trim() : ''; if (p.required && !val) { el.classList.add('is-invalid'); el.focus(); return; } el && el.classList.remove('is-invalid'); if (val) params[p.name] = val; } var dryRun = document.getElementById('wf-run-dryrun').checked; var workflowId = _pendingWorkflowId; return window.Pulse.api.post('/api/executions', { workflow_id: workflowId, params: params, dry_run: dryRun }).then(function (result) { window.lt.modal.close('wf-run-modal'); _pendingWorkflowId = null; window.Pulse.toast.success(dryRun ? 'Dry run started' : 'Workflow started'); window.location.href = '/executions?open=' + encodeURIComponent(result.id); }).catch(function (e) { window.Pulse.toast.error((e && e.message) || 'Failed to start execution'); }); } /* --------------------------------------------------------------- Actions + page registration --------------------------------------------------------------- */ window.Pulse.actions.registerAll({ 'wf:create-open': function () { openCreateModal(); }, 'wf:create-submit': function (el) { return submitCreateForm(el); }, 'wf:execute': function (el) { openRunModal(el.getAttribute('data-workflow-id')); }, 'wf:edit-open': function (el) { return openEditModal(el.getAttribute('data-workflow-id')); }, 'wf:edit-submit': function (el) { return submitEditForm(el); }, 'wf:delete': function (el) { return deleteWorkflow(el.getAttribute('data-workflow-id'), el.getAttribute('data-workflow-name')); }, 'wf:param-submit': function () { return submitRunForm(); } }); // Enter in a param input submits the Run form (matches old behaviour). document.addEventListener('keydown', function (ev) { if (ev.key !== 'Enter') return; var target = ev.target; if (!target || target.tagName !== 'INPUT') return; if (!target.closest || !target.closest('#wf-run-form')) return; ev.preventDefault(); submitRunForm(); }); window.Pulse.registerPage({ name: 'workflows', init: function () { return loadWorkflows(); }, refresh: function () { return loadWorkflows(); }, onEvent: function (type) { if (type === 'workflow_created' || type === 'workflow_updated' || type === 'workflow_deleted') { loadWorkflows(); return true; } return false; } }); })();