diff --git a/lib/launcher.js b/lib/launcher.js index 7b6a0bde..13ae2116 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -258,7 +258,7 @@ class Launcher { const libraryEnabled = this.settings?.features ? this.settings.features['shared-library'] : false if (libraryEnabled && this.config.licensed) { settings.nodesDir = settings.nodesDir || [] - settings.nodesDir.push(path.join(__dirname, 'plugins', 'node_modules', '@flowforge', 'flowforge-library-plugin')) + settings.nodesDir.push(path.join(__dirname, 'plugins', 'node_modules', '@flowfuse', 'flowfuse-library-plugin')) const sharedLibraryConfig = { id: 'flowfuse-team-library', type: 'flowfuse-team-library', @@ -274,6 +274,22 @@ class Launcher { sources: [sharedLibraryConfig] } } + if (this.config.licensed) { + settings.nodesDir = settings.nodesDir || [] + settings.nodesDir.push(path.join(__dirname, 'plugins', 'node_modules', '@flowfuse', 'flowfuse-blueprint-plugin')) + settings.editorTheme.library = settings.editorTheme.library || { sources: [] } + settings.editorTheme.library.sources.push({ + id: 'flowfuse-blueprint-library', + type: 'flowfuse-blueprint-library', + label: 'Blueprints', + icon: 'font-awesome/fa-map-o', + types: ['flows'], + readOnly: true, + forgeURL: this.config.forgeURL, + teamID: settings.flowforge.teamID, + token: this.config.token + }) + } if (this.config.https) { // The `https` config can contain any valid setting from the Node-RED // https object. For convenience, the `key`, `ca` and `cert` settings diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-blueprint-plugin/blueprintPlugin.js b/lib/plugins/node_modules/@flowfuse/flowfuse-blueprint-plugin/blueprintPlugin.js new file mode 100644 index 00000000..c629b98f --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-blueprint-plugin/blueprintPlugin.js @@ -0,0 +1,175 @@ +/* + * IMPORTANT + * This plugin is a duplicate of the one in nr-launcher/lib/storage/blueprintPlugin.js + Any changes made to either should be made to both. + * This is needed here because a device, edited via a tunnel in developer mode, also needs to + be able to access the team library. + * NOTE + * HttpProxyAgent & HttpsProxyAgent are specific to the device-agent only +*/ +const got = require('got') + +const getProxyAgent = () => { + const agent = {} + if (process.env.http_proxy) { + const HttpAgent = require('http-proxy-agent').HttpProxyAgent + agent.http = new HttpAgent(process.env.http_proxy, { timeout: 2000 }) + } + if (process.env.https_proxy) { + const HttpsAgent = require('https-proxy-agent').HttpsProxyAgent + agent.https = new HttpsAgent(process.env.https_proxy, { timeout: 2000 }) + } + return agent +} + +module.exports = function (RED) { + const PLUGIN_TYPE_ID = 'flowfuse-blueprint-library' + + // We do not retrieve the blueprint list on every request; we get it once and cache the result. + // The cache is expired after 2 minutes. Most interactions with the library will be short-lived + // and within this interval - so this is the right balance between performance and freshness. + const blueprintCache = {} + let cacheLastRefreshedAt = 0 + const CACHE_EXPIRY_PERIOD = 2 * 60 * 1000 // 2 minutes in milliseconds + + // global-config support for imported modules was added in 4.1. + const versionParts = RED.version().split('.') + const globalConfigSupported = (versionParts[0] === '4' && versionParts[1] >= '1') || (versionParts[0] > '4') + + class FFBlueprintLibraryPlugin { + constructor (config) { + this.type = PLUGIN_TYPE_ID + this.id = config.id + this.label = config.label + const { forgeURL, teamID, token } = config + if (!teamID) { + throw new Error('Missing required configuration property: teamID') + } + this.teamID = teamID + if (!forgeURL) { + throw new Error('Missing required configuration property: forgeURL') + } + if (!token) { + throw new Error('Missing required configuration property: token') + } + this._client = got.extend({ + prefixUrl: config.forgeURL + '/api/v1/flow-blueprints/', + headers: { + 'user-agent': 'FlowFuse Blueprint Library Plugin v0.1', + authorization: 'Bearer ' + token + }, + timeout: { + request: 10000 + }, + agent: getProxyAgent() + }) + } + + /** + * Initialise the store. + */ + async init () { + } + + /** + * Get an entry from the store + * @param {string} type The type of entry - this library only supports 'flow' + * @param {string} path The path to the library entry + * @return if 'path' resolves to a single entry, it returns the contents + * of that entry. + * if 'path' resolves to a 'directory', it returns a listing of + * the contents of the directory + * if 'path' is not valid, it should throw a suitable error + */ + async getEntry (type, name) { + if (type !== 'flows') { + // This should not happen as the library is registred with `types: ['flows']` to restrict + // where it is exposed in Node-RED. + throw new Error(`FlowFuse Blueprint Library Plugin: Unsupported type '${type}' - only 'flow' is supported`) + } + await this.loadBlueprints() + if (!name) { + const categories = Object.keys(blueprintCache) + categories.sort() + return categories + } + if (name.endsWith('/')) { + // If the name ends with a slash, return the contents of that directory + const category = name.slice(0, -1) // Remove the trailing slash + const blueprints = blueprintCache[category] || [] + return blueprints.map(blueprint => { + return { + fn: blueprint.name + } + }) + } else { + // A blueprint name was provided: / + const [category, blueprintName] = name.split(/\/(.*)/s) + if (blueprintCache[category]) { + const blueprint = blueprintCache[category].find(b => b.name === blueprintName) + if (blueprint) { + const blueprintRequest = await this._client.get(blueprint.id) + const blueprintDetails = JSON.parse(blueprintRequest.body) + const flows = blueprintDetails.flows.flows || [] + if (globalConfigSupported) { + // Add/update the `global-config` node with module information + // This will allow Node-RED 4.1+ to notify the user about what modules + // are required. + let globalConfig = flows.find(node => node.type === 'global-config') + if (!globalConfig) { + globalConfig = { + type: 'global-config', + id: RED.util.generateId(), + env: [], + modules: {} + } + flows.unshift(globalConfig) + } else { + globalConfig.modules = globalConfig.modules || {} + } + for (const moduleName of Object.keys(blueprintDetails.modules || {})) { + if (moduleName !== 'node-red') { + globalConfig.modules[moduleName] = globalConfig.modules[moduleName] || blueprintDetails.modules[moduleName] + } + } + } + return flows || [] + } else { + throw new Error(`Blueprint ${blueprintName} not found in category ${category}`) + } + } + } + return [] + } + + async loadBlueprints () { + if (Date.now() - cacheLastRefreshedAt < CACHE_EXPIRY_PERIOD) { + return + } + try { + const result = await this._client.get('', { + searchParams: { + filter: 'active', + team: this.teamID + } + }) + const blueprints = JSON.parse(result.body) + for (const blueprint of blueprints.blueprints) { + blueprintCache[blueprint.category || 'blueprints'] = blueprintCache[blueprint.category || 'blueprints'] || [] + blueprintCache[blueprint.category || 'blueprints'].push(blueprint) + } + cacheLastRefreshedAt = Date.now() + } catch (err) { + RED.log.error(`FlowFuse Blueprint Plugin: failed to load blueprints: ${err}`) + } + } + } + + RED.plugins.registerPlugin(PLUGIN_TYPE_ID, { + type: 'node-red-library-source', + class: FFBlueprintLibraryPlugin, + onadd: () => { + RED.log.info('FlowFuse Blueprint Library Plugin loaded') + } + }) +} diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-blueprint-plugin/package.json b/lib/plugins/node_modules/@flowfuse/flowfuse-blueprint-plugin/package.json new file mode 100644 index 00000000..c46c1c38 --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-blueprint-plugin/package.json @@ -0,0 +1,19 @@ +{ + "name": "@flowfuse/flowfuse-blueprint-library-plugin", + "version": "0.0.1", + "description": "FlowFuse Blueprint library plugin for Node-RED", + "keywords": [ + "node-red", + "flowfuse" + ], + "author": { + "name": "FlowFuse Inc." + }, + "license": "Apache-2.0", + "node-red": { + "version": ">=2.2.0", + "plugins": { + "flowfuse-library": "blueprintPlugin.js" + } + } +} \ No newline at end of file diff --git a/lib/plugins/node_modules/@flowforge/flowforge-library-plugin/libraryPlugin.js b/lib/plugins/node_modules/@flowfuse/flowfuse-library-plugin/libraryPlugin.js similarity index 100% rename from lib/plugins/node_modules/@flowforge/flowforge-library-plugin/libraryPlugin.js rename to lib/plugins/node_modules/@flowfuse/flowfuse-library-plugin/libraryPlugin.js diff --git a/lib/plugins/node_modules/@flowforge/flowforge-library-plugin/package.json b/lib/plugins/node_modules/@flowfuse/flowfuse-library-plugin/package.json similarity index 91% rename from lib/plugins/node_modules/@flowforge/flowforge-library-plugin/package.json rename to lib/plugins/node_modules/@flowfuse/flowfuse-library-plugin/package.json index 5adb8d0c..2b0a20da 100644 --- a/lib/plugins/node_modules/@flowforge/flowforge-library-plugin/package.json +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-library-plugin/package.json @@ -1,5 +1,5 @@ { - "name": "@flowforge/flowforge-library-plugin", + "name": "@flowfuse/flowfuse-library-plugin", "version": "0.0.1", "description": "FlowFuse library plugin for Node-RED", "keywords": [ diff --git a/test/unit/lib/plugins/libraryPlugin_spec.js b/test/unit/lib/plugins/libraryPlugin_spec.js index 1aea8aa6..e391f9a8 100644 --- a/test/unit/lib/plugins/libraryPlugin_spec.js +++ b/test/unit/lib/plugins/libraryPlugin_spec.js @@ -3,7 +3,7 @@ const should = require('should') // eslint-disable-line const sinon = require('sinon') // eslint-disable-line const HttpProxyAgent = require('http-proxy-agent').HttpProxyAgent const HttpsProxyAgent = require('https-proxy-agent').HttpsProxyAgent -const pluginModule = require('../../../../lib/plugins/node_modules/@flowforge/flowforge-library-plugin/libraryPlugin.js') +const pluginModule = require('../../../../lib/plugins/node_modules/@flowfuse/flowfuse-library-plugin/libraryPlugin.js') let FFTeamLibraryPluginClass = null // simulate RED object to capture the plugin class for performing unit tests on the internal plugin