From b7e6a14fa615932abbee09bc1df78feefe3794f4 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Fri, 15 Nov 2024 15:27:10 +0000 Subject: [PATCH 01/24] Stash --- .../lib/deviceEditor/DeviceTunnelManager.js | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js index cc53ffefbc..f5159566bc 100644 --- a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js +++ b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js @@ -32,6 +32,8 @@ * @typedef {(connection: WebSocket, request: FastifyRequest) => void} wsHandler */ +const fs = require('node:fs') + class DeviceTunnelManager { // private members /** @type {Map} */ #tunnels @@ -236,14 +238,41 @@ class DeviceTunnelManager { /** @type {httpHandler} */ tunnel._handleHTTPGet = (request, reply) => { - const id = tunnel.nextRequestId++ - tunnel.requests[id] = reply - tunnel.socket.send(JSON.stringify({ - id, - method: request.method, - headers: request.headers, - url: request.url.substring(`/api/v1/devices/${tunnel.deviceId}/editor/proxy`.length) - })) + const url = request.url.substring(`/api/v1/devices/${tunnel.deviceId}/editor/proxy`.length) + if (url === '/vendor/monaco/dist/editor.js') { + console.log('*********************** monoco') + const data = fs.readFileSync('/tmp/editor.js') + reply.headers({ + 'Content-Type': 'application/json', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else if (url.startsWith('/vendor/vendor.js')) { + console.log('*********************** vendor', url) + const data = fs.readFileSync('/tmp/vendor.js') + reply.headers({ + 'Content-Type': 'application/json', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else if (url.startsWith('/vendor/mermaid/mermaid.min.js')) { + console.log('*********************** mermaid', url) + const data = fs.readFileSync('/tmp/mermaid.min.js') + reply.headers({ + 'Content-Type': 'application/json', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else { + const id = tunnel.nextRequestId++ + tunnel.requests[id] = reply + tunnel.socket.send(JSON.stringify({ + id, + method: request.method, + headers: request.headers, + url: request.url.substring(`/api/v1/devices/${tunnel.deviceId}/editor/proxy`.length) + })) + } } tunnel._handleHTTP = (request, reply) => { From c4f495e912dbb5d60f95c65dcbe6ba7be818a678 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Fri, 15 Nov 2024 17:06:41 +0000 Subject: [PATCH 02/24] PoC --- forge/db/controllers/Device.js | 3 + .../20241115-01-add-nr-version-device.js | 19 +++++ forge/db/models/Device.js | 1 + .../lib/deviceEditor/DeviceTunnelManager.js | 83 +++++++++++++------ 4 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 forge/db/migrations/20241115-01-add-nr-version-device.js diff --git a/forge/db/controllers/Device.js b/forge/db/controllers/Device.js index b449075de0..4c6664d7c7 100644 --- a/forge/db/controllers/Device.js +++ b/forge/db/controllers/Device.js @@ -43,6 +43,9 @@ module.exports = { if (state.agentVersion) { device.set('agentVersion', state.agentVersion) } + if (state.nodeRedVersion) { + device.set('nodeRedVersion', state.nodeRedVersion) + } device.set('editorAffinity', state.affinity || null) if (!state.snapshot || state.snapshot === '0') { if (device.activeSnapshotId !== null) { diff --git a/forge/db/migrations/20241115-01-add-nr-version-device.js b/forge/db/migrations/20241115-01-add-nr-version-device.js new file mode 100644 index 0000000000..60cd2b3d82 --- /dev/null +++ b/forge/db/migrations/20241115-01-add-nr-version-device.js @@ -0,0 +1,19 @@ +/** + * Add Node-RED version to Device table + */ +const { DataTypes } = require('sequelize') + +module.exports = { + /** + * upgrade database + * @param {QueryInterface} context Sequelize.QueryInterface + */ + up: async (context) => { + await context.addColumn('Devices', 'nodeRedVersion', { + type: DataTypes.TEXT, + defaultValue: null + }) + }, + down: async (context) => { + } +} \ No newline at end of file diff --git a/forge/db/models/Device.js b/forge/db/models/Device.js index 1dfa227ebd..0cdf580302 100644 --- a/forge/db/models/Device.js +++ b/forge/db/models/Device.js @@ -35,6 +35,7 @@ module.exports = { lastSeenAt: { type: DataTypes.DATE, allowNull: true }, settingsHash: { type: DataTypes.STRING, allowNull: true }, agentVersion: { type: DataTypes.STRING, allowNull: true }, + nodeRedVersion: { type: DataTypes.STRING, allowNull: true }, mode: { type: DataTypes.STRING, allowNull: true, defaultValue: 'autonomous' }, editorAffinity: { type: DataTypes.STRING, defaultValue: '' }, editorToken: { type: DataTypes.STRING, defaultValue: '' }, diff --git a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js index f5159566bc..1c042063b3 100644 --- a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js +++ b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js @@ -35,6 +35,7 @@ const fs = require('node:fs') class DeviceTunnelManager { + // private members /** @type {Map} */ #tunnels @@ -61,6 +62,9 @@ class DeviceTunnelManager { this.closeTunnel(deviceId) } }) + + this.pathPrefix = '/opt/share/projects/flowforge/test/device-cache' // app.config.device.cache_path + this.pathPostfix = 'node_modules/@node-red/editor-client/public/' } /** @@ -161,6 +165,8 @@ class DeviceTunnelManager { device.editorConnected = true await device.save() + tunnel.nodeRedVersion = device.nodeRedVersion + // Handle messages sent from the device tunnel.socket.on('message', msg => { const response = JSON.parse(msg.toString()) @@ -239,31 +245,58 @@ class DeviceTunnelManager { /** @type {httpHandler} */ tunnel._handleHTTPGet = (request, reply) => { const url = request.url.substring(`/api/v1/devices/${tunnel.deviceId}/editor/proxy`.length) - if (url === '/vendor/monaco/dist/editor.js') { - console.log('*********************** monoco') - const data = fs.readFileSync('/tmp/editor.js') - reply.headers({ - 'Content-Type': 'application/json', - 'FF-Proxied': 'true' - }) - reply.send(data) - } else if (url.startsWith('/vendor/vendor.js')) { - console.log('*********************** vendor', url) - const data = fs.readFileSync('/tmp/vendor.js') - reply.headers({ - 'Content-Type': 'application/json', - 'FF-Proxied': 'true' - }) - reply.send(data) - } else if (url.startsWith('/vendor/mermaid/mermaid.min.js')) { - console.log('*********************** mermaid', url) - const data = fs.readFileSync('/tmp/mermaid.min.js') - reply.headers({ - 'Content-Type': 'application/json', - 'FF-Proxied': 'true' - }) - reply.send(data) - } else { + if (tunnel.nodeRedVersion && fs.existsSync(`${manager.pathPrefix}/${tunnel.nodeRedVersion}`)) { + if (url === '/vendor/monaco/dist/editor.js') { + const data = fs.readFileSync(`${manager.pathPrefix}/${tunnel.nodeRedVersion}/${manager.pathPostfix}vendor/monaco/dist/editor.js`) + reply.headers({ + 'Content-Type': 'application/json; charset=UTF-8', + 'Cache-Control': 'public, max-age=0', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else if (url.startsWith('/vendor/vendor.js')) { + const data = fs.readFileSync(`${manager.pathPrefix}/${tunnel.nodeRedVersion}/${manager.pathPostfix}vendor/vendor.js`) + reply.headers({ + 'Content-Type': 'application/json; charset=UTF-8', + 'Cache-Control': 'public, max-age=0', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else if (url.startsWith('/vendor/mermaid/mermaid.min.js')) { + const data = fs.readFileSync(`${manager.pathPrefix}/${tunnel.nodeRedVersion}/${manager.pathPostfix}vendor/mermaid/mermaid.min.js`) + reply.headers({ + 'Content-Type': 'application/json; charset=UTF-8', + 'Cache-Control': 'public, max-age=0', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else if (url.startsWith('/red/red.min.js')) { + const data = fs.readFileSync(`${manager.pathPrefix}/${tunnel.nodeRedVersion}/${manager.pathPostfix}red/red.min.js`) + reply.headers({ + 'Content-Type': 'application/json; charset=UTF-8', + 'Cache-Control': 'public, max-age=0', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else if (url.startsWith('/red/style.min.css')) { + const data = fs.readFileSync(`${manager.pathPrefix}/${tunnel.nodeRedVersion}/${manager.pathPostfix}red/style.min.css`) + reply.headers({ + 'Content-Type': 'text/css; charset=UTF-8', + 'Cache-Control': 'public, max-age=0', + 'FF-Proxied': 'true' + }) + reply.send(data) + } else { + const id = tunnel.nextRequestId++ + tunnel.requests[id] = reply + tunnel.socket.send(JSON.stringify({ + id, + method: request.method, + headers: request.headers, + url: request.url.substring(`/api/v1/devices/${tunnel.deviceId}/editor/proxy`.length) + })) + } + } else { const id = tunnel.nextRequestId++ tunnel.requests[id] = reply tunnel.socket.send(JSON.stringify({ From d187d7c9de8e82d96201c1fa323a4dda9ce4a2b5 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Mon, 18 Nov 2024 13:24:13 +0000 Subject: [PATCH 03/24] fix lint --- forge/db/migrations/20241115-01-add-nr-version-device.js | 2 +- forge/ee/lib/deviceEditor/DeviceTunnelManager.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/forge/db/migrations/20241115-01-add-nr-version-device.js b/forge/db/migrations/20241115-01-add-nr-version-device.js index 60cd2b3d82..bf9c4f0dd4 100644 --- a/forge/db/migrations/20241115-01-add-nr-version-device.js +++ b/forge/db/migrations/20241115-01-add-nr-version-device.js @@ -16,4 +16,4 @@ module.exports = { }, down: async (context) => { } -} \ No newline at end of file +} diff --git a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js index 1c042063b3..8549817ab3 100644 --- a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js +++ b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js @@ -33,9 +33,7 @@ */ const fs = require('node:fs') - class DeviceTunnelManager { - // private members /** @type {Map} */ #tunnels @@ -286,7 +284,7 @@ class DeviceTunnelManager { 'FF-Proxied': 'true' }) reply.send(data) - } else { + } else { const id = tunnel.nextRequestId++ tunnel.requests[id] = reply tunnel.socket.send(JSON.stringify({ From 75ba4a6cc1ed5362ad75743d1f3ed4f2e978c4f0 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 28 Nov 2024 10:19:31 +0000 Subject: [PATCH 04/24] Move cache to sensible location Pervious code had hard coded path on my machine, this moves it to under FLOWFORGE_HOME in `var/device/cache` --- forge/config/index.js | 6 ++++++ forge/ee/lib/deviceEditor/DeviceTunnelManager.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/forge/config/index.js b/forge/config/index.js index 9c7edc1c21..5b710fa44c 100644 --- a/forge/config/index.js +++ b/forge/config/index.js @@ -110,6 +110,12 @@ module.exports = { } } + if (!config.device) { + config.device = { + cache_path: path.join(config.home, '/var/device/cache') + } + } + // need to check that maxIdleDuration is less than maxDuration if (config.sessions) { if (config.sessions.maxIdleDuration && config.sessions.maxDuration) { diff --git a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js index 8549817ab3..622702926f 100644 --- a/forge/ee/lib/deviceEditor/DeviceTunnelManager.js +++ b/forge/ee/lib/deviceEditor/DeviceTunnelManager.js @@ -61,7 +61,7 @@ class DeviceTunnelManager { } }) - this.pathPrefix = '/opt/share/projects/flowforge/test/device-cache' // app.config.device.cache_path + this.pathPrefix = app.config.device.cache_path this.pathPostfix = 'node_modules/@node-red/editor-client/public/' } From e5ccd414f5e1a5da647c0814e2c4f71ed881f751 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 10 Dec 2024 15:09:41 +0000 Subject: [PATCH 05/24] Allow NR Dashboard to be loaded in iFrames part of #4689 paired with https://github.com/FlowFuse/nr-launcher/pull/306 --- forge/lib/templates.js | 3 ++ .../pages/admin/Template/sections/Editor.vue | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/forge/lib/templates.js b/forge/lib/templates.js index be22313abd..930684a775 100644 --- a/forge/lib/templates.js +++ b/forge/lib/templates.js @@ -4,6 +4,7 @@ module.exports = { 'disableTours', 'httpAdminRoot', 'dashboardUI', + 'dashboardIFrame', 'codeEditor', 'theme', 'page_title', @@ -36,6 +37,7 @@ module.exports = { disableTours: false, httpAdminRoot: '', dashboardUI: '/ui', + dashboardIFrame: false, codeEditor: 'monaco', theme: 'forge-light', page_title: 'FlowFuse', @@ -65,6 +67,7 @@ module.exports = { disableTours: true, httpAdminRoot: true, dashboardUI: true, + dashboardIFrame: true, codeEditor: true, theme: true, page_title: false, diff --git a/frontend/src/pages/admin/Template/sections/Editor.vue b/frontend/src/pages/admin/Template/sections/Editor.vue index 9aa92c2262..4e1dd2a350 100644 --- a/frontend/src/pages/admin/Template/sections/Editor.vue +++ b/frontend/src/pages/admin/Template/sections/Editor.vue @@ -66,6 +66,27 @@ +
+
+
+ + Allow Dashboard to be embedded in an iFrame + + + +
+ +
+
+
+
+

Upgrade your stack to be able to enable

+

embedding Dashboards in iFrames

+ Upgrade +
+
@@ -248,6 +269,15 @@ export default { }, debugLimitDisabled () { return !this.editTemplate && !this.editable.policy.debugMaxLength + }, + dashboardIFrameAvailable () { + const launcherVersion = this.instance?.meta?.versions?.launcher + if (!launcherVersion) { + // We won't have this for a suspended project - so err on the side + // of permissive + return true + } + return SemVer.satisfies(SemVer.coerce(launcherVersion), '>=2.12.0') } } } From 78ea58a1401f21fb83b9b313c49eb021ef096e48 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 16 Dec 2024 12:27:55 +0000 Subject: [PATCH 06/24] add KEY_DISABLE_AUTO_SAFE_MODE to project settings --- forge/db/models/Project.js | 5 +++-- forge/db/models/ProjectSettings.js | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index c972312f41..d9b8b1013a 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -17,7 +17,7 @@ const { col, fn, DataTypes, Op, where } = require('sequelize') const Controllers = require('../controllers') -const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME } = require('./ProjectSettings') +const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME, KEY_DISABLE_AUTO_SAFE_MODE } = require('./ProjectSettings') const BANNED_NAME_LIST = [ 'app', @@ -408,7 +408,8 @@ module.exports = { { key: KEY_HA }, { key: KEY_PROTECTED }, { key: KEY_CUSTOM_HOSTNAME }, - { key: KEY_HEALTH_CHECK_INTERVAL } + { key: KEY_HEALTH_CHECK_INTERVAL }, + { key: KEY_DISABLE_AUTO_SAFE_MODE } ] }, required: false diff --git a/forge/db/models/ProjectSettings.js b/forge/db/models/ProjectSettings.js index 628574e766..4f6b50e2de 100644 --- a/forge/db/models/ProjectSettings.js +++ b/forge/db/models/ProjectSettings.js @@ -16,6 +16,7 @@ const KEY_PROTECTED = 'protected' const KEY_HEALTH_CHECK_INTERVAL = 'healthCheckInterval' const KEY_CUSTOM_HOSTNAME = 'customHostname' const KEY_SHARED_ASSETS = 'sharedAssets' +const KEY_DISABLE_AUTO_SAFE_MODE = 'disableAutoSafeMode' module.exports = { KEY_SETTINGS, @@ -25,6 +26,7 @@ module.exports = { KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME, KEY_SHARED_ASSETS, + KEY_DISABLE_AUTO_SAFE_MODE, name: 'ProjectSettings', schema: { ProjectId: { type: DataTypes.UUID, unique: 'pk_settings' }, From 95c56b2c627482786b739cc30f0c7b14b86ca157 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 16 Dec 2024 12:28:30 +0000 Subject: [PATCH 07/24] Update view to permit disableAutoSafeMode to be sent to launcher --- forge/db/views/Project.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/forge/db/views/Project.js b/forge/db/views/Project.js index 3d9033c4d8..2f4c4d28d1 100644 --- a/forge/db/views/Project.js +++ b/forge/db/views/Project.js @@ -1,4 +1,4 @@ -const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME } = require('../models/ProjectSettings') +const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME, KEY_DISABLE_AUTO_SAFE_MODE } = require('../models/ProjectSettings') module.exports = function (app) { app.addSchema({ @@ -37,7 +37,8 @@ module.exports = function (app) { launcherSettings: { type: 'object', properties: { - healthCheckInterval: { type: 'number' } + healthCheckInterval: { type: 'number' }, + disableAutoSafeMode: { type: 'boolean' } }, additionalProperties: false } @@ -75,6 +76,11 @@ module.exports = function (app) { result.launcherSettings = {} result.launcherSettings.healthCheckInterval = heathCheckIntervalRow?.value } + const disableAutoSafeMode = proj.ProjectSettings?.find((projectSettingsRow) => projectSettingsRow.key === KEY_DISABLE_AUTO_SAFE_MODE) + if (typeof disableAutoSafeMode?.value === 'boolean') { + result.launcherSettings = result.launcherSettings || {} + result.launcherSettings.disableAutoSafeMode = disableAutoSafeMode.value + } // Environment result.settings.env = app.db.controllers.Project.insertPlatformSpecificEnvVars(proj, result.settings.env) if (!result.settings.palette?.modules) { From bf885d85af51a1c625ec1da84662faee5454af2b Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 16 Dec 2024 12:28:53 +0000 Subject: [PATCH 08/24] update API to persist disableAutoSafeMode setting --- forge/routes/api/project.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/forge/routes/api/project.js b/forge/routes/api/project.js index 1c635e258c..46bdc75f94 100644 --- a/forge/routes/api/project.js +++ b/forge/routes/api/project.js @@ -1,4 +1,4 @@ -const { KEY_SETTINGS, KEY_HEALTH_CHECK_INTERVAL, KEY_SHARED_ASSETS } = require('../../db/models/ProjectSettings') +const { KEY_SETTINGS, KEY_HEALTH_CHECK_INTERVAL, KEY_DISABLE_AUTO_SAFE_MODE, KEY_SHARED_ASSETS } = require('../../db/models/ProjectSettings') const { Roles } = require('../../lib/roles') const ProjectActions = require('./projectActions') @@ -456,15 +456,24 @@ module.exports = async function (app) { } // Launcher settings - if (request.body?.launcherSettings?.healthCheckInterval) { - const oldInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL) - const newInterval = parseInt(request.body.launcherSettings.healthCheckInterval, 10) - if (isNaN(newInterval) || newInterval < 5000) { - reply.code(400).send({ code: 'invalid_heathCheckInterval', error: 'Invalid heath check interval' }) - return + if (request.body?.launcherSettings) { + if (request.body.launcherSettings.healthCheckInterval) { + const oldInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL) + const newInterval = parseInt(request.body.launcherSettings.healthCheckInterval, 10) + if (isNaN(newInterval) || newInterval < 5000) { + reply.code(400).send({ code: 'invalid_heathCheckInterval', error: 'Invalid heath check interval' }) + return + } + if (oldInterval !== newInterval) { + changesToPersist.healthCheckInterval = { from: oldInterval, to: newInterval } + } } - if (oldInterval !== newInterval) { - changesToPersist.healthCheckInterval = { from: oldInterval, to: newInterval } + if (typeof request.body.launcherSettings.disableAutoSafeMode === 'boolean') { + const oldInterval = await request.project.getSetting(KEY_DISABLE_AUTO_SAFE_MODE) + const newInterval = request.body.launcherSettings.disableAutoSafeMode + if (oldInterval !== newInterval) { + changesToPersist.disableAutoSafeMode = { from: oldInterval, to: newInterval } + } } } @@ -529,6 +538,10 @@ module.exports = async function (app) { await request.project.updateSetting(KEY_HEALTH_CHECK_INTERVAL, changesToPersist.healthCheckInterval.to, { transaction }) updates.pushDifferences({ healthCheckInterval: changesToPersist.healthCheckInterval.from }, { healthCheckInterval: changesToPersist.healthCheckInterval.to }) } + if (changesToPersist.disableAutoSafeMode) { + await request.project.updateSetting(KEY_DISABLE_AUTO_SAFE_MODE, changesToPersist.disableAutoSafeMode.to, { transaction }) + updates.pushDifferences({ disableAutoSafeMode: changesToPersist.disableAutoSafeMode.from }, { disableAutoSafeMode: changesToPersist.disableAutoSafeMode.to }) + } await transaction.commit() // all good, commit the transaction @@ -802,6 +815,7 @@ module.exports = async function (app) { settings.state = request.project.state settings.stack = request.project.ProjectStack?.properties || {} settings.healthCheckInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL) + settings.disableAutoSafeMode = await request.project.getSetting(KEY_DISABLE_AUTO_SAFE_MODE) settings.settings = await app.db.controllers.Project.getRuntimeSettings(request.project) if (settings.settings.env) { settings.env = Object.assign({}, settings.settings.env, settings.env) From a2ea361e986fb1af865e3835b3ef09ffab969309 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 16 Dec 2024 12:29:17 +0000 Subject: [PATCH 09/24] Add UI to display disableAutoSafeMode option under launcher settings --- .../instance/Settings/LauncherSettings.vue | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/instance/Settings/LauncherSettings.vue b/frontend/src/pages/instance/Settings/LauncherSettings.vue index ba3de78328..76d5654657 100644 --- a/frontend/src/pages/instance/Settings/LauncherSettings.vue +++ b/frontend/src/pages/instance/Settings/LauncherSettings.vue @@ -8,6 +8,13 @@ Flows that perform CPU intensive work may need to increase this from the default of 7500ms. + + Disable Auto Safe Mode + +
Save settings @@ -60,7 +67,8 @@ export default { computed: { ...mapState('account', ['team', 'teamMembership']), unsavedChanges: function () { - return this.original.healthCheckInterval !== this.input.healthCheckInterval + return this.original.healthCheckInterval !== this.input.healthCheckInterval || + this.original.disableAutoSafeMode !== this.input.disableAutoSafeMode } }, watch: { @@ -69,6 +77,11 @@ export default { if (this.mounted) { this.validateFormInputs() } + }, + 'input.disableAutoSafeMode': function (value) { + if (this.mounted) { + this.validateFormInputs() + } } }, mounted () { @@ -98,10 +111,13 @@ export default { getSettings: function () { this.original.healthCheckInterval = this.project?.launcherSettings?.healthCheckInterval this.input.healthCheckInterval = this.project?.launcherSettings.healthCheckInterval + this.original.disableAutoSafeMode = this.project?.launcherSettings?.disableAutoSafeMode ?? false + this.input.disableAutoSafeMode = this.project?.launcherSettings.disableAutoSafeMode ?? false }, async saveSettings () { const launcherSettings = { - healthCheckInterval: this.input.healthCheckInterval + healthCheckInterval: this.input.healthCheckInterval, + disableAutoSafeMode: this.input.disableAutoSafeMode ?? false } if (!this.validateFormInputs()) { alerts.emit('Please correct the errors before saving.', 'error') From e049af055f0c5eb6a46bba74d0d1979b582ce496 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 16 Dec 2024 12:30:11 +0000 Subject: [PATCH 10/24] update unit tests to ensure disableAutoSafeMode setting is persisted --- test/unit/forge/routes/api/project_spec.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/unit/forge/routes/api/project_spec.js b/test/unit/forge/routes/api/project_spec.js index 3d9623c841..23bd4b1c54 100644 --- a/test/unit/forge/routes/api/project_spec.js +++ b/test/unit/forge/routes/api/project_spec.js @@ -10,7 +10,7 @@ const setup = require('../setup') const FF_UTIL = require('flowforge-test-utils') const { Roles } = FF_UTIL.require('forge/lib/roles') -const { KEY_HEALTH_CHECK_INTERVAL } = FF_UTIL.require('forge/db/models/ProjectSettings') +const { KEY_HEALTH_CHECK_INTERVAL, KEY_DISABLE_AUTO_SAFE_MODE } = FF_UTIL.require('forge/db/models/ProjectSettings') const { START_DELAY, STOP_DELAY } = FF_UTIL.require('forge/containers/stub/index.js') describe('Project API', function () { @@ -1691,7 +1691,7 @@ describe('Project API', function () { { name: 'two', value: '2' } ]) // should be unchanged }) - it('Change launcher health check interval - owner', async function () { + it('Change launcher settings - owner', async function () { // Setup some flows/credentials await addFlowsToProject(app, TestObjects.project1.id, @@ -1708,15 +1708,18 @@ describe('Project API', function () { url: `/api/v1/projects/${TestObjects.project1.id}`, payload: { launcherSettings: { - healthCheckInterval: 9876 + healthCheckInterval: 9876, + disableAutoSafeMode: true } }, cookies: { sid: TestObjects.tokens.alice } }) response.statusCode.should.equal(200) - const newValue = await TestObjects.project1.getSetting(KEY_HEALTH_CHECK_INTERVAL) - should(newValue).equal(9876) + const healthValue = await TestObjects.project1.getSetting(KEY_HEALTH_CHECK_INTERVAL) + should(healthValue).equal(9876) + const safeModeValue = await TestObjects.project1.getSetting(KEY_DISABLE_AUTO_SAFE_MODE) + should(safeModeValue).equal(true) }) it('Change launcher health check interval bad value - owner', async function () { // Setup some flows/credentials From dc5cec1372888d01efedeba59510799ea9f747ed Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 16 Dec 2024 14:39:49 +0000 Subject: [PATCH 11/24] Add conditional rendering for disableAutoSafeMode based on launcher version support --- .../instance/Settings/LauncherSettings.vue | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/instance/Settings/LauncherSettings.vue b/frontend/src/pages/instance/Settings/LauncherSettings.vue index 76d5654657..5fce2d4bb0 100644 --- a/frontend/src/pages/instance/Settings/LauncherSettings.vue +++ b/frontend/src/pages/instance/Settings/LauncherSettings.vue @@ -8,7 +8,7 @@ Flows that perform CPU intensive work may need to increase this from the default of 7500ms. - + Disable Auto Safe Mode