Skip to content

Commit 881fbea

Browse files
authored
Merge pull request #4282 from FlowFuse/3818-rename-snapshot
Edit snapshot
2 parents d9dda8b + 6cc940c commit 881fbea

22 files changed

Lines changed: 618 additions & 31 deletions

File tree

docs/user/envvar.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,7 @@ In addition, the following variables are set when running on a device:
6262

6363
When deploying the same set of flows out to multiple devices, these variables can
6464
be used by the flows to identify the specific device being run on.
65+
66+
NOTE: `FF_SNAPSHOT_NAME` will not be immediately updated when the current snapshot is edited.
67+
It will only be updated when the snapshot is changed or a setting that causes the device to
68+
be restarted is changed.

docs/user/snapshots.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ To create a snapshot:
2424

2525
The list of snapshots will update with the newly created entry at the top.
2626

27+
## Edit a snapshot
28+
29+
To edit a snapshot:
30+
31+
1. Go to the instance's page and select the **Snapshots** tab.
32+
2. Open the dropdown menu to the right of the snapshot you want to edit and
33+
select the **Edit Snapshot** option.
34+
3. Update the name and description as required.
35+
4. Click **Update**
36+
37+
NOTE:
38+
Changes made to a snapshot will not be immediately reflected on devices that are already running this snapshot.
39+
40+
2741
## Download a snapshot
2842

2943
A snapshot can be downloaded to your local machine for backup or sharing.

forge/auditLog/application.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ module.exports = {
2626
async created (actionedBy, error, application, device, snapshot) {
2727
await log('application.device.snapshot.created', actionedBy, application?.id, generateBody({ error, device, snapshot }))
2828
},
29+
async updated (actionedBy, error, application, device, snapshot, updates) {
30+
await log('application.device.snapshot.updated', actionedBy, application?.id, generateBody({ error, device, snapshot, updates }))
31+
},
2932
async deleted (actionedBy, error, application, device, snapshot) {
3033
await log('application.device.snapshot.deleted', actionedBy, application?.id, generateBody({ error, device, snapshot }))
3134
},

forge/auditLog/project.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ module.exports = {
7878
async created (actionedBy, error, project, snapshot) {
7979
await log('project.snapshot.created', actionedBy, project?.id, generateBody({ error, project, snapshot }))
8080
},
81+
async updated (actionedBy, error, project, snapshot, updates) {
82+
await log('project.snapshot.updated', actionedBy, project?.id, generateBody({ error, project, snapshot, updates }))
83+
},
8184
async rolledBack (actionedBy, error, project, snapshot) {
8285
await log('project.snapshot.rolled-back', actionedBy, project?.id, generateBody({ error, project, snapshot }))
8386
},

forge/db/controllers/Snapshot.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
const { ValidationError } = require('sequelize')
2+
const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
3+
14
module.exports = {
25
/**
36
* Get a snapshot by ID
@@ -108,6 +111,31 @@ module.exports = {
108111
return true
109112
},
110113

114+
/**
115+
* Update a snapshot
116+
* @param {*} app - app instance
117+
* @param {*} snapshot - snapshot object
118+
* @param {*} options - options to update
119+
* @param {String} [options.name] - name of the snapshot
120+
* @param {String} [options.description] - description of the snapshot
121+
*/
122+
async updateSnapshot (app, snapshot, options) {
123+
const updates = {}
124+
if (hasProperty(options, 'name') && (typeof options.name !== 'string' || options.name.trim() === '')) {
125+
throw new ValidationError('Snapshot name is required')
126+
}
127+
if (options.name) {
128+
updates.name = options.name
129+
}
130+
if (typeof options.description !== 'undefined') {
131+
updates.description = options.description
132+
}
133+
if (Object.keys(updates).length > 0) {
134+
await snapshot.update(updates)
135+
}
136+
return snapshot
137+
},
138+
111139
/**
112140
* Upload a snapshot.
113141
* @param {*} app - app instance

forge/lib/permissions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const Permissions = {
8484
'snapshot:meta': { description: 'View a Snapshot', role: Roles.Viewer },
8585
'snapshot:full': { description: 'View full snapshot details excluding credentials', role: Roles.Member },
8686
'snapshot:export': { description: 'Export a snapshot including credentials', role: Roles.Member },
87+
'snapshot:edit': { description: 'Edit a Snapshot', role: Roles.Owner },
8788
'snapshot:delete': { description: 'Delete a Snapshot', role: Roles.Owner },
8889
'snapshot:import': { description: 'Import a Snapshot', role: Roles.Owner },
8990

forge/routes/api/snapshot.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @memberof forge.routes.api
88
*/
99

10+
const { UpdatesCollection } = require('../../auditLog/formatters.js')
11+
1012
module.exports = async function (app) {
1113
/** @type {typeof import('../../db/controllers/Snapshot.js')} */
1214
const snapshotController = app.db.controllers.Snapshot
@@ -160,6 +162,54 @@ module.exports = async function (app) {
160162
reply.send({ status: 'okay' })
161163
})
162164

165+
/**
166+
* Update a snapshot
167+
*/
168+
app.put('/:id', {
169+
preHandler: app.needsPermission('snapshot:edit'),
170+
schema: {
171+
summary: 'Update a snapshot',
172+
tags: ['Snapshots'],
173+
params: {
174+
type: 'object',
175+
properties: {
176+
id: { type: 'string' }
177+
}
178+
},
179+
body: {
180+
type: 'object',
181+
properties: {
182+
name: { type: 'string' },
183+
description: { type: 'string' }
184+
}
185+
},
186+
response: {
187+
200: {
188+
$ref: 'Snapshot'
189+
},
190+
'4xx': {
191+
$ref: 'APIError'
192+
}
193+
}
194+
}
195+
}, async (request, reply) => {
196+
// capture the original name/description for the audit log
197+
const snapshotBefore = { name: request.snapshot.name, description: request.snapshot.description }
198+
// perform the update
199+
const snapshot = await snapshotController.updateSnapshot(request.snapshot, request.body)
200+
// log the update
201+
const snapshotAfter = { name: snapshot.name, description: snapshot.description }
202+
const updates = new UpdatesCollection()
203+
updates.pushDifferences(snapshotBefore, snapshotAfter)
204+
if (request.ownerType === 'device') {
205+
const application = await request.owner.getApplication()
206+
await applicationLogger.application.device.snapshot.updated(request.session.User, null, application, request.owner, request.snapshot, updates)
207+
} else if (request.ownerType === 'instance') {
208+
await projectLogger.project.snapshot.updated(request.session.User, null, request.owner, request.snapshot, updates)
209+
}
210+
reply.send(projectSnapshotView.snapshot(snapshot))
211+
})
212+
163213
/**
164214
* Export a snapshot for later import in another project or platform
165215
*/

frontend/src/api/snapshots.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,29 @@ const deleteSnapshot = async (snapshotId) => {
7878
})
7979
}
8080

81+
/**
82+
* Update a snapshot
83+
* @param {String} snapshotId - id of the snapshot
84+
* @param {Object} options - options to update
85+
* @param {String} [options.name] - name of the snapshot
86+
* @param {String} [options.description] - description of the snapshot
87+
*/
88+
const updateSnapshot = async (snapshotId, options) => {
89+
return client.put(`/api/v1/snapshots/${snapshotId}`, options).then(res => {
90+
const props = {
91+
'snapshot-id': snapshotId,
92+
'updated-at': (new Date()).toISOString()
93+
}
94+
product.capture('$ff-snapshot-updated', props, {})
95+
return res.data
96+
})
97+
}
98+
8199
export default {
82100
getSummary,
83101
getFullSnapshot,
84102
exportSnapshot,
85103
importSnapshot,
86-
deleteSnapshot
104+
deleteSnapshot,
105+
updateSnapshot
87106
}

frontend/src/components/audit-log/AuditEntryIcon.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ const iconMap = {
167167
],
168168
clock: [
169169
'project.snapshot.created',
170+
'project.snapshot.updated',
170171
'project.device.snapshot.created',
171172
'project.snapshot.deleted',
172173
'project.snapshot.rollback',
@@ -176,6 +177,7 @@ const iconMap = {
176177
'project.snapshot.device-target-set',
177178
'project.snapshot.deviceTarget', // legacy event
178179
'application.device.snapshot.created',
180+
'application.device.snapshot.updated',
179181
'application.device.snapshot.deleted',
180182
'application.device.snapshot.exported',
181183
'application.device.snapshot.imported',

frontend/src/components/audit-log/AuditEntryVerbose.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,11 @@
392392
<span v-if="!error && entry.body?.device && entry.body.snapshot">Snapshot '{{ entry.body.snapshot?.name }}' has been been created from Application owned Device '{{ entry.body.device?.name }}'.</span>
393393
<span v-else-if="!error">Device or Snapshot data not found in audit entry.</span>
394394
</template>
395+
<template v-else-if="entry.event === 'application.device.snapshot.updated'">
396+
<label>{{ AuditEvents[entry.event] }}</label>
397+
<span v-if="!error && entry.body && entry.body.updates">Snapshot '{{ entry.body.snapshot?.name }}' of Application owned Device '{{ entry.body.device?.name }}' has been been updated has with following changes: <AuditEntryUpdates :updates="entry.body.updates" /></span>
398+
<span v-else-if="!error">Change data not found in audit entry.</span>
399+
</template>
395400
<template v-else-if="entry.event === 'application.device.snapshot.deleted'">
396401
<label>{{ AuditEvents[entry.event] }}</label>
397402
<span v-if="!error && entry.body?.device && entry.body.snapshot">Snapshot '{{ entry.body.snapshot?.name }}' has been been deleted for Application owned Device '{{ entry.body.device?.name }}'.</span>
@@ -541,6 +546,11 @@
541546
<span v-if="!error && entry.body?.project && entry.body.snapshot">A new Snapshot '{{ entry.body.snapshot?.name }}' has been created for Instance '{{ entry.body.project?.name }}'.</span>
542547
<span v-else-if="!error">Instance data not found in audit entry.</span>
543548
</template>
549+
<template v-else-if="entry.event === 'project.snapshot.updated'">
550+
<label>{{ AuditEvents[entry.event] }}</label>
551+
<span v-if="!error && entry.body && entry.body.updates">Snapshot '{{ entry.body.snapshot?.name }}' of Instance '{{ entry.body.project?.name }}' has been been updated has with following changes: <AuditEntryUpdates :updates="entry.body.updates" /></span>
552+
<span v-else-if="!error">Change data not found in audit entry.</span>
553+
</template>
544554
<template v-else-if="entry.event === 'project.device.snapshot.created'">
545555
<label>{{ AuditEvents[entry.event] }}</label>
546556
<span v-if="!error && entry.body?.project && entry.body.snapshot">A new Snapshot '{{ entry.body.snapshot?.name }}' has been created from Device '{{ entry.body.device?.name }}' for Instance '{{ entry.body.project?.name }}'.</span>

0 commit comments

Comments
 (0)