Skip to content

Commit

Permalink
Merge branch 'main' into run-e2e-tests-locally-without-smtp-server
Browse files Browse the repository at this point in the history
  • Loading branch information
cstns authored Jul 11, 2024
2 parents 42e929c + ec4247a commit d9cd006
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 73 deletions.
7 changes: 7 additions & 0 deletions docs/user/assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ to generate code directly within the editor.
This is useful when you want to quickly add code to an
existing function node without having to generate a full function node from scratch.

### In-line JSON Generation

The FlowFuse Assistant plugin also adds a new code lens to the JSON editor that allows you
to generate JSON directly within the monaco editor.

This is useful when you want to quickly generate JSON in a template node, change node, inject node or
any node that the TypedInput offers the JSON editor.
2 changes: 1 addition & 1 deletion forge/lib/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const Permissions = {
'platform:audit-log': { description: 'View platform audit log', role: Roles.Admin },

// assistant
'assistant:function': { description: 'Access the assistant function endpoint', role: Roles.Member }
'assistant:method': { description: 'Access the assistant method endpoint', role: Roles.Member }
}

module.exports = {
Expand Down
12 changes: 7 additions & 5 deletions forge/routes/api/assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ module.exports = async function (app) {
app.addHook('preHandler', app.verifySession)

/**
* Endpoint for assistant functions
* Endpoint for assistant methods
* For now, this is simply a relay to an external assistant service
* In the future, we may decide to bring that service inside the core or
* use an alternative means of accessing it.
*/
app.post('/function', {
preHandler: app.needsPermission('assistant:function'),
app.post('/:method', {
preHandler: app.needsPermission('assistant:method'),
schema: {
hide: true, // dont show in swagger
body: {
Expand All @@ -45,8 +45,10 @@ module.exports = async function (app) {
}
},
async (request, reply) => {
// const method = request.params.method // FUTURE: allow for different methods
const method = 'function' // for now, only function node/code generation is supported
const method = request.params.method // the method to call at the assistant service
if (/^[a-z0-9_-]+$/.test(method) === false) {
return reply.code(400).send({ code: 'invalid_method', error: 'Invalid method name' })
}

const serviceUrl = app.config.assistant?.service?.url
const serviceToken = app.config.assistant?.service?.token
Expand Down
4 changes: 2 additions & 2 deletions forge/routes/auth/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const IMPLICIT_TOKEN_SCOPES = {
'team:projects:list', // permit a device being edited via a tunnel in developer mode to list projects
'library:entry:create', // permit a device being edited via a tunnel in developer mode to create library entries
'library:entry:list', // permit a device being edited via a tunnel in developer mode to list library entries
'assistant:function' // permit calls to the assistant endpoint for function node/code creation
'assistant:method' // permit calls to the assistant endpoint for method node/code/json/etc creation
],
project: [
'user:read',
Expand All @@ -19,7 +19,7 @@ const IMPLICIT_TOKEN_SCOPES = {
'team:projects:list',
'library:entry:create',
'library:entry:list',
'assistant:function'
'assistant:method'
]
}

Expand Down
47 changes: 40 additions & 7 deletions frontend/src/pages/account/Security/dialogs/MFASetupDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@
<template #default>
<div class="space-y-4">
<template v-if="step === 0">
<p>
To get started, scan the following QR code into your Authenticator app, then click next to continue.
</p>
<div class="text-center mt-4">
<img v-if="!!qrcode" :src="qrcode" class="m-auto border rounded">
</div>
<template v-if="showQRCode">
<p>
To get started, scan the following QR code into your Authenticator app, then click next to continue.
</p>
<div class="text-center mt-4">
<template v-if="!!qrcode">
<img v-if="!!qrcode" :src="qrcode" class="m-auto border rounded">
<p>
<a class="cursor-pointer" @click="showSecret()">Can't scan QR code?</a>
</p>
</template>
</div>
</template>
<template v-else>
<p>
To get started, enter the following code into your Authenticator app, then click next to continue.
</p>
<div class="text-center mt-4">
<p class="text-2xl w-72 text-wrap font-mono tracking-wider mx-auto my-2">
<template v-for="i in secretCode" :key="i">
<span class="mx-1">{{ i }}</span><wbr>
</template>
</p>
<p>
<a class="cursor-pointer" @click="hideSecret()">Show QR code</a>
</p>
</div>
</template>
</template>
<template v-if="step === 1">
<p>
Expand Down Expand Up @@ -57,12 +79,15 @@ export default {
async show () {
this.step = 0
this.qrcode = ''
this.showQRCode = true
this.secretCode = []
this.verifyToken = ''
this.verifyError = ''
this.$refs.dialog.show()
try {
const mfaDetails = await userApi.enableMFA()
this.qrcode = mfaDetails.qrcode
this.secretCode = mfaDetails.url.split('=')[1].match(/.{1,4}/g)
} catch (err) {
}
Expand All @@ -72,8 +97,10 @@ export default {
data () {
return {
step: 0,
showQRCode: true,
qrcode: '',
verifyToken: ''
verifyToken: '',
secretCode: []
}
},
computed: {
Expand All @@ -86,6 +113,12 @@ export default {
close () {
this.$refs.dialog.close()
},
showSecret () {
this.showQRCode = false
},
hideSecret () {
this.showQRCode = true
},
complete () {
this.$emit('user-updated')
this.close()
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/instance/Snapshots/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export default {
showRollbackDialog (snapshot) {
Dialog.show({
header: 'Deploy Snapshot',
kind: 'danger',
text: `This will overwrite the current instance.
All changes to the flows, settings and environment variables made since the last snapshot will be lost.
Are you sure you want to deploy to this snapshot?`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</label>
<div class="items-wrapper" :class="{one: singleDevice, two: twoDevices, three: threeDevices}">
<div
v-for="device in Array.from(application.devices.values())"
v-for="device in devices"
:key="device.id"
class="item-wrapper"
@click.stop="openDevice(device)"
Expand Down Expand Up @@ -91,14 +91,14 @@ export default {
emits: ['delete-device'],
computed: {
hasMoreDevices () {
return this.application.deviceCount > this.application.devices.size
return this.application.deviceCount > this.application.devices.length
},
hasNoDevices () {
return this.application.devices.size === 0
return this.application.devices.length === 0
},
remainingDevices () {
if (this.hasNoDevices || this.hasMoreDevices) {
return this.application.deviceCount - this.application.devices.size
return this.application.deviceCount - this.application.devices.length
} else return 0
},
singleDevice () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ export default {
emits: ['delete-instance'],
computed: {
instances () {
return Array.from(this.application.instances.values())
return this.application.instances
},
hasMoreInstances () {
return this.application.instanceCount > this.application.instances.size
return this.application.instanceCount > this.application.instances.length
},
hasNoInstances () {
return this.application.instances.size === 0
return this.application.instances.length === 0
},
remainingInstances () {
if (this.hasNoInstances || this.hasMoreInstances) {
return this.application.instanceCount - this.application.instances.size
return this.application.instanceCount - this.application.instances.length
} else return 0
},
singleInstance () {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/pages/team/Applications/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ export default {
},
computed: {
applicationsList () {
return Array.from(this.applications.values())
return Array.from(this.applications.values()).map(app => {
return {
...app,
instances: Array.from(app.instances.values()),
devices: Array.from(app.devices.values())
}
})
},
filteredApplications () {
if (this.filterTerm) {
Expand Down
30 changes: 30 additions & 0 deletions test/e2e/frontend/cypress/tests-ee/devices/snapshots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,34 @@ describe('FlowForge - Devices - With Billing', () => {
cy.get('[data-el="snapshots"] tbody').find('tr').contains('uploaded snapshot2')
cy.get('[data-el="snapshots"] tbody').find('tr').contains('snapshot2 description')
})
it('Can rollback a snapshot', () => {
// Premise: Ensure the rollback endpoint is available and callable
// (NOTE: this is not testing the full mechanics of the rollback feature, only to prevent repeat regression. See #2032)
cy.intercept('PUT', '/api/*/devices/*').as('rollbackSnapshot')

cy.intercept('GET', '/api/*/applications/*/snapshots*', deviceSnapshots).as('getSnapshots')
cy.intercept('GET', '/api/*/snapshots/*/full', deviceFullSnapshot).as('fullSnapshot')

cy.contains('span', 'application-device-a').click()
cy.get('[data-nav="device-snapshots"]').click()

// click kebab menu in row 1
cy.get('[data-el="snapshots"] tbody').find('.ff-kebab-menu').eq(0).click()

// click the Rollback Snapshot option
cy.get('[data-el="snapshots"] tbody .ff-kebab-menu .ff-kebab-options').find('.ff-list-item').eq(IDX_DEPLOY_SNAPSHOT).click()

cy.get('[data-el="platform-dialog"]').should('be.visible')
cy.get('[data-el="platform-dialog"] .ff-dialog-header').contains('Deploy Snapshot to device')

// find .ff-btn--danger with text "Confirm" and click it
cy.get('[data-el="platform-dialog"] .ff-btn--danger').contains('Confirm').click()

// check body sent to /api/*/devices/*
cy.wait('@rollbackSnapshot').then(interception => {
const body = interception.request.body
expect(body).to.have.property('targetSnapshot')
expect(body.targetSnapshot).to.be.a('string')
})
})
})
24 changes: 24 additions & 0 deletions test/e2e/frontend/cypress/tests/instances/snapshots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,30 @@ describe('FlowForge - Instance Snapshots', () => {
cy.get('[data-el="snapshots"] tbody').find('tr').contains('uploaded snapshot2')
cy.get('[data-el="snapshots"] tbody').find('tr').contains('snapshot2 description')
})

it('Can rollback a snapshot', () => {
// Premise: Ensure the rollback endpoint is available and callable
// (NOTE: this is not testing the full mechanics of the rollback feature, only to prevent repeat regression. See #2032)
cy.intercept('POST', '/api/*/projects/*/actions/rollback').as('rollbackSnapshot')

// click kebab menu in row 1
cy.get('[data-el="snapshots"] tbody').find('.ff-kebab-menu').eq(0).click()
// click the Rollback Snapshot option
cy.get('[data-el="snapshots"] tbody .ff-kebab-menu .ff-kebab-options').find('.ff-list-item').eq(IDX_DEPLOY_SNAPSHOT).click()

cy.get('[data-el="platform-dialog"]').should('be.visible')
cy.get('[data-el="platform-dialog"] .ff-dialog-header').contains('Deploy Snapshot')

// find .ff-btn--danger with text "Confirm" and click it
cy.get('[data-el="platform-dialog"] .ff-btn--danger').contains('Confirm').click()

// check body sent to /api/*/projects/*/actions/rollback
cy.wait('@rollbackSnapshot').then(interception => {
const body = interception.request.body
expect(body).to.have.property('snapshot')
expect(body.snapshot).to.be.a('string')
})
})
})

describe('FlowForge shows audit logs', () => {
Expand Down
Loading

0 comments on commit d9cd006

Please sign in to comment.