Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement health check interval user setting #3716

Merged
merged 26 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e9d1299
introduce new key setting KEY_HEALTH_CHECK_INTERVAL
Steve-Mcl Apr 15, 2024
7efe08c
update API for launcher settings (KEY_HEALTH_CHECK_INTERVAL)
Steve-Mcl Apr 15, 2024
a7e2585
add launcher settings page to instance settings.
Steve-Mcl Apr 15, 2024
c56437a
fix existing test - remove brittle length check
Steve-Mcl Apr 15, 2024
7c11b32
add e2e test
Steve-Mcl Apr 15, 2024
71a38e8
add unit tests
Steve-Mcl Apr 15, 2024
89db3d2
Update frontend/src/pages/instance/Settings/LauncherSettings.vue
Steve-Mcl Apr 18, 2024
b97a870
Update forge/routes/api/project.js
Steve-Mcl Apr 18, 2024
00df3d9
Update test/unit/forge/routes/api/project_spec.js
Steve-Mcl Apr 18, 2024
9412788
alert for invalid value
Steve-Mcl Apr 19, 2024
8378317
add client side validation on health check interval
Steve-Mcl Apr 19, 2024
9311dbf
fix existing test
Steve-Mcl Apr 19, 2024
81588d2
add e2e test for client side validation
Steve-Mcl Apr 19, 2024
c842486
Merge branch 'main' into 3715-health-check-interval
Steve-Mcl Apr 19, 2024
d27f625
align path name with other routes
Steve-Mcl Apr 19, 2024
4479eb4
Merge branch 'main' into 3715-health-check-interval
joepavitt May 1, 2024
214d704
Merge branch 'main' into 3715-health-check-interval
Steve-Mcl May 2, 2024
3c5e027
add healthCheckInterval to /settings
Steve-Mcl May 2, 2024
2c38934
Merge branch 'main' into 3715-health-check-interval
Steve-Mcl May 2, 2024
3bb90c0
Merge branch 'main' into 3715-health-check-interval
knolleary May 7, 2024
14872fa
Merge branch 'main' into 3715-health-check-interval
Steve-Mcl May 7, 2024
104a8ce
Merge branch 'main' into 3715-health-check-interval
Steve-Mcl May 7, 2024
4ae8a01
Merge branch 'main' into 3715-health-check-interval
hardillb May 7, 2024
1155281
Merge branch 'main' into 3715-health-check-interval
Steve-Mcl May 7, 2024
86ae936
Merge branch 'main' into 3715-health-check-interval
knolleary May 8, 2024
629776b
Merge branch 'main' into 3715-health-check-interval
knolleary May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { DataTypes, Op } = require('sequelize')

const Controllers = require('../controllers')

const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED } = require('./ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL } = require('./ProjectSettings')

const BANNED_NAME_LIST = [
'www',
Expand Down Expand Up @@ -358,7 +358,8 @@ module.exports = {
{ key: KEY_SETTINGS },
{ key: KEY_HOSTNAME },
{ key: KEY_HA },
{ key: KEY_PROTECTED }
{ key: KEY_PROTECTED },
{ key: KEY_HEALTH_CHECK_INTERVAL }
]
},
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 @@ -13,12 +13,14 @@ const KEY_SETTINGS = 'settings'
const KEY_HOSTNAME = 'hostname'
const KEY_HA = 'ha'
const KEY_PROTECTED = 'protected'
const KEY_HEALTH_CHECK_INTERVAL = 'healthCheckInterval'

module.exports = {
KEY_SETTINGS,
KEY_HOSTNAME,
KEY_HA,
KEY_PROTECTED,
KEY_HEALTH_CHECK_INTERVAL,
name: 'ProjectSettings',
schema: {
ProjectId: { type: DataTypes.UUID, unique: 'pk_settings' },
Expand Down
16 changes: 15 additions & 1 deletion 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 } = require('../models/ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL } = require('../models/ProjectSettings')

module.exports = function (app) {
app.addSchema({
Expand Down Expand Up @@ -32,6 +32,13 @@ module.exports = function (app) {
protected: {
type: 'object',
additionalProperties: true
},
launcherSettings: {
type: 'object',
properties: {
healthCheckInterval: { type: 'number' }
},
additionalProperties: false
}
}
})
Expand Down Expand Up @@ -61,6 +68,13 @@ module.exports = function (app) {
} else {
result.settings = {}
}
// Launcher Settings
const heathCheckIntervalRow = proj.ProjectSettings?.find((projectSettingsRow) => projectSettingsRow.key === KEY_HEALTH_CHECK_INTERVAL)
if (heathCheckIntervalRow) {
result.launcherSettings = {}
result.launcherSettings.healthCheckInterval = heathCheckIntervalRow?.value
}
// Environment
result.settings.env = app.db.controllers.Project.insertPlatformSpecificEnvVars(proj, result.settings.env)
if (!result.settings.palette?.modules) {
// If there are no modules listed in settings, check the StorageSettings
Expand Down
24 changes: 23 additions & 1 deletion forge/routes/api/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KEY_HOSTNAME, KEY_SETTINGS } = require('../../db/models/ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HEALTH_CHECK_INTERVAL } = require('../../db/models/ProjectSettings')
const { Roles } = require('../../lib/roles')

const { isFQDN } = require('../../lib/validate')
Expand Down Expand Up @@ -312,6 +312,7 @@ module.exports = async function (app) {
name: { type: 'string' },
hostname: { type: 'string' },
settings: { type: 'object' },
launcherSettings: { type: 'object' },
projectType: { type: 'string' },
stack: { type: 'string' },
sourceProject: {
Expand Down Expand Up @@ -478,6 +479,19 @@ module.exports = async function (app) {
changesToPersist.stack = { from: request.project.stack, to: stack }
}

// 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' })
Steve-Mcl marked this conversation as resolved.
Show resolved Hide resolved
return
}
if (oldInterval !== newInterval) {
changesToPersist.healthCheckInterval = { from: oldInterval, to: newInterval }
}
}

/// Persist the changes
const updates = new app.auditLog.formatters.UpdatesCollection()
const transaction = await app.db.sequelize.transaction() // start a transaction
Expand Down Expand Up @@ -541,6 +555,11 @@ module.exports = async function (app) {
}
}

if (changesToPersist.healthCheckInterval) {
await request.project.updateSetting(KEY_HEALTH_CHECK_INTERVAL, changesToPersist.healthCheckInterval.to, { transaction })
updates.pushDifferences({ healthCheckInterval: changesToPersist.healthCheckInterval.from }, { healthCheckInterval: changesToPersist.healthCheckInterval.to })
}

await transaction.commit() // all good, commit the transaction

// Log the updates
Expand Down Expand Up @@ -795,7 +814,9 @@ module.exports = async function (app) {
reply.code(400).send({ code: 'project_suspended', error: 'Project suspended' })
return
}
// get settings from the driver
const settings = await app.containers.settings(request.project)
// add instance settings
settings.env = settings.env || {}
settings.baseURL = request.project.url
settings.forgeURL = app.config.base_url
Expand All @@ -805,6 +826,7 @@ module.exports = async function (app) {
settings.auditURL = request.project.auditURL
settings.state = request.project.state
settings.stack = request.project.ProjectStack?.properties || {}
settings.healthCheckInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL)
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
116 changes: 116 additions & 0 deletions frontend/src/pages/instance/Settings/LauncherSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<FormHeading class="mb-6">Launcher Settings</FormHeading>
<form class="space-y-6" data-el="launcher-settings-form">
<FormRow v-model="input.healthCheckInterval" type="number" :error="errors.healthCheckInterval">
Health check interval (ms)
<template #description>
The interval at which the launcher will check the health of Node-RED.
Flows that perform CPU intensive work may need to increase this from the default of 7500ms.
</template>
</FormRow>

<div class="space-x-4 whitespace-nowrap">
<ff-button size="small" :disabled="!unsavedChanges || !validateFormInputs()" data-action="save-settings" @click="saveSettings()">Save settings</ff-button>
</div>
</form>
</template>

<script>

import { useRouter } from 'vue-router'

import { mapState } from 'vuex'

import InstanceApi from '../../../api/instances.js'
import FormHeading from '../../../components/FormHeading.vue'
import FormRow from '../../../components/FormRow.vue'
import permissionsMixin from '../../../mixins/Permissions.js'
import alerts from '../../../services/alerts.js'

export default {
name: 'LauncherSettings',
components: {
FormRow,
FormHeading
},
mixins: [permissionsMixin],
inheritAttrs: false,
props: {
project: {
type: Object,
required: true
}
},
emits: ['instance-updated'],
data () {
return {
mounted: false,
original: {
healthCheckInterval: null
},
input: {
healthCheckInterval: null
},
errors: {
healthCheckInterval: ''
}

}
},
computed: {
...mapState('account', ['team', 'teamMembership']),
unsavedChanges: function () {
return this.original.healthCheckInterval !== this.input.healthCheckInterval
}
},
watch: {
project: 'getSettings',
'input.healthCheckInterval': function (value) {
if (this.mounted) {
this.validateFormInputs()
}
}
},
mounted () {
this.checkAccess()
this.getSettings()
this.mounted = true
},
methods: {
checkAccess: function () {
if (!this.hasPermission('project:edit')) {
useRouter().push({ replace: true, path: 'general' })
}
},
validateFormInputs () {
if (!this.unsavedChanges) {
this.errors.healthCheckInterval = ''
} else {
const hci = parseInt(this.input.healthCheckInterval)
if (isNaN(hci) || hci < 5000) {
this.errors.healthCheckInterval = 'Health check interval must be 5000 or greater'
} else {
this.errors.healthCheckInterval = ''
}
}
return !this.errors.healthCheckInterval
},
getSettings: function () {
this.original.healthCheckInterval = this.project?.launcherSettings?.healthCheckInterval
this.input.healthCheckInterval = this.project?.launcherSettings.healthCheckInterval
},
async saveSettings () {
const launcherSettings = {
healthCheckInterval: this.input.healthCheckInterval
}
if (!this.validateFormInputs()) {
alerts.emit('Please correct the errors before saving.', 'error')
return
}
await InstanceApi.updateInstance(this.project.id, { launcherSettings })
this.$emit('instance-updated')
alerts.emit('Instance successfully updated. Restart the instance to apply the changes.', 'confirmation')
}
}
}
</script>
1 change: 1 addition & 0 deletions frontend/src/pages/instance/Settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default {
this.sideNavigation.push({ name: 'Editor', path: './editor' })
this.sideNavigation.push({ name: 'Security', path: './security' })
this.sideNavigation.push({ name: 'Palette', path: './palette' })
this.sideNavigation.push({ name: 'Launcher', path: './launcher' })
if (this.features.emailAlerts && this.team.type.properties.features?.emailAlerts) {
this.sideNavigation.push({ name: 'Alerts', path: './alerts' })
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/instance/Settings/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import InstanceSettingsEditor from './Editor.vue'
import InstanceSettingsEnvVar from './Environment.vue'
import InstanceSettingsGeneral from './General.vue'
import InstanceSettingsHA from './HighAvailability.vue'
import InstanceSettingsLauncher from './LauncherSettings.vue'
import InstanceSettingsPalette from './Palette.vue'
import InstanceSettingsProtect from './ProtectInstance.vue'
import InstanceSettingsSecurity from './Security.vue'
Expand All @@ -26,5 +27,6 @@ export default [
title: 'Instance - Change Type'
}
},
{ path: 'launcher', name: 'instance-settings-launcher', component: InstanceSettingsLauncher },
{ path: 'alerts', name: 'instance-settings-alerts', component: InstanceSettingsAlerts }
]
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ describe('FlowFuse EE - Instance - Alerts', () => {
navigateToInstanceSettings('BTeam', 'instance-2-1')

// check Alerts in list and click
cy.get('[data-el="section-side-menu"] li').should('have.length', 8)
cy.get('[data-el="section-side-menu"] li:last a').contains('Alerts')
cy.get('[data-el="section-side-menu"] li:last').click()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/// <reference types="Cypress" />
describe('FlowFuse - Instance - Settings - Launcher', () => {
function navigateToInstanceSettings (teamName, projectName) {
cy.request('GET', '/api/v1/user/teams')
.then((response) => {
const team = response.body.teams.find(
(team) => team.name === teamName
)
return cy.request('GET', `/api/v1/teams/${team.id}/projects`)
})
.then((response) => {
const project = response.body.projects.find(
(project) => project.name === projectName
)
cy.visit(`/instance/${project.id}/settings/general`)
cy.wait('@getInstance')
})
}

function getForm () {
return cy.get('[data-el="launcher-settings-form"]')
}

beforeEach(() => {
cy.intercept('GET', '/api/*/projects/').as('getProjects')
cy.intercept('GET', '/api/*/projects/*').as('getInstance')
cy.login('bob', 'bbPassword')
cy.home()
})

it('Validates health check interval user input', () => {
cy.intercept('PUT', '/api/*/projects/*').as('updateInstance')
// navigate to instance settings -> launcher tab
cy.login('bob', 'bbPassword')
cy.home()
navigateToInstanceSettings('BTeam', 'instance-2-1')

cy.get('[data-el="section-side-menu"] li').contains('Launcher').click()

// wait for url /instance/***/settings/launcher
cy.url().should('include', 'settings/launcher')

// Change value to < 5000
getForm().first('div').get('.ff-input > input[type=number]').clear()
getForm().first('div').get('.ff-input > input[type=number]').type(4999)
cy.get('[data-action="save-settings"]').should('be.disabled')
getForm().first('div').get('[data-el="form-row-error"').contains('Health check interval must be 5000 or greater').should('exist')

// Change value to > 5000
getForm().first('div').get('.ff-input > input[type=number]').clear()
getForm().first('div').get('.ff-input > input[type=number]').type(5001)
cy.get('[data-action="save-settings"]').should('not.be.disabled')
getForm().first('div').get('[data-el="form-row-error"').should('not.exist')
})

it('Can set health check interval value', () => {
cy.intercept('PUT', '/api/*/projects/*').as('updateInstance')

navigateToInstanceSettings('BTeam', 'instance-2-1')

// locate and click on the launcher tab
cy.get('[data-el="section-side-menu"] li').contains('Launcher').click()

// wait for url /instance/***/settings/launcher
cy.url().should('include', 'settings/launcher')

// // ensure the first child's title is correct
getForm().should('exist')
getForm().first('div').should('exist')
getForm().first('div').get('[data-el="form-row-title"]').contains('Health check interval (ms)').should('exist')
// ensure the first child's numeric input exists
getForm().first('div').get('.ff-input > input[type=number]').should('exist')

// Change value & save
const randomBetween6789and9876 = Math.floor(Math.random() * (9876 - 6789 + 1)) + 6789
getForm().first('div').get('.ff-input > input[type=number]').clear()
getForm().first('div').get('.ff-input > input[type=number]').type(randomBetween6789and9876)
cy.get('[data-action="save-settings"]').click()
cy.wait('@updateInstance')

// refresh page
navigateToInstanceSettings('BTeam', 'instance-2-1')
cy.get('[data-el="section-side-menu"] li').contains('Launcher').click()

// check value is restored
cy.get('[data-el="launcher-settings-form"]').first().get('.ff-input > input[type=number]').should('have.value', randomBetween6789and9876)
})
})
Loading
Loading