Skip to content

Commit

Permalink
Merge branch 'main' into allow-team-trial-creation
Browse files Browse the repository at this point in the history
  • Loading branch information
knolleary authored Dec 19, 2024
2 parents 6f14d0d + 449189a commit 420c7ae
Show file tree
Hide file tree
Showing 19 changed files with 326 additions and 126 deletions.
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,69 @@
#### 2.12.0: Release

- Add note about Private CA chain (#4901)
- Bump actions/github-script from 6 to 7 (#4897)
- Bump flowfuse/github-actions-workflows from 0.37.0 to 0.38.0 (#4896)
- Make it clearer which IP address to use (#4887)
- Bump codecov/codecov-action from 4 to 5 (#4795)
- Support disabling instance launcher "auto safe mode" (#4922) @Steve-Mcl
- Allow NR Dashboard to be loaded in iFrames (#4900) @hardillb
- Add system-ui as a backup font for heebo (to match internal font) (#4946) @cstns
- Remove platform banners from the applications page (#4939) @cstns
- Better device proxy cache (#4792) @hardillb
- Fix application child routes not making the applications nav menu active (#4885) @cstns
- Decrease device auto timeout to 15 seconds from 30 (#4932) @hardillb
- Add logo version for dark backgrounds (#4930) @Yndira-E
- Open Dashboard and Editor links in new tab by default (#4923) @joepavitt
- Update the sign up page and box layout to new branding (#4924) @joepavitt
- Bump nanoid from 3.3.7 to 3.3.8 (#4918) @app/dependabot
- Add note to Instance Types setting default Stack (#4917) @hardillb
- Team Bill Of Materials UI (#4872) @cstns
- Remove notifications for deleted instances (#4899) @hardillb
- Revert Device log changes (#4916) @hardillb
- Allow for prefix/suffix to SSO GroupNames (#4902) @hardillb
- Add device agent docker timezone docs (#4907) @hardillb
- Ensure Device Provisioning tokens removed with Team (#4906) @hardillb
- Return device type in application/devices (#4904) @hardillb
- Fix device log race condition between publish and disconnect (#4903) @cstns
- Ensure device logs always shown (#4893) @hardillb
- Add some Team Broker developement docs (#4799) @hardillb
- Ensure Instance suspended on expired license (#4888) @hardillb
- Bump cypress from 13.13.1 to 13.16.1 (#4895) @app/dependabot
- ci: Fix prestaging slack notification conditional (#4892) @ppawlowski
- ci: "upstream" packages validation workflow (#4455) @ppawlowski
- docs: Change links to Docker Compose files (#4890) @ppawlowski
- Fix main nav matching context order (#4869) @cstns
- Use default behavior for platform wide anchors (part I) (#4834) @cstns
- Bump path-to-regexp and express (#4879) @app/dependabot
- Fix padding on Device Group Settings view (#4865) @knolleary
- docs: Add description how to start Device Agent on system boot (#4878) @ppawlowski
- Send invite Reminders (#4824) @hardillb
- Fixe the outline of the first search result title (#4877) @cstns
- Add more filters for admin notification targeting (#4843) @knolleary
- Topic hierarchy follow up (#4818) @cstns
- Update role-based permissions table (#4863) @sumitshinde-84
- Add Team BOM api endpoint (#4849) @hardillb
- Ensure existing http auth tokens shown (#4861) @hardillb
- fix hovering over pipeline and application name and update empty state message (#4859) @cstns
- Expand the UNS Hierarchy by default & improve hover behaviour (#4854) @joepavitt
- Fix access permission for team pipeline api (#4856) @hardillb
- Improve help text and empty state language for Teams > Pipelines (#4855) @joepavitt
- Navigation - Add Team Pipelines View (#4852) @cstns
- Prevent viewer role users from getting 404 when accesing applications (#4846) @cstns
- Team Pipelines API (#4847) @hardillb
- ci: Publish to npm only on successful tests (#4848) @ppawlowski
- Team member device mode toggle (#4844) @hardillb
- Improve padding/sizing of the global search box (#4825) @joepavitt
- Bump @sentry/browser and @sentry/vue (#4731) @app/dependabot
- Allow branding settings to be cleared in the UI (#4841) @knolleary
- ci: Test docs along with website (#4840) @ppawlowski
- Improved Admin Team view (#4770) @knolleary
- 4563 replace instance and audit logs dropdowns (#4567) @cstns
- Support Search by id in Global Search (#4814) @Steve-Mcl
- fix device groups layout (#4817) @Steve-Mcl
- docs: fix failing anchors on kubernetes and docker docs (#4812) @ppawlowski
- Fix broken anchor links in docs (#4811) @Steve-Mcl

#### 2.11.0: Release

- Bump flowfuse/github-actions-workflows from 0.36.0 to 0.37.0 (#4733)
Expand Down
6 changes: 6 additions & 0 deletions forge/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions forge/db/controllers/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions forge/db/migrations/20241218-01-add-nr-version-device.js
Original file line number Diff line number Diff line change
@@ -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) => {
}
}
1 change: 1 addition & 0 deletions forge/db/models/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' },
Expand Down
5 changes: 3 additions & 2 deletions forge/db/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions forge/db/models/ProjectSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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' },
Expand Down
10 changes: 8 additions & 2 deletions forge/db/views/Project.js
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -37,7 +37,8 @@ module.exports = function (app) {
launcherSettings: {
type: 'object',
properties: {
healthCheckInterval: { type: 'number' }
healthCheckInterval: { type: 'number' },
disableAutoSafeMode: { type: 'boolean' }
},
additionalProperties: false
}
Expand Down Expand Up @@ -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) {
Expand Down
47 changes: 47 additions & 0 deletions forge/ee/lib/deviceEditor/DeviceTunnelManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@
* @typedef {(connection: WebSocket, request: FastifyRequest) => void} wsHandler
*/

const fs = require('node:fs')
const path = require('node:path')
const localCacheFiles = [
{ path: '/vendor/monaco/dist/editor.js', type: 'application/json; charset=UTF-8' }, // ~4.1MB
{ path: '/vendor/monaco/dist/ts.worker.js', type: 'application/json; charset=UTF-8' }, // ~4.7MB
{ path: '/vendor/monaco/dist/css.worker.js', type: 'application/json; charset=UTF-8' }, // ~1.1MB
{ path: '/vendor/vendor.js', type: 'application/json; charset=UTF-8' }, // ~1.1MB
{ path: '/vendor/mermaid/mermaid.min.js', type: 'application/json; charset=UTF-8' }, // ~2.5MB
{ path: '/red/red.min.js', type: 'application/json; charset=UTF-8' },
{ path: '/red/style.min.css', type: 'text/css; charset=UTF-8' }
]

class DeviceTunnelManager {
// private members
/** @type {Map<String, DeviceTunnel>} */ #tunnels
Expand Down Expand Up @@ -59,6 +71,9 @@ class DeviceTunnelManager {
this.closeTunnel(deviceId)
}
})

this.pathPrefix = app.config.device?.cache_path
this.pathPostfix = 'node_modules/@node-red/editor-client/public/'
}

/**
Expand Down Expand Up @@ -159,6 +174,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())
Expand Down Expand Up @@ -236,6 +253,36 @@ class DeviceTunnelManager {

/** @type {httpHandler} */
tunnel._handleHTTPGet = (request, reply) => {
const url = request.url.substring(`/api/v1/devices/${tunnel.deviceId}/editor/proxy`.length)
try {
// check this is a cached item before hitting the file system.
// if the file is foound for this version of node-red, serve it from
// the file system, otherwise, fall through to the device tunnel logic
const cacheEntry = localCacheFiles.find(f => url.startsWith(f.path))
if (tunnel.nodeRedVersion && cacheEntry) {
const cachParentDir = path.join(manager.pathPrefix, tunnel.nodeRedVersion)
const cacheDirExists = fs.existsSync(cachParentDir)
if (cacheDirExists) {
const filePath = path.join(cachParentDir, manager.pathPostfix, cacheEntry.path)
const fileExists = fs.existsSync(filePath)
if (fileExists) {
console.info(`Serving cached file: ${filePath}`) // usefull for debugging
const data = fs.readFileSync(filePath)
reply.headers({
'Content-Type': cacheEntry.type,
'Cache-Control': 'public, max-age=0',
'FF-Proxied': 'true'
})
reply.send(data)
return
}
}
}
} catch (_error) {
console.error('Error serving cached file', _error)
// Ignore errors, drop through to regular logic
}
// non cached requests are forwarded to the device
const id = tunnel.nextRequestId++
tunnel.requests[id] = reply
tunnel.socket.send(JSON.stringify({
Expand Down
3 changes: 3 additions & 0 deletions forge/lib/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
'disableTours',
'httpAdminRoot',
'dashboardUI',
'dashboardIFrame',
'codeEditor',
'theme',
'page_title',
Expand Down Expand Up @@ -36,6 +37,7 @@ module.exports = {
disableTours: false,
httpAdminRoot: '',
dashboardUI: '/ui',
dashboardIFrame: false,
codeEditor: 'monaco',
theme: 'forge-light',
page_title: 'FlowFuse',
Expand Down Expand Up @@ -65,6 +67,7 @@ module.exports = {
disableTours: true,
httpAdminRoot: true,
dashboardUI: true,
dashboardIFrame: true,
codeEditor: true,
theme: true,
page_title: false,
Expand Down
32 changes: 23 additions & 9 deletions forge/routes/api/project.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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 }
}
}
}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/pages/admin/Template/sections/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@
</div>
<LockSetting class="flex justify-end flex-col" tooltip="This setting is fixed and cannot be changed." />
</div>
<div v-if="dashboardIFrameAvailable">
<div class="flex flex-col sm:flex-row">
<div class="w-full max-w-md sm:mr-8">
<FormRow v-model="editable.settings.dashboardIFrame" :error="editable.errors.dashboardIFrame" :disabled="!editTemplate && !editable.policy.dashboardIFrame" type="checkbox">
Allow Dashboard to be embedded in an iFrame
<template #description>
Sets the <span>Content-Security-Policy: frame-ancestor '*'</span> HTTP Header for the Dashboard
</template>
<template #append><ChangeIndicator :value="editable.changed.settings.dashboardIFrame" /></template>
</FormRow>
</div>
<LockSetting v-model="editable.policy.dashboardIFrame" class="flex justify-end flex-col" :editTemplate="editTemplate" :changed="editable.changed.policy.dashboardIFrame" />
</div>
</div>
<div v-else class="flex flex-col sm:flex-row">
<div class="space-y-4 w-full max-w-md sm:mr-8">
<p>Upgrade your stack to be able to enable</p>
<p>embedding Dashboards in iFrames</p>
<ff-button size="small" to="general">Upgrade</ff-button>
</div>
</div>
<div class="flex flex-col sm:flex-row">
<div class="w-full max-w-md sm:mr-8">
<FormRow v-model="editable.settings.codeEditor" :disabled="!editTemplate && !editable.policy.codeEditor" type="select" :options="[{label:'monaco', value:'monaco'},{label:'ace', value:'ace'}]">
Expand Down Expand Up @@ -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')
}
}
}
Expand Down
Loading

0 comments on commit 420c7ae

Please sign in to comment.