+ This will overwrite the current instance. + All changes to the flows and settings made since the last snapshot will be lost. +
+Are you sure you want to deploy to this snapshot?
+{{ description }}
+{{ description }}
@@ -35,6 +35,11 @@ export default { default: false, type: Boolean }, + // Render line breaks in the description as new lines + multilineDescription: { + default: false, + type: Boolean + }, // Full-width tappable tile layout, with the label stacked above its description tile: { default: false, diff --git a/frontend/src/ui-components/components/form/RadioGroup.vue b/frontend/src/ui-components/components/form/RadioGroup.vue index 43e97951e6..3e9a1dd059 100644 --- a/frontend/src/ui-components/components/form/RadioGroup.vue +++ b/frontend/src/ui-components/components/form/RadioGroup.vue @@ -8,6 +8,7 @@ :description="option.description" :disabled="option.disabled" :hide-description="orientation === 'horizontal'" + :multiline-description="multilineDescription" :tile="tile" @select="select" /> @@ -39,6 +40,11 @@ export default { default: null, type: Array }, + // Render line breaks in the option descriptions as new lines + multilineDescription: { + default: false, + type: Boolean + }, // Full-width tappable tile layout, with each label stacked above its description tile: { default: false, diff --git a/frontend/src/ui-components/stylesheets/ff-core.scss b/frontend/src/ui-components/stylesheets/ff-core.scss index 78df34f1c0..bca9f3ef3f 100644 --- a/frontend/src/ui-components/stylesheets/ff-core.scss +++ b/frontend/src/ui-components/stylesheets/ff-core.scss @@ -71,6 +71,10 @@ p code { .ff-description { font-size: $ff-funit-sm; color: var(--ff-color-text-subtle); + + &.ff-description--multiline { + white-space: pre-line; + } } hr { diff --git a/test/e2e/frontend/cypress/tests/instances/version-history/snapshots.spec.js b/test/e2e/frontend/cypress/tests/instances/version-history/snapshots.spec.js index d1498f050f..341ba72030 100644 --- a/test/e2e/frontend/cypress/tests/instances/version-history/snapshots.spec.js +++ b/test/e2e/frontend/cypress/tests/instances/version-history/snapshots.spec.js @@ -693,6 +693,7 @@ describe('FlowFuse - Instance Snapshots', () => { cy.get('[data-el="platform-dialog"]').should('be.visible') cy.get('[data-el="platform-dialog"] .ff-dialog-header').contains('Restore Snapshot') + cy.get('[data-el="platform-dialog"] [data-form="env-vars-mode"]').should('exist') // find .ff-btn--danger with text "Confirm" and click it cy.get('[data-el="platform-dialog"] .ff-btn--danger').contains('Confirm').click() @@ -702,6 +703,7 @@ describe('FlowFuse - Instance Snapshots', () => { const body = interception.request.body expect(body).to.have.property('snapshot') expect(body.snapshot).to.be.a('string') + expect(body.mergeEnvVars).to.equal(true) }) }) }) diff --git a/test/unit/forge/auditLog/project_spec.js b/test/unit/forge/auditLog/project_spec.js index 7ae472e949..c42d63f5a1 100644 --- a/test/unit/forge/auditLog/project_spec.js +++ b/test/unit/forge/auditLog/project_spec.js @@ -346,6 +346,14 @@ describe('Audit Log > Project', async function () { logEntry.body.snapshot.id.should.equal(SNAPSHOT.hashid) }) + it('Records whether env vars were merged when rolling back a snapshot', async function () { + await projectLogger.project.snapshot.rolledBack(ACTIONED_BY, null, PROJECT, SNAPSHOT, { mergeEnvVars: true }) + const logEntry = await getLog() + logEntry.should.have.property('event', 'project.snapshot.rolled-back') + logEntry.body.should.only.have.keys('project', 'snapshot', 'info') + logEntry.body.info.should.eql({ mergeEnvVars: true }) + }) + it('Provides a logger for deleteing a snapshot of a project', async function () { await projectLogger.project.snapshot.deleted(ACTIONED_BY, null, PROJECT, SNAPSHOT) // check log stored diff --git a/test/unit/forge/routes/api/projectSnapshots_spec.js b/test/unit/forge/routes/api/projectSnapshots_spec.js index c042a01b43..9344f4d6ba 100644 --- a/test/unit/forge/routes/api/projectSnapshots_spec.js +++ b/test/unit/forge/routes/api/projectSnapshots_spec.js @@ -937,6 +937,70 @@ describe('Project Snapshots API', function () { rolledBackSettings.env.should.have.property('two', 'b') }) + it('keeps the instance env vars when rolling back with mergeEnvVars', async function () { + const setEnv = async (env) => { + const result = await addFlowsToProject(app, + TestObjects.project1.id, + TestObjects.tokens.project, + TestObjects.tokens.alice, + [{ id: 'node1' }], + { testCreds: 'abc' }, + 'key1', + { env } + ) + result.updateProjectSettingsResponse.statusCode.should.equal(200) + } + const getEnv = async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/v1/projects/${app.project.id}/settings`, + headers: { authorization: `Bearer ${TestObjects.tokens.project}` } + }) + return response.json().env + } + const rollback = (snapshotId, mergeEnvVars) => app.inject({ + method: 'POST', + url: `/api/v1/projects/${app.project.id}/actions/rollback`, + payload: { snapshot: snapshotId, mergeEnvVars }, + cookies: { sid: TestObjects.tokens.alice } + }) + + await setEnv([ + { name: 'one', value: 'a' }, + { name: 'two', value: 'b' } + ]) + const snapshot = (await createSnapshot(TestObjects.project1.id, 'merge-env-snapshot', TestObjects.tokens.alice)).json() + + // 'one' changed and 'three' added after the snapshot, 'two' removed + await setEnv([ + { name: 'one', value: 'a2' }, + { name: 'three', value: 'c' } + ]) + + const mergeResponse = await rollback(snapshot.id, true) + mergeResponse.statusCode.should.equal(200) + const mergeLog = await app.db.models.AuditLog.findOne({ where: { event: 'project.snapshot.rolled-back', entityId: app.project.id }, order: [['id', 'DESC']] }) + JSON.parse(mergeLog.body).info.should.eql({ mergeEnvVars: true }) + const merged = await getEnv() + merged.should.have.property('one', 'a2') // existing value kept + merged.should.have.property('two', 'b') // added from snapshot + merged.should.have.property('three', 'c') // existing key kept + + await setEnv([ + { name: 'one', value: 'a2' }, + { name: 'three', value: 'c' } + ]) + + const replaceResponse = await rollback(snapshot.id, false) + replaceResponse.statusCode.should.equal(200) + const replaceLog = await app.db.models.AuditLog.findOne({ where: { event: 'project.snapshot.rolled-back', entityId: app.project.id }, order: [['id', 'DESC']] }) + JSON.parse(replaceLog.body).info.should.eql({ mergeEnvVars: false }) + const replaced = await getEnv() + replaced.should.have.property('one', 'a') + replaced.should.have.property('two', 'b') + replaced.should.not.have.property('three') + }) + it('holds a restarting mask through the bounce when rolling back a running instance', async function () { const snapResponse = await createSnapshot(app.project.id, 'rollback-mask-snapshot', TestObjects.tokens.alice) snapResponse.statusCode.should.equal(200)