UNPKG

@bea.steers/node-red-contrib-nuclio

Version:
1,206 lines (1,109 loc) 49.1 kB
<style> .node_label_monospace { font-family: monospace; font-size: 0.82em; font-weight: 900; } </style> <!-- ----------------------------------------------------------------------- --> <!-- Nuclio Server --> <!-- ----------------------------------------------------------------------- --> <script type="text/javascript"> RED.nodes.registerType('nuclio-config', { category: 'config', icon: "nuclio-logo.svg", defaults: { address: { value: 'NUCLIO_ADDRESS', required: true }, addressType: { value: 'env' }, publicAddress: { value: 'http://localhost:8070', required: false }, publicAddressType: { value: 'str' }, }, label: function () { return this.publicAddress || this.address || 'Nuclio Server'; }, oneditprepare: function () { $('#node-config-input-address').typedInput({ default: this.addressType, types: ['str', 'global', 'env'], }); $('#node-config-input-publicAddress').typedInput({ default: this.publicAddressType, types: ['str', 'global', 'env'], }); }, }); RED.nodes.registerType('nuclio', { category: 'function', color: '#65a9fc', inputs: 1, outputs: 2, outputLabels: ['result', 'fallback'], icon: "nuclio.io.ico", defaults: { function: { type: 'nuclio-function', required: true, }, timeoutMs: { value: '' }, maxInFlight: { value: '' }, headers: { value: [] }, }, label: function () { const fn = RED.nodes.node(this.function); return fn?.name || 'nuclio invoke'; }, oneditprepare: function() { this.headersList = $("#node-input-headers").editableList({ addItem: function(row, index, data) { $(`<input class='node-input-key' type="text" placeholder="Header" />`).appendTo(row).val(data.name) $(`<input class='node-input-value' placeholder="value" />`).appendTo(row).attr('value', data.value).typedInput({ defaultType: data.type || 'str', types: ['str','num','json','env','cred'], }); }, removable: true, }); for (let data of this.headers || []) { this.headersList.editableList('addItem', data); } }, oneditsave: function() { let node = this; node.headers = []; $("#node-input-headers").editableList('items').each(function(i) { let name = $(this).find(".node-input-key").val(); let type = $(this).find(".node-input-value").typedInput('type'); let value = $(this).find(".node-input-value").typedInput('value'); name && node.headers.push({ name, type, value }); }); delete this.headersList; }, oneditcancel: function() { delete this.headersList; }, }); </script> <script type="text/html" data-template-name="nuclio-config"> <div class="form-row"> <label for="node-config-input-address"> <span>Nuclio Dashboard URL:</span> </label> <input type="text" id="node-config-input-address" placeholder="http://localhost:8070" /> <div> <small>The address that node-red can reach nuclio at. (typically port 8070)</small> </div> </div> <div class="form-row"> <label for="node-config-input-publicAddress"> <span>Public Nuclio Dashboard URL (optional)</span> </label> <input type="text" id="node-config-input-publicAddress" placeholder="http://localhost:8070" /> <div> <small>The address that YOU (and your browser) can reach nuclio by. This is just used for links to the nuclio dashboard and is not critical to operation. This could just be your local port forward address for simplicity.</small> </div> </div> </script> <!-- ----------------------------------------------------------------------- --> <!-- Nuclio Project --> <!-- ----------------------------------------------------------------------- --> <script type="text/javascript"> RED.nodes.registerType('nuclio-project', { category: 'config', icon: "nuclio-logo.svg", defaults: { name: { value: 'default' }, nameType: { value: 'str' }, }, label: function () { return this.name || 'default'; }, oneditprepare: function () { $('#node-project-input-name').typedInput({ default: this.nameType, types: ['str', 'global', 'env'], }); }, }); </script> <script type="text/html" data-template-name="nuclio-project"> <div class="form-row"> <label for="node-project-input-name"> <span>Nuclio Project Name</span> </label> <input type="text" id="node-project-input-name" placeholder="default" /> </div> </script> <!-- ----------------------------------------------------------------------- --> <!-- Nuclio Function --> <!-- ----------------------------------------------------------------------- --> <script type="text/javascript"> const sampleList = (list) => list[Math.floor(Math.random() * list.length)]; const randomEndpoint = () => { const adjectives = ['adaptive', 'agile', 'airy', 'amber', 'amplified', 'blazing', 'blissful', 'boundless', 'breezy', 'bright', 'buoyant', 'calm', 'caressing', 'cascading', 'celestial', 'cloudy', 'cocooned', 'cosmic', 'crystalline', 'dappled', 'dazzling', 'dewlit', 'digital', 'dreamy', 'drifting', 'dynamic', 'elastic', 'electric', 'elegant', 'endless', 'ethereal', 'evolving', 'feathered', 'fluent', 'futuristic', 'gentle', 'gentlehearted', 'glassy', 'global', 'glowing', 'gossamer', 'graceful', 'gravitational', 'harmonic', 'harmonious', 'hazy', 'hushed', 'hyper', 'infinite', 'innovative', 'interstellar', 'iridescent', 'joyful', 'kindred', 'kinetic', 'lavender', 'lilac', 'lilting', 'lucent', 'luminous', 'lunar', 'magnetic', 'mellow', 'melodic', 'mighty', 'misty', 'modular', 'moonlit', 'nanotech', 'nebula', 'neural', 'oblique', 'omniscient', 'opalescent', 'orbital', 'orbiting', 'pastel', 'peachy', 'pearl', 'perennial', 'petal', 'plasma', 'plush', 'pulsating', 'quantum', 'radiant', 'reactive', 'revolutionary', 'rippling', 'roselight', 'rosy', 'seamless', 'serene', 'shimmering', 'shiny', 'silken', 'silver', 'silvery', 'smooth', 'soaring', 'soft', 'solar', 'sonic', 'soothing', 'sparkling', 'spectral', 'spun', 'stellar', 'sunny', 'supercharged', 'sweet', 'swift', 'tranquil', 'transcendent', 'turbo', 'twinkling', 'ultra', 'unified', 'velvet', 'velvety', 'vibrant', 'virtual', 'volatile', 'warm', 'wavy', 'weightless', 'whimsical', 'wistful', 'zealous', 'zippy']; const nouns = ['accelerator', 'api', 'array', 'aura', 'backbone', 'band', 'beacon', 'bloom', 'breeze', 'bubble', 'cartridge', 'channel', 'chip', 'circuit', 'cluster', 'cocoon', 'conduit', 'core', 'dawn', 'domain', 'dream', 'drift', 'dusk', 'echo', 'endpoint', 'engine', 'fabric', 'feather', 'flame', 'flow', 'flux', 'gateway', 'gleam', 'glean', 'glimmer', 'glow', 'grid', 'halo', 'harmony', 'haze', 'hive', 'horizon', 'hub', 'interface', 'kernel', 'lattice', 'light', 'link', 'logic', 'lotus', 'lull', 'matrix', 'mesh', 'mist', 'module', 'murmur', 'muse', 'nest', 'net', 'nexus', 'node', 'note', 'opal', 'orb', 'oscillator', 'path', 'pathway', 'petal', 'pipeline', 'plane', 'point', 'portal', 'processor', 'protocol', 'pulse', 'radar', 'reactor', 'relay', 'ribbon', 'ripple', 'rose', 'route', 'scaffold', 'scent', 'segment', 'sequence', 'service', 'shade', 'shell', 'shimmer', 'sky', 'song', 'spark', 'spectrum', 'sphere', 'stream', 'switch', 'system', 'terrain', 'thread', 'tide', 'token', 'tone', 'topology', 'trace', 'trail', 'twilight', 'vector', 'veil', 'vertex', 'vibe', 'warp', 'wave', 'wavelength', 'web', 'webbing', 'whisper', 'wind', 'wing', 'wire', 'wisp', 'zephyr', 'zone']; return `${sampleList(adjectives)}-${sampleList(nouns)}`; } const configCode = ` apiVersion: "nuclio.io/v1" kind: NuclioFunction metadata: labels: {} annotations: {} spec: build: commands: [] # - pip install requests numpy pandas # # add multiple workers: # triggers: # mh: # kind: http # numWorkers: 4 ############# # Kubernetes: # env: [] # envFrom: [] # volumes: [] # # Pod replica auto-scaling: # minReplicas: 1 # maxReplicas: 1 # resources: {} # # Kubernetes Limits & Requests for the function's CPU and memory usage. # requests: # cpu: 1 # memory: 128M # limits: # cpu: 2 # memory: 256M # nvidia.com/gpu: 1 `.trim(); const SAMPLES = { python: { config: configCode, entrypoint: 'handler', code: ` import nuclio_sdk async def handler(context: nuclio_sdk.Context, event: nuclio_sdk.Event): # reverse the input, return JSON return { "some-output": event.body[::-1], "hi": "Hello, from Nuclio :]", } `.trim() }, golang: { config: configCode, entrypoint: 'Handler', code: ` package main import ( "github.com/nuclio/nuclio-sdk-go" ) func Handler(context *nuclio.Context, event nuclio.Event) (interface{}, error) { context.Logger.Info("This is an unstructured %s", "log") return nuclio.Response{ StatusCode: 200, ContentType: "application/text", Body: []byte("Hello, from Nuclio :]"), }, nil } `.trim() }, nodejs: { config: configCode, entrypoint: 'handler', code: ` exports.handler = function(context, event) { var body = event.body.toString(); context.logger.info('reversing: ' + body); context.callback(body.split('').reverse().join('')); }; `.trim() }, shell: { config: configCode, code: ` #!/bin/sh # reverse the input rev /dev/stdin `.trim() }, } const getUniqueValues = (node, key) => { let endpoints = {}; RED.nodes.eachConfig(n => { if (n.type !== "nuclio-function" || n.server !== node.server || n.project !== node.project || n.id === node.id) return; endpoints[key(n)] = n.id; }); return endpoints; }; const cleanRouteName = (route_prefix) => route_prefix.replace(/^\//g, '').replace(/\//g, ' '); const nodeName = (n) => n.name || cleanRouteName(n.route_prefix); class Poller { constructor(fn, interval) { this.fn = fn; this.interval = interval; this.pollInterval = null; } start() { if (this.pollInterval) return; this.fn(); this.pollInterval = setInterval(this.fn, this.interval); } stop() { if (!this.pollInterval) return; clearInterval(this.pollInterval); this.pollInterval = null; } } const fmt = (obj) => Object.entries(obj).map(([k,v]) => { if (typeof v === 'string' && v.includes('\n')) { return `<div class="nuclio-kv-row"><span class="nuclio-kv-key">${k}</span><span class="nuclio-kv-value nuclio-kv-multiline">${v}</span></div>`; } return `<div class="nuclio-kv-row"><span class="nuclio-kv-key">${k}</span><span class="nuclio-kv-value">${JSON.stringify(v)}</span></div>`; }).join(''); const statusChips = (obj) => Object.entries(obj || {}).map(([k,v]) => { const value = typeof v === 'string' ? v : JSON.stringify(v); return `<span class="nuclio-chip"><span class="nuclio-chip-key">${k}</span><span class="nuclio-chip-value">${value}</span></span>`; }).join(''); // const yamlFmt = (obj) => Object.entries(obj).map(([k,v]) => `<b>${k}</b>: ${yamlFmt(v)}`).join('\n'); const canUpdateElement = (element) => { if (!element) return true; const selection = window.getSelection(); if (!selection || selection.isCollapsed) return true; if (!selection.rangeCount) return true; const range = selection.getRangeAt(0); return !element.contains(range.commonAncestorContainer); }; const statusTone = (state) => { if (!state) return 'unknown'; if (state === 'ready') return 'ok'; if (state === 'error' || state === 'unhealthy') return 'error'; if (state === 'scaledToZero') return 'idle'; if (state.includes('waiting') || state === 'building' || state === 'configuringResources') return 'working'; return 'warn'; }; const splitByDotWithEscape = (str) => str.split(/(?<!\\)\./).map(part => part.replace(/\\\./g, '.')); const redactSecrets = (obj, secretVars) => { if (!obj) return obj; const redacted = JSON.parse(JSON.stringify(obj)); for (const secret of secretVars || []) { if (!secret?.name) continue; const keys = splitByDotWithEscape(secret.name); const lastKey = keys.pop(); let c = redacted; for (const key of keys) { if (!c || typeof c !== 'object') { c = null; break; } c = c[key]; } if (c && Object.prototype.hasOwnProperty.call(c, lastKey)) { c[lastKey] = '[redacted]'; } } return redacted; }; const getAuthHeaders = () => { const headers = {}; if (!RED.settings || typeof RED.settings.get !== 'function') return headers; const authTokens = RED.settings.get("auth-tokens") || {}; const token = authTokens.access_token || authTokens.token; if (token) headers.Authorization = `Bearer ${token}`; return headers; }; const ajaxJson = (options) => $.ajax({ dataType: 'json', ...options, headers: { ...getAuthHeaders(), ...(options.headers || {}), }, }); RED.nodes.registerType('nuclio-function', { category: 'config', icon: "nuclio.io.ico", defaults: { server: { type: 'nuclio-config', required: false, }, project: { type: 'nuclio-project', required: false, }, name: { value: '', validate: function(v) { if(!v) { v = randomEndpoint(); let existing = getUniqueValues(this, n=>n.name); while (existing[v]) { v = randomEndpoint(); } this.name = v; } return !getUniqueValues(this, n=>n.name)[v]; } }, runtime: { value: 'python:3.11' }, code: { value: SAMPLES.python.code }, configCode: { value: SAMPLES.python.config }, env_vars: { value: [] }, secret_vars: { value: [] }, }, label: function () { return this.name || 'nuclio fn'; }, oneditprepare: function() { let node = this; node.originalName = node.name; // <!-- ----------------------------------------------------------------------- --> // <!-- Buttons / Links --> // <!-- ----------------------------------------------------------------------- --> const updateDashboardLink = () => { const name = $('#node-config-input-name').val(); const project = RED.nodes.node(node.project)?.name || 'default'; const server = RED.nodes.node(node.server); // const url = server?.address || ''; // const proxyPath = `/nuclio/dashboard/projects/${project}/functions/${name}/code?id=${node.id}`; // const fullUrl = url && name ? proxyPath : ''; const publicAddress = server?.publicAddress || ''; const fullUrl = publicAddress && name ? `${publicAddress.replace(/\/$/,'')}/projects/${project}/functions/${name}/code` : ''; if (fullUrl) { $('#nuclio-dashboard-link').attr('href', fullUrl).removeClass('nuclio-hidden'); } else { $('#nuclio-dashboard-link').addClass('nuclio-hidden'); } }; updateDashboardLink(); $("#nuclio-dashboard-link").on('pointerdown', function () { let name = $('#node-config-input-name').val(); if (!name) return; updateDashboardLink(); }); $('#node-config-input-name').on('input', updateDashboardLink); $('#node-config-input-project, #node-config-input-server').on('change', updateDashboardLink); $('#nuclio-redeploy').on('click', function() { ajaxJson({ url: `/nuclio/api/functions/deploy?id=${node.id}`, method: 'POST', }).done(function(data) { console.log(data); }).fail((e) => { alert('Error redeploying: ' + JSON.stringify(e)); }); }) $('#nuclio-generate-name').on('click', function() { let name = randomEndpoint(); let existing = getUniqueValues(node, n=>n.name); while (existing[name]) { name = randomEndpoint(); } $('#node-config-input-name').val(name); }); // <!-- ----------------------------------------------------------------------- --> // <!-- Code Editors --> // <!-- ----------------------------------------------------------------------- --> const runtime = node.runtime || 'python:3.11'; const [runtimeBase, runtimeVersion] = runtime.split(':'); const lang = { python: 'python', golang: 'go', nodejs: 'javascript', shell: 'shell', }[runtimeBase]; this.editor = RED.editor.createEditor({ id: 'node-config-input-nuclio-editor', mode: `ace/mode/${lang}`, value: this.code || "", // height: '700px', // stateId: stateId, focus: true, // extraLibs: extraLibs }); this.configeditor = RED.editor.createEditor({ id: 'node-config-input-nuclio-config-editor', mode: `ace/mode/yaml`, value: this.configCode || "", // height: '700px', // stateId: stateId, focus: true, // extraLibs: extraLibs }); $('#node-config-input-runtime').on('change', function() { const runtime = $(this).val(); const [runtimeBase, runtimeVersion] = runtime.split(':'); const lang = { python: 'python', golang: 'go', nodejs: 'javascript', shell: 'shell', }[runtimeBase]; console.log("Changing runtime & language", runtime, lang); let code = node.editor.getValue(); let configCode = node.configeditor.getValue(); const codes = Object.values(SAMPLES).map(v => v.code); const configCodes = Object.values(SAMPLES).map(v => v.config); if(code.trim() === '' || codes.includes(code)) { code = SAMPLES[runtimeBase].code; node.editor.setValue(code); } if(configCode.trim() === '' || configCodes.includes(configCode)) { node.configeditor.setValue(SAMPLES[runtimeBase].config); } node.editor.setMode(`ace/mode/${lang}`); }); // <!-- ----------------------------------------------------------------------- --> // <!-- Settings --> // <!-- ----------------------------------------------------------------------- --> this.envList = $("#node-config-input-env_vars-x").editableList({ addItem: function(row, index, data) { $(`<input class='node-input-key' type="text" placeholder="VAR_NAME" />`).appendTo(row).val(data.name) $(`<input class='node-input-value' placeholder="value" />`).appendTo(row).attr('value', data.value).typedInput({ defaultType: data.type || 'str', types: ['str','num','json','env','cred'], }); }, removable: true, }); for(let data of this.env_vars || []) { this.envList.editableList('addItem', data) } // TODO: add secrets/overrides support e.g. with `build.codeEntryType: "github"` // build.codeEntryAttributes.headers.Authorization: "my-GitHub-access-token" (typedInput: cred/env) this.secretList = $("#node-config-input-secret_vars-x").editableList({ addItem: function(row, index, data) { $(`<input class='node-input-key' type="text" placeholder="VAR_NAME" />`).appendTo(row).val(data.name) $(`<input class='node-input-value' placeholder="value" />`).appendTo(row).attr('value', data.value).typedInput({ defaultType: data.type || 'cred', types: ['env','cred'], }); }, removable: true, }); for(let data of this.secret_vars || []) { this.secretList.editableList('addItem', data) } // <!-- ----------------------------------------------------------------------- --> // <!-- Status Page --> // <!-- ----------------------------------------------------------------------- --> this.statusPoller = new Poller(() => { ajaxJson({ url: `/nuclio/api/functions?id=${this.id}`, method: 'GET', }).done(function (data) { const { status, metadata, spec } = data; const { logs, ...statusMeta } = status; const state = status.state; const safeSpec = redactSecrets(spec, node.secret_vars); const tone = statusTone(state); if (canUpdateElement(document.getElementById('nuclio-status'))) { $('#nuclio-status').html(statusChips(statusMeta)); } $('#nuclio-status-state').text(state || 'unknown'); $('#nuclio-status-chip').attr('data-tone', tone); $('#nuclio-status-updated').text(`Updated ${new Date().toLocaleTimeString()}`); if (canUpdateElement(document.getElementById('nuclio-deploy-logs'))) { $('#nuclio-deploy-logs').html(logs?.map(({ level, message, time, name, requestID, ...log }) => //builderKind, versionInfo, `<div><b>${level}</b> ${message} ${Object.keys(log).length ? `\n<div style="padding-left: 12px;">${fmt(log)}</div>` : ''}</div>`).join('\n')); } if (canUpdateElement(document.getElementById('nuclio-spec'))) { $('#nuclio-spec').text(JSON.stringify({ metadata, spec: safeSpec }, null, 2)); } }).fail((e) => { const message = e?.responseJSON?.error || e?.statusText || 'error'; if (canUpdateElement(document.getElementById('nuclio-status'))) { $('#nuclio-status').html(statusChips({ error: message })); } $('#nuclio-status-state').text('error'); $('#nuclio-status-chip').attr('data-tone', 'error'); }); ajaxJson({ url: `/nuclio/api/functions/logs?id=${this.id}`, method: 'GET', }).done(function (data) { // $('#nuclio-logs').text(JSON.stringify(data, null, 2)); const logHtml = Object.entries(data||{}).map(([replica, log]) => //builderKind, versionInfo, `<div><b>${replica}</b> \n<div style='padding-left: 12px'>${log || '-- no logs --'}</div></div>`).join('\n'); if (canUpdateElement(document.getElementById('nuclio-logs'))) { $('#nuclio-logs').html(logHtml); } $('#nuclio-logs').closest('.nuclio-logbox').toggleClass('nuclio-hidden', !logHtml.trim()); }).fail((e) => { const message = e?.responseJSON?.error || e?.statusText || 'error'; if (canUpdateElement(document.getElementById('nuclio-logs'))) { $('#nuclio-logs').text(message); } $('#nuclio-logs').closest('.nuclio-logbox').removeClass('nuclio-hidden'); }); }, 2400); // <!-- ----------------------------------------------------------------------- --> // <!-- Tabs --> // <!-- ----------------------------------------------------------------------- --> var tabs = RED.tabs.create({ id: "nuclio-tabs", onchange: function(tab) { $("#nuclio-tabs-content").children().hide(); $("#" + tab.id).show(); if (tab.id === "nuclio-tab-status") { node.statusPoller.start(); } else { node.statusPoller.stop(); } } }); tabs.addTab({ id: "nuclio-tab-app", label: "Code" }); tabs.addTab({ id: "nuclio-tab-deps", label: "Config" }); tabs.addTab({ id: "nuclio-tab-status", label: "Status" }); tabs.activateTab("nuclio-tab-app"); }, oneditsave: function() { let node = this; this.code = this.editor.getValue(); this.editor.destroy(); delete this.editor; this.configCode = this.configeditor.getValue(); this.configeditor.destroy(); delete this.configeditor; this.statusPoller.stop(); delete this.statusPoller; node.env_vars = []; $("#node-config-input-env_vars-x").editableList('items').each(function(i) { let name = $(this).find(".node-input-key").val(); let type = $(this).find(".node-input-value").typedInput('type'); let value = $(this).find(".node-input-value").typedInput('value'); name && node.env_vars.push({ name, type, value }); }) delete this.envList; node.secret_vars = []; $("#node-config-input-secret_vars-x").editableList('items').each(function(i) { let name = $(this).find(".node-input-key").val(); let type = $(this).find(".node-input-value").typedInput('type'); let value = $(this).find(".node-input-value").typedInput('value'); name && node.secret_vars.push({ name, type, value }); }) delete this.secretList; }, oneditcancel: function() { this.editor.destroy(); delete this.editor; this.configeditor.destroy(); delete this.configeditor; this.statusPoller.stop(); delete this.statusPoller; this.name = this.originalName; delete this.envList; delete this.secretList; }, }); </script> <!-- ----------------------------------------------------------------------- Form UI ----------------------------------------------------------------------- --> <script type="text/html" data-template-name="nuclio-function"> <style> #nuclio-tabs-content { height: 100%; min-height: 350px; max-height: calc(100% - 95px); } #nuclio-tabs-content > * { height: 100%; display: flex; flex-direction: column; justify-content: stretch; } #nuclio-tabs-content .node-text-editor { flex-grow: 1; } /* Editable List */ #nuclio-tabs-content .node-input-list-row > label { /* font-size: 1em; */ /* font-weight: 700; */ margin: 12px 0 0 4px; width: 100%; } #nuclio-tabs-content .red-ui-editableList-item-content { display: flex; justify-content: stretch; } #nuclio-tabs-content .red-ui-editableList-item-content > * { flex-grow: 1; } #nuclio-tabs-content .red-ui-editableList-container input.node-input-key { flex-basis: 100px; flex-grow: 0; } #node-config-input-env_vars-x .red-ui-typedInput-container, #nuclio-tabs-content .node-input-list-row .red-ui-editableList-container input { border-radius: 0; } #nuclio-tabs-content .red-ui-editableList-container input { width: auto; } #nuclio-tabs-content a { text-decoration: underline; color: inherit; } /* Status tab */ #nuclio-tab-status { font-family: "Optima", "Segoe UI", "Trebuchet MS", sans-serif; color: #1d2a33; line-height: 1.4; } .nuclio-status-shell { border-radius: 12px; padding: 16px; background: linear-gradient(135deg, #f7f9fb, #eef2f6); border: 1px solid #e0e6ee; letter-spacing: 0.01em; } .nuclio-status-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } .nuclio-status-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .nuclio-status-label { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: #6b7a86; } .nuclio-status-chip { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; font-weight: 600; background: #e8eef3; border: 1px solid #d3dde6; } .nuclio-status-chip[data-tone="ok"] { background: #e7f7ee; border-color: #b9e7cd; color: #147a42; } .nuclio-status-chip[data-tone="working"] { background: #fff3da; border-color: #f0d29b; color: #9a6b15; } .nuclio-status-chip[data-tone="warn"] { background: #fff0e6; border-color: #f2c7aa; color: #a34811; } .nuclio-status-chip[data-tone="error"] { background: #ffe6e6; border-color: #f3b2b2; color: #b21c1c; } .nuclio-status-chip[data-tone="idle"] { background: #eef1f4; border-color: #d7dde4; color: #4b5b66; } .nuclio-status-chip[data-tone="unknown"] { background: #f3f4f6; border-color: #dfe3e8; color: #6b7a86; } .nuclio-status-meta { font-size: 12px; color: #6b7a86; } .nuclio-status-actions { display: flex; gap: 8px; align-items: center; } .nuclio-status-actions a, .nuclio-status-actions button { border-radius: 8px; padding: 6px 12px; border: 1px solid #d3dde6; background: #ffffff; color: #1d2a33; text-decoration: none; font-weight: 600; cursor: pointer; } .nuclio-status-actions button { background: #1d2a33; color: #ffffff; border-color: #1d2a33; } .nuclio-status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; } .nuclio-card { background: #ffffff; border: 1px solid #e0e6ee; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 8px; } .nuclio-card.compact { padding: 10px 12px; } .nuclio-card-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: #6b7a86; } .nuclio-card.full { grid-column: 1 / -1; } .nuclio-chips { display: flex; flex-wrap: wrap; gap: 8px; } .nuclio-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; background: #f3f6f9; border: 1px solid #e0e6ee; font-size: 12px; color: #1d2a33; } .nuclio-chip-key { text-transform: lowercase; letter-spacing: 0.02em; color: #5a6a76; } .nuclio-chip-value { font-family: "SFMono-Regular", "Menlo", "Consolas", "Liberation Mono", monospace; font-weight: 600; } .nuclio-kv { display: grid; gap: 6px; } .nuclio-kv-row { display: grid; grid-template-columns: 180px 1fr; gap: 8px; align-items: start; font-size: 12px; } .nuclio-kv-key { color: #51606b; text-transform: lowercase; letter-spacing: 0.02em; } .nuclio-kv-value { color: #1d2a33; word-break: break-word; } .nuclio-kv-multiline { white-space: pre-wrap; } .nuclio-logbox { margin: 0; background: #f7f9fb; border-radius: 8px; padding: 10px; overflow: auto; overflow-x: auto; max-height: 240px; min-height: 60px; border: 1px solid #e6ecf2; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; } .nuclio-hidden { display: none; } .red-ui-editor pre.nuclio-logbox code { white-space: pre; } #nuclio-logs, #nuclio-deploy-logs, #nuclio-status, #nuclio-spec { font-family: "SFMono-Regular", "Menlo", "Consolas", "Liberation Mono", monospace; font-size: 12px; line-height: 1.5; } #nuclio-spec { max-height: 320px; } </style> <div class="form-row nuclio-tabs-row"> <ul style="min-width: 600px; margin-bottom: 20px;" id="nuclio-tabs"></ul> </div> <div id="nuclio-tabs-content" style="min-height: calc(100% - 25px);"> <div id="nuclio-tab-app" style="display:none"> <div class="form-row"> <p> <h2 style="display: inline-block;"> <a target="_blank" href="https://docs.nuclio.io/en/stable/index.html" title="">Nuclio</a> &nbsp;&nbsp; </h2> <a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/python/python-reference.html">Python</a> &nbsp;&nbsp; <a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/golang/golang-reference.html">Go</a> &nbsp;&nbsp; <a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/nodejs/nodejs-reference.html">NodeJS</a> &nbsp;&nbsp; <a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/shell/shell-reference.html">Shell</a> &nbsp;&nbsp; <a target="_blank" href="https://docs.nuclio.io/en/stable/examples/README.html">Examples</a> &nbsp;&nbsp; <a target="_blank" href="https://docs.nuclio.io/en/stable/reference/function-configuration/batching.html" title="Request batching to handle multiple requests at once.">Batching</a> &nbsp;&nbsp; <a target="_blank" href="https://docs.nuclio.io/en/stable/reference/function-configuration/function-configuration-reference.html" title="">Ref.</a> &nbsp;&nbsp; </p> </div> <div class="form-row"> <div> <label for="node-config-input-name"> <span>Function Name</span> </label> <input type="text" id="node-config-input-name" placeholder="What should the URL endpoint be?" title="This is the URL that nodered will call to run this function." /> <!-- <small class="name-error">Name must be unique.</small> --> <button id="nuclio-generate-name" class="btn btn-default" title="Generate a random name">🔀</button> </div> </div> <div class="form-row"> <div> <label for="node-config-input-runtime"> <span>Runtime [<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/runtimes/index.html" title="">docs</a>]:</span></span> </label> <select id="node-config-input-runtime" class="form-control"> <option value="python:3.12">python:3.12 (entrypoint: handler)</option> <option value="python:3.11">python:3.11 (entrypoint: handler)</option> <option value="python:3.10">python:3.10 (entrypoint: handler)</option> <option value="python:3.9">python:3.9 (entrypoint: handler)</option> <option value="golang">golang (entrypoint: Handler)</option> <option value="nodejs">nodejs (entrypoint: handler)</option> <option value="shell">shell (stdin/stdout)</option> <!-- <option value="ruby">Ruby</option> --> </select> </div> </div> <div style="min-height:150px;" class="node-text-editor" id="node-config-input-nuclio-editor" ></div> </div> <div id="nuclio-tab-deps" style="display:none"> <div class="form-row"> <label for="node-config-input-server">Server</label> <input type="text" id="node-config-input-server" /> <!-- <div> <small>What Nuclio Server should this deploy to?</small> </div> --> </div> <div class="form-row"> <label for="node-config-input-project"> <span>Project</span> </label> <input type="text" id="node-config-input-project" placeholder="default" /> </div> <div class="form-row"> <h2>Function Config [<a target="_blank" href="https://docs.nuclio.io/en/stable/reference/function-configuration/function-configuration-reference.html" title="">docs</a>]:</h2> <p> </p> </div> <div style="min-height:150px;" class="node-text-editor" id="node-config-input-nuclio-config-editor" ></div> <div class="form-row node-input-list-row"> <label for="node-config-input-env_vars-x"> Environment Variables </label> <p> <small>You can specify environment variables that will be available to your application at runtime.</small> </p> <ol id="node-config-input-env_vars-x"></ol> </div> <div class="form-row node-input-list-row"> <label for="node-config-input-secret_vars-x"> Secret Overrides </label> <p> <small>You can specify secret variables (e.g. build.codeEntryAttributes.s3SecretAccessKey: my-secret-@ccess-k3y)</small> </p> <ol id="node-config-input-secret_vars-x"></ol> </div> </div> <div id="nuclio-tab-status" style="display:none"> <div class="nuclio-status-shell"> <div class="nuclio-status-header"> <div class="nuclio-status-left"> <div class="nuclio-status-label">Status</div> <div class="nuclio-status-chip" id="nuclio-status-chip" data-tone="unknown"> <span id="nuclio-status-state">unknown</span> </div> <div class="nuclio-status-meta" id="nuclio-status-updated">Updated —</div> </div> <div class="nuclio-status-actions"> <a id="nuclio-dashboard-link" href="about:blank" target="_blank" rel="noopener noreferrer">Dashboard</a> <button id="nuclio-redeploy">Redeploy</button> </div> </div> <div class="nuclio-status-grid"> <section class="nuclio-card compact full"> <div class="nuclio-card-title">Runtime Status</div> <div class="nuclio-chips" id="nuclio-status"></div> </section> <section class="nuclio-card full"> <div class="nuclio-card-title">Run Logs</div> <pre class="nuclio-logbox"><code id="nuclio-logs"></code></pre> </section> <section class="nuclio-card full"> <div class="nuclio-card-title">Build Logs</div> <pre class="nuclio-logbox"><code id="nuclio-deploy-logs"></code></pre> </section> <section class="nuclio-card full"> <div class="nuclio-card-title">Spec</div> <pre class="nuclio-logbox"><code id="nuclio-spec"></code></pre> </section> </div> </div> </div> </div> </script> <script type="text/html" data-template-name="nuclio"> <style> #node-input-headers .red-ui-editableList-item-content { display: flex; align-items: center; gap: 6px; } #node-input-headers .red-ui-editableList-item-content > input.node-input-key { flex: 0 0 40%; } #node-input-headers .red-ui-editableList-item-content > .red-ui-typedInput-container { flex: 1 1 auto; min-width: 0; } #node-input-headers .red-ui-editableList-item-content > input.node-input-value { width: 100%; } </style> <div class="form-row"> <label for="node-input-function"> <span>Function</span> </label> <input type="text" id="node-input-function" /> <div> <small>Invoke a shared Nuclio function configuration.</small> </div> </div> <div class="form-row"> <label for="node-input-timeoutMs"> <span>Timeout (ms)</span> </label> <input type="text" id="node-input-timeoutMs" placeholder="30000" /> <div> <small>Function call timeout in milliseconds.</small> </div> </div> <div class="form-row"> <label for="node-input-maxInFlight"> <span>Concurrency Cap</span> </label> <input type="text" id="node-input-maxInFlight" placeholder="0 = unlimited" /> <div> <small>Limits in-flight requests; excess messages go to the fallback output.</small> </div> </div> <div class="form-row node-input-list-row"> <label for="node-input-headers"> Request Headers </label> <div> <small>Optional headers to include with each invocation.</small> </div> <ol id="node-input-headers"></ol> </div> </script> <script type="text/html" data-help-name="nuclio-function"> <p>Deploys and reconciles a Nuclio function from Node-RED.</p> <h3>Inputs</h3> <dl class="message-properties"> <dt>Code</dt> <dd>Function source code stored in this config node.</dd> <dt>Config</dt> <dd>YAML function configuration applied on deploy.</dd> <dt>Environment Variables</dt> <dd>Key/value pairs injected into the function environment.</dd> <dt>Secret Overrides</dt> <dd>Typed values written into the config at the specified paths.</dd> </dl> <h3>Details</h3> <p>This config node owns deployment and reconciliation. It creates or updates the Nuclio function in the selected project and keeps status refreshed while open.</p> <p>Use a separate <b>Nuclio Invoke</b> node to call the function at runtime.</p> </script> <script type="text/html" data-help-name="nuclio"> <p>Invokes a shared Nuclio function configuration.</p> <h3>Inputs</h3> <dl class="message-properties"> <dt>payload</dt> <dd>Request body sent to the function.</dd> </dl> <h3>Outputs</h3> <ol class="node-ports"> <li>Result <dl class="message-properties"> <dt>payload</dt> <dd>Response body from the function.</dd> <dt>statusCode</dt> <dd>HTTP status code returned by the function.</dd> <dt>statusText</dt> <dd>HTTP status text.</dd> <dt>headers</dt> <dd>Response headers.</dd> <dt>response</dt> <dd>Raw axios response object.</dd> <dt>requestTime</dt> <dd>Elapsed time in ms for the request.</dd> </dl> </li> <li>Fallback <dl class="message-properties"> <dt>payload</dt> <dd>Error payload if the function is unavailable.</dd> <dt>error</dt> <dd>Error object when present.</dd> </dl> </li> </ol> <h3>Details</h3> <p>Select a <b>Nuclio Function</b> config node to invoke. Optional headers, timeout, and concurrency cap apply per node instance.</p> </script> <script type="text/html" data-help-name="nuclio-config"> <p>Defines the Nuclio dashboard address used by Node-RED.</p> <h3>Fields</h3> <dl class="message-properties"> <dt>Nuclio Dashboard URL</dt> <dd>Address reachable by Node-RED (typically <code>http://host:8070</code>).</dd> <dt>Public Nuclio Dashboard URL</dt> <dd>Optional public address used for links in the editor.</dd> </dl> </script> <script type="text/html" data-help-name="nuclio-project"> <p>Defines the Nuclio project name used for deployment.</p> <h3>Fields</h3> <dl class="message-properties"> <dt>Nuclio Project Name</dt> <dd>Project to create or update functions within. Defaults to <code>default</code>.</dd> </dl> </script>