UNPKG

@seqera/node-red-seqera

Version:

Node-RED nodes for interacting with the Seqera Platform API

172 lines (145 loc) 6.37 kB
module.exports = function (RED) { function SeqeraWorkflowMonitorNode(config) { RED.nodes.createNode(this, config); const node = this; // Store typedInput and config properties node.workflowIdProp = config.workflowId; node.workflowIdPropType = config.workflowIdType; node.workspaceIdProp = config.workspaceId; node.workspaceIdPropType = config.workspaceIdType; node.pollIntervalProp = config.poll; node.pollIntervalPropType = config.pollType; node.keepPolling = config.keepPolling !== false; // default to true if undefined node.seqeraConfig = RED.nodes.getNode(config.seqera); node.defaultBaseUrl = (node.seqeraConfig && node.seqeraConfig.baseUrl) || "https://api.cloud.seqera.io"; // Local (deprecated) credentials – still supported for parity with other nodes node.credentials = RED.nodes.getCredentials(node.id); const { apiCall } = require("./_utils"); // Helper to format date as yyyy-mm-dd HH:MM:SS const formatDateTime = () => { const d = new Date(); const pad = (n) => n.toString().padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${d.toLocaleTimeString()}`; }; let intervalId = null; const clearPolling = () => { if (intervalId) { clearInterval(intervalId); intervalId = null; } }; node.on("close", () => { clearPolling(); }); // Helper to evaluate typedInput properties (supports JSONata) const evalProp = async (p, t, msg) => { if (t === "jsonata") { const expr = RED.util.prepareJSONataExpression(p, node); return await new Promise((resolve, reject) => { RED.util.evaluateJSONataExpression(expr, msg, (err, value) => { if (err) return reject(err); resolve(value); }); }); } return RED.util.evaluateNodeProperty(p, t, node, msg); }; // Helper to map workflow status to Node-RED colour const mapColor = (stat) => { const s = (stat || "").toLowerCase(); if (/^(submitted|pending)$/.test(s)) return "yellow"; if (/^(running)$/.test(s)) return "blue"; if (/^(completed|succeeded|success)$/.test(s)) return "green"; if (/^(failed|error|cancelled|canceled)$/.test(s)) return "red"; return "grey"; }; async function fetchStatus(msg, send) { try { // Evaluate properties on every poll so that msg overrides can change const workflowId = await evalProp(node.workflowIdProp, node.workflowIdPropType, msg); const workspaceIdOverride = await evalProp(node.workspaceIdProp, node.workspaceIdPropType, msg); const pollInterval = await evalProp(node.pollIntervalProp, node.pollIntervalPropType, msg); if (!workflowId) { throw new Error("workflowId not provided"); } const baseUrl = (node.seqeraConfig && node.seqeraConfig.baseUrl) || node.defaultBaseUrl; const workspaceId = workspaceIdOverride || (node.seqeraConfig && node.seqeraConfig.workspaceId) || msg.workspaceId || null; const urlBase = `${baseUrl.replace(/\/$/, "")}/workflow/${workflowId}`; const url = workspaceId ? `${urlBase}?workspaceId=${workspaceId}` : urlBase; const response = await apiCall(node, "get", url, { headers: { Accept: "application/json" } }); const wfStatus = response.data?.workflow?.status || "unknown"; const statusLower = wfStatus.toLowerCase(); // Set node status in editor node.status({ fill: mapColor(statusLower), shape: /^(submitted|running|pending)$/.test(statusLower) ? "ring" : "dot", text: `${statusLower}: ${formatDateTime()}`, }); const outMsg = { ...msg, payload: response.data, workflowId: response.data?.workflow?.id || workflowId, }; // Decide which output to send to if (/^(submitted|running|pending)$/.test(statusLower)) { send([outMsg, null, null]); } else if (/^(completed|succeeded|success)$/.test(statusLower)) { send([null, outMsg, null]); } else { send([null, null, outMsg]); } // If keepPolling disabled OR workflow reached a final state, stop polling if (!node.keepPolling || !/^(submitted|running|pending)$/.test(statusLower)) { clearPolling(); } // Update polling interval if changed dynamically if (node.keepPolling && /^(submitted|running|pending)$/.test(statusLower)) { const pollSec = parseInt(pollInterval, 10) || 5; if (pollSec * 1000 !== node._currentPollMs) { clearPolling(); node._currentPollMs = pollSec * 1000; intervalId = setInterval(() => fetchStatus(msg, send), node._currentPollMs); } } } catch (err) { node.error(`Seqera API request failed: ${err.message}`, msg); node.status({ fill: "red", shape: "dot", text: `error: ${formatDateTime()}` }); clearPolling(); } } node.on("input", async function (msg, send, done) { clearPolling(); try { // Kick off status fetch (will set up interval if needed) await fetchStatus(msg, send); // Start polling loop if enabled and interval not yet set if (node.keepPolling && !intervalId) { const pollInterval = await evalProp(node.pollIntervalProp, node.pollIntervalPropType, msg); const pollSec = parseInt(pollInterval, 10) || 5; node._currentPollMs = pollSec * 1000; intervalId = setInterval(() => fetchStatus(msg, send), node._currentPollMs); } if (done) done(); } catch (err) { if (done) done(err); } }); } RED.nodes.registerType("seqera-workflow-monitor", SeqeraWorkflowMonitorNode, { credentials: { token: { type: "password" }, }, defaults: { name: { value: "" }, seqeraConfig: { value: "", type: "seqera-config", required: true }, workflowId: { value: "workflowId" }, workflowIdType: { value: "msg" }, workspaceId: { value: "workspaceId" }, workspaceIdType: { value: "msg" }, keepPolling: { value: true }, poll: { value: 5 }, pollType: { value: "num" }, }, }); };