From 6364970b7efd4f8d0ef3c48c72d757610c55a5a0 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Thu, 18 Dec 2025 18:08:12 +0100 Subject: [PATCH] Enable autocomplete for undeployed seqera-config nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, autocomplete for launchpad names and data link names would only work after the seqera-config node had been deployed. This was because the backend could only retrieve config node properties via RED.nodes.getNode(), which only returns deployed nodes. This change enables autocomplete to work immediately after the config node is saved (closed with Done/Update), without requiring deployment: Frontend changes: - Use RED.nodes.node(configId) to get config from editor state - Pass baseUrl and workspaceId as query parameters to backend - Use params.set() for workspace override to ensure precedence Backend changes: - Accept baseUrl/workspaceId from query params when config not deployed - Use RED.nodes.getCredentials(configId) to look up saved API token - Add explicit credentials check before making API calls Files modified: - nodes/workflow-launch.html - nodes/datalink-list.html - nodes/datalink-poll.html - nodes/workflow-launch.js - nodes/_utils.js 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- nodes/_utils.js | 42 ++++++++++++++++++++++---------------- nodes/datalink-list.html | 13 ++++++++++-- nodes/datalink-poll.html | 13 ++++++++++-- nodes/workflow-launch.html | 13 ++++++++++-- nodes/workflow-launch.js | 42 ++++++++++++++++++++++---------------- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/nodes/_utils.js b/nodes/_utils.js index 14a0f7d..2a65c37 100644 --- a/nodes/_utils.js +++ b/nodes/_utils.js @@ -67,29 +67,35 @@ async function handleDatalinkAutoComplete(RED, req, res) { // Try to get the node, but it might not exist yet if this is a new node let node = RED.nodes.getNode(nodeId); - let seqeraConfigId = null; + let seqeraConfigId = req.query.seqeraConfig || req.body?.seqeraConfig; let baseUrl = "https://api.cloud.seqera.io"; let workspaceId = null; + let credentials = null; if (node && node.seqeraConfig) { - // Node exists and has config + // Node exists and has config - use deployed node's config seqeraConfigId = node.seqeraConfig.id; baseUrl = node.seqeraConfig.baseUrl || baseUrl; workspaceId = node.seqeraConfig.workspaceId; + credentials = node.seqeraConfig.credentials; // Check for workspace ID override in the node configuration if (node.workspaceIdProp && node.workspaceIdPropType === "str" && node.workspaceIdProp.trim()) { workspaceId = node.workspaceIdProp; } - } else { - // Try to get config ID from request body or query params - seqeraConfigId = req.query.seqeraConfig || req.body?.seqeraConfig; - if (seqeraConfigId) { - const configNode = RED.nodes.getNode(seqeraConfigId); - if (configNode) { - baseUrl = configNode.baseUrl || baseUrl; - workspaceId = configNode.workspaceId; - } + } else if (seqeraConfigId) { + // Try to get deployed config node + const configNode = RED.nodes.getNode(seqeraConfigId); + if (configNode) { + baseUrl = configNode.baseUrl || baseUrl; + workspaceId = configNode.workspaceId; + credentials = configNode.credentials; + } else { + // Config node not deployed - use query params for baseUrl/workspaceId + // and look up saved credentials by config node ID + baseUrl = req.query.baseUrl || baseUrl; + workspaceId = req.query.workspaceId || null; + credentials = RED.nodes.getCredentials(seqeraConfigId); } } @@ -102,16 +108,16 @@ async function handleDatalinkAutoComplete(RED, req, res) { return res.json([]); // Return empty array instead of error for better UX } - // Create a temporary node-like object for apiCall if we don't have a real node - const nodeForApi = node || { - seqeraConfig: RED.nodes.getNode(seqeraConfigId), + if (!credentials || !credentials.token) { + return res.json([]); // No credentials available + } + + // Create a temporary node-like object for apiCall + const nodeForApi = { + seqeraConfig: { credentials }, warn: () => {}, // Dummy warn function }; - if (!nodeForApi.seqeraConfig) { - return res.json([]); - } - // Build the data-links API URL with search parameter const params = new URLSearchParams({ workspaceId }); if (search) { diff --git a/nodes/datalink-list.html b/nodes/datalink-list.html index ddaee67..717a24f 100644 --- a/nodes/datalink-list.html +++ b/nodes/datalink-list.html @@ -180,16 +180,25 @@ return; } + // Get config node from editor (works for undeployed nodes) + const configNode = RED.nodes.node(seqeraConfigId); + const params = new URLSearchParams({ search: value, seqeraConfig: seqeraConfigId, }); - // Add workspace ID override if configured + // Pass config values for undeployed nodes + if (configNode) { + if (configNode.baseUrl) params.append("baseUrl", configNode.baseUrl); + if (configNode.workspaceId) params.append("workspaceId", configNode.workspaceId); + } + + // Add workspace ID override if configured (takes precedence) const workspaceIdOverride = $("#node-input-workspaceId").typedInput("value"); const workspaceIdType = $("#node-input-workspaceId").typedInput("type"); if (workspaceIdType === "str" && workspaceIdOverride && workspaceIdOverride.trim()) { - params.append("workspaceId", workspaceIdOverride); + params.set("workspaceId", workspaceIdOverride); } $.ajax({ diff --git a/nodes/datalink-poll.html b/nodes/datalink-poll.html index 356a612..9dbd58e 100644 --- a/nodes/datalink-poll.html +++ b/nodes/datalink-poll.html @@ -231,16 +231,25 @@ return; } + // Get config node from editor (works for undeployed nodes) + const configNode = RED.nodes.node(seqeraConfigId); + const params = new URLSearchParams({ search: value, seqeraConfig: seqeraConfigId, }); - // Add workspace ID override if configured + // Pass config values for undeployed nodes + if (configNode) { + if (configNode.baseUrl) params.append("baseUrl", configNode.baseUrl); + if (configNode.workspaceId) params.append("workspaceId", configNode.workspaceId); + } + + // Add workspace ID override if configured (takes precedence) const workspaceIdOverride = $("#node-input-workspaceId").typedInput("value"); const workspaceIdType = $("#node-input-workspaceId").typedInput("type"); if (workspaceIdType === "str" && workspaceIdOverride && workspaceIdOverride.trim()) { - params.append("workspaceId", workspaceIdOverride); + params.set("workspaceId", workspaceIdOverride); } $.ajax({ diff --git a/nodes/workflow-launch.html b/nodes/workflow-launch.html index 96717b9..712dc39 100644 --- a/nodes/workflow-launch.html +++ b/nodes/workflow-launch.html @@ -204,16 +204,25 @@ return; } + // Get config node from editor (works for undeployed nodes) + const configNode = RED.nodes.node(seqeraConfigId); + const params = new URLSearchParams({ search: value, seqeraConfig: seqeraConfigId, }); - // Add workspace ID override if configured + // Pass config values for undeployed nodes + if (configNode) { + if (configNode.baseUrl) params.append("baseUrl", configNode.baseUrl); + if (configNode.workspaceId) params.append("workspaceId", configNode.workspaceId); + } + + // Add workspace ID override if configured (takes precedence) const workspaceIdOverride = $("#node-input-workspaceId").typedInput("value"); const workspaceIdType = $("#node-input-workspaceId").typedInput("type"); if (workspaceIdType === "str" && workspaceIdOverride && workspaceIdOverride.trim()) { - params.append("workspaceId", workspaceIdOverride); + params.set("workspaceId", workspaceIdOverride); } $.ajax({ diff --git a/nodes/workflow-launch.js b/nodes/workflow-launch.js index 1709ea9..ddd0e2c 100644 --- a/nodes/workflow-launch.js +++ b/nodes/workflow-launch.js @@ -7,29 +7,35 @@ module.exports = function (RED) { // Try to get the node, but it might not exist yet if this is a new node let node = RED.nodes.getNode(nodeId); - let seqeraConfigId = null; + let seqeraConfigId = req.query.seqeraConfig || req.body?.seqeraConfig; let baseUrl = "https://api.cloud.seqera.io"; let workspaceId = null; + let credentials = null; if (node && node.seqeraConfig) { - // Node exists and has config + // Node exists and has config - use deployed node's config seqeraConfigId = node.seqeraConfig.id; baseUrl = node.seqeraConfig.baseUrl || baseUrl; workspaceId = node.seqeraConfig.workspaceId; + credentials = node.seqeraConfig.credentials; // Check for workspace ID override in the node configuration if (node.workspaceIdProp && node.workspaceIdPropType === "str" && node.workspaceIdProp.trim()) { workspaceId = node.workspaceIdProp; } - } else { - // Try to get config ID from request body or query params - seqeraConfigId = req.query.seqeraConfig || req.body?.seqeraConfig; - if (seqeraConfigId) { - const configNode = RED.nodes.getNode(seqeraConfigId); - if (configNode) { - baseUrl = configNode.baseUrl || baseUrl; - workspaceId = configNode.workspaceId; - } + } else if (seqeraConfigId) { + // Try to get deployed config node + const configNode = RED.nodes.getNode(seqeraConfigId); + if (configNode) { + baseUrl = configNode.baseUrl || baseUrl; + workspaceId = configNode.workspaceId; + credentials = configNode.credentials; + } else { + // Config node not deployed - use query params for baseUrl/workspaceId + // and look up saved credentials by config node ID + baseUrl = req.query.baseUrl || baseUrl; + workspaceId = req.query.workspaceId || null; + credentials = RED.nodes.getCredentials(seqeraConfigId); } } @@ -42,16 +48,16 @@ module.exports = function (RED) { return res.json([]); // Return empty array instead of error for better UX } - // Create a temporary node-like object for apiCall if we don't have a real node - const nodeForApi = node || { - seqeraConfig: RED.nodes.getNode(seqeraConfigId), + if (!credentials || !credentials.token) { + return res.json([]); // No credentials available + } + + // Create a temporary node-like object for apiCall + const nodeForApi = { + seqeraConfig: { credentials }, warn: () => {}, // Dummy warn function }; - if (!nodeForApi.seqeraConfig) { - return res.json([]); - } - const { apiCall } = require("./_utils"); // Build the pipelines API URL