Skip to content

Commit

Permalink
Merge branch 'add-a-team-link-component-to-simplify-routing' into mov…
Browse files Browse the repository at this point in the history
…e-instance-pages-under-team-namespace
  • Loading branch information
cstns authored Dec 19, 2024
2 parents 4b2e126 + 1622f35 commit 9d241e7
Show file tree
Hide file tree
Showing 26 changed files with 584 additions and 197 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
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
7 changes: 7 additions & 0 deletions forge/db/models/Team.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ module.exports = {
}]
})
},
countForUser: async function (User) {
return M.TeamMember.count({
where: {
UserId: User.id
}
})
},
forUser: async function (User) {
return M.TeamMember.findAll({
where: {
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
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
2 changes: 1 addition & 1 deletion forge/lib/userTeam.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function completeUserSignup (app, user) {
// return
}
// only create a personal team if no other teams exist
if (!((await app.db.models.Team.forUser(user)).length)) {
if ((await app.db.models.Team.countForUser(user)) === 0) {
let teamTypeId = app.settings.get('user:team:auto-create:teamType')

if (!teamTypeId) {
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
73 changes: 69 additions & 4 deletions forge/routes/api/team.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const crypto = require('crypto')

const { Op } = require('sequelize')

const { Roles } = require('../../lib/roles')
Expand Down Expand Up @@ -395,7 +397,8 @@ module.exports = async function (app) {
properties: {
name: { type: 'string' },
type: { type: 'string' },
slug: { type: 'string' }
slug: { type: 'string' },
trial: { type: 'boolean' }
}
},
response: {
Expand Down Expand Up @@ -429,6 +432,34 @@ module.exports = async function (app) {
return
}

let trialMode = false
if (app.license.active() && app.billing && request.body.trial) {
// Check this user is allowed to create a trial team of this type.
// Rules:
// 1. teamType must have trial mode enabled
const teamTrialActive = await teamType.getProperty('trial.active', false)
if (!teamTrialActive) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
// 2. user must have no existing teams
const existingTeamCount = await app.db.models.Team.countForUser(request.session.User)
if (existingTeamCount > 0) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
// 3. user must be < 1 week old
const delta = Date.now() - request.session.User.createdAt.getTime()
if (delta > 1000 * 60 * 60 * 24 * 7) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
trialMode = true
} else if (request.body.trial) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}

let team

try {
Expand All @@ -444,9 +475,43 @@ module.exports = async function (app) {
const teamView = app.db.views.Team.team(team)

if (app.license.active() && app.billing) {
const session = await app.billing.createSubscriptionSession(team, request.session.User)
app.auditLog.Team.billing.session.created(request.session.User, null, team, session)
teamView.billingURL = session.url
if (trialMode) {
await app.billing.setupTrialTeamSubscription(team, request.session.User)
// In trial mode, we may also auto-create their first application and instance
if (app.settings.get('user:team:auto-create:instanceType')) {
const instanceTypeId = app.settings.get('user:team:auto-create:instanceType')
const instanceType = await app.db.models.ProjectType.byId(instanceTypeId)
const instanceStack = await instanceType?.getDefaultStack() || (await instanceType.getProjectStacks())?.[0]
const instanceTemplate = await app.db.models.ProjectTemplate.findOne({ where: { active: true } })
if (!instanceType) {
app.log.warn(`Unable to create Trial Instance in team ${team.hashid}: Instance type with id ${instanceTypeId} from 'user:team:auto-create:instanceType' not found`)
} else if (!instanceStack) {
app.log.warn(`Unable to create Trial Instance in team ${team.hashid}: Unable to find a stack for use with instance type ${instanceTypeId}`)
} else if (!instanceTemplate) {
app.log.warn(`Unable to create Trial Instance in team ${team.hashid}: Unable to find the default instance template`)
} else {
const applicationName = `${request.session.User.name}'s Application`
const application = await app.db.models.Application.create({
name: applicationName.charAt(0).toUpperCase() + applicationName.slice(1),
TeamId: team.id
})
await app.auditLog.Team.application.created(request.session.User, null, team, application)
await app.auditLog.Application.application.created(request.session.User, null, application)

const safeTeamName = team.name.toLowerCase().replace(/[\W_]/g, '-')
const safeUserName = request.session.User.username.toLowerCase().replace(/[\W_]/g, '-')

const instanceProperties = {
name: `${safeTeamName}-${safeUserName}-${crypto.randomBytes(4).toString('hex')}`
}
await app.db.controllers.Project.create(team, application, request.session.User, instanceType, instanceStack, instanceTemplate, instanceProperties)
}
}
} else {
const session = await app.billing.createSubscriptionSession(team, request.session.User)
app.auditLog.Team.billing.session.created(request.session.User, null, team, session)
teamView.billingURL = session.url
}
}

reply.send(teamView)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<router-link :to="homeLink">
<img class="ff-logo" src="/ff-logo--wordmark--dark.png">
</router-link>
<global-search v-if="hasAMinimumTeamRoleOf(Roles.Viewer)" />
<global-search v-if="teams.length > 0 && hasAMinimumTeamRoleOf(Roles.Viewer)" />
<!-- Mobile: Toggle(User Options) -->
<div class="flex ff-mobile-navigation-right" data-el="mobile-nav-right">
<NotificationsButton class="ff-header--mobile-notificationstoggle" :class="{'active': mobileTeamSelectionOpen}" />
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/TeamTypeSelection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div>
<div class="flex gap-6 justify-center relative z-10 flex-wrap">
<team-type-tile v-for="type in types" :key="type.id" :team-type="type" />
</div>
</div>
</template>

<script>
import { mapState } from 'vuex'
import teamTypesApi from '../api/teamTypes.js'
import TeamTypeTile from './TeamTypeTile.vue'
export default {
name: 'TeamTypeSelection',
components: {
'team-type-tile': TeamTypeTile
},
data () {
return {
types: []
}
},
computed: {
...mapState('account', ['user'])
},
async created () {
const { types } = await teamTypesApi.getTeamTypes()
this.types = types.sort((a, b) => a.order - b.order)
}
}
</script>
Loading

0 comments on commit 9d241e7

Please sign in to comment.