Skip to content

Commit

Permalink
Merge pull request #3835 from FlowFuse/3789-snapshot-view-flows
Browse files Browse the repository at this point in the history
Visualise a snapshots flows
  • Loading branch information
joepavitt authored May 8, 2024
2 parents 9395ad5 + 3fb8254 commit 6c8efd2
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 13 deletions.
61 changes: 61 additions & 0 deletions frontend/src/api/snapshots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import product from '../services/product.js'
import daysSince from '../utils/daysSince.js'

import client from './client.js'

/**
* Get summary of a snapshot
*/
const getSummary = (snapshotId) => {
return client.get(`/api/v1/snapshots/${snapshotId}`).then(res => {
res.data.createdSince = daysSince(res.data.createdAt)
res.data.updatedSince = daysSince(res.data.updatedAt)
return res.data
})
}

/**
* Get full snapshot
*/
const getFullSnapshot = (snapshotId) => {
return client.get(`/api/v1/snapshots/${snapshotId}/full`).then(res => {
res.data.createdSince = daysSince(res.data.createdAt)
res.data.updatedSince = daysSince(res.data.updatedAt)
return res.data
})
}

/**
* Export a snapshot for later import in another project or platform
*/
const exportSnapshot = (snapshotId, options) => {
return client.post(`/api/v1/snapshots/${snapshotId}/export`, options).then(res => {
const props = {
'snapshot-id': res.data.id
}
product.capture('$ff-snapshot-export', props, {})
return res.data
})
}

/**
* Delete a snapshot
* @param {String} snapshotId - id of the snapshot
*/
const deleteSnapshot = async (snapshotId) => {
return client.delete(`/api/v1/snapshots/${snapshotId}`).then(res => {
const props = {
'snapshot-id': snapshotId,
'deleted-at': (new Date()).toISOString()
}
product.capture('$ff-snapshot-deleted', props, {})
return res.data
})
}

export default {
deleteSnapshot,
getFullSnapshot,
exportSnapshot,
getSummary
}
68 changes: 68 additions & 0 deletions frontend/src/components/dialogs/SnapshotViewerDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template>
<ff-dialog ref="dialog" :header="header" confirm-label="Close" :closeOnConfirm="true" data-el="snapshot-view-dialog" boxClass="!min-w-[80%] !min-h-[80%] !w-[80%] !h-[80%]" contentClass="overflow-hidden" @confirm="confirm()">
<template #default>
<div ref="viewer" data-el="ff-flow-previewer" class="ff-flow-viewer">
Loading...
</div>
</template>
<template #actions>
<div class="flex justify-end">
<ff-button @click="confirm()">Close</ff-button>
</div>
</template>
</ff-dialog>
</template>
<script>
import FlowRenderer from '@flowfuse/flow-renderer'
export default {
name: 'SnapshotViewerDialog',
components: {
// FlowViewer
},
setup () {
return {
show (snapshot) {
this.$refs.dialog.show()
this.snapshot = snapshot
setTimeout(() => {
this.renderFlows()
}, 20)
}
}
},
data () {
return {
snapshot: []
}
},
computed: {
flow () {
return this.snapshot?.flows?.flows || []
},
header () {
return this.snapshot?.name || 'Snapshot'
}
},
mounted () {
},
methods: {
confirm () {
this.$refs.dialog.close()
},
renderFlows () {
const flowRenderer = new FlowRenderer()
flowRenderer.renderFlows(this.flow, {
container: this.$refs.viewer
})
}
}
}
</script>
<style scoped>
.ff-flow-viewer {
height: 100%;
}
</style>
26 changes: 25 additions & 1 deletion frontend/src/pages/application/Snapshots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
</div>
<ff-loading v-if="loading" message="Loading Snapshots..." />
<template v-if="snapshots.length > 0">
<ff-data-table data-el="snapshots" class="space-y-4" :columns="columns" :rows="snapshots" :show-search="true" search-placeholder="Search Snapshots..." />
<ff-data-table data-el="snapshots" class="space-y-4" :columns="columns" :rows="snapshots" :show-search="true" search-placeholder="Search Snapshots...">
<template #context-menu="{row}">
<ff-list-item :disabled="!canViewSnapshot(row)" label="View Snapshot" @click="showViewSnapshotDialog(row)" />
</template>
</ff-data-table>
</template>
<template v-else-if="!loading">
<EmptyState>
Expand All @@ -32,17 +36,22 @@
</EmptyState>
</template>
</div>
<SnapshotViewerDialog ref="snapshotViewerDialog" data-el="dialog-view-snapshot" />
</template>

<script>
import { markRaw } from 'vue'
import { mapState } from 'vuex'
import ApplicationApi from '../../api/application.js'
import SnapshotsApi from '../../api/snapshots.js'
import EmptyState from '../../components/EmptyState.vue'
import SectionTopMenu from '../../components/SectionTopMenu.vue'
import SnapshotViewerDialog from '../../components/dialogs/SnapshotViewerDialog.vue'
import UserCell from '../../components/tables/cells/UserCell.vue'
import permissionsMixin from '../../mixins/Permissions.js'
import Alerts from '../instance/Settings/Alerts.vue'
// Table Cells
import DaysSince from './Snapshots/components/cells/DaysSince.vue'
Expand All @@ -53,6 +62,7 @@ export default {
name: 'ApplicationSnapshots',
components: {
SectionTopMenu,
SnapshotViewerDialog,
EmptyState
},
mixins: [permissionsMixin],
Expand Down Expand Up @@ -102,6 +112,9 @@ export default {
]
}
},
computed: {
...mapState('account', ['teamMembership'])
},
mounted () {
this.loadSnapshots()
},
Expand All @@ -111,6 +124,17 @@ export default {
const data = await ApplicationApi.getSnapshots(this.application.id, null, null, null)
this.snapshots = [...data.snapshots]
this.loading = false
},
canViewSnapshot: function (row) {
return this.hasPermission('snapshot:full')
},
showViewSnapshotDialog (row) {
SnapshotsApi.getFullSnapshot(row.id).then((data) => {
this.$refs.snapshotViewerDialog.show(data)
}).catch(err => {
console.error(err)
Alerts.emit('Failed to get snapshot.', 'warning')
})
}
}
}
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/pages/device/Snapshots/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<ff-button kind="primary" data-action="create-snapshot" :disabled="!developerMode || busyMakingSnapshot" @click="showCreateSnapshotDialog"><template #icon-left><PlusSmIcon /></template>Create Snapshot</ff-button>
</template>
<template v-if="showContextMenu" #context-menu="{row}">
<ff-list-item v-if="hasPermission('snapshot:full')" label="View Snapshot" @click="showViewSnapshotDialog(row)" />
<ff-list-item v-if="hasPermission('device:snapshot:delete') && rowIsThisDevice(row)" label="Delete Snapshot" kind="danger" @click="showDeleteSnapshotDialog(row)" />
<ff-list-item v-if="!rowIsThisDevice(row)" disabled label="No actions available" kind="info" />
</template>
Expand Down Expand Up @@ -52,6 +53,7 @@
</EmptyState>
</template>
<SnapshotCreateDialog ref="snapshotCreateDialog" title="Create Device Snapshot" data-el="dialog-create-device-snapshot" :show-set-as-target="true" :device="device" @device-upload-success="onSnapshotCreated" @device-upload-failed="onSnapshotFailed" @canceled="onSnapshotCancel" />
<SnapshotViewerDialog ref="snapshotViewerDialog" data-el="dialog-view-snapshot" />
</div>
</template>

Expand All @@ -62,9 +64,11 @@ import { mapState } from 'vuex'
import ApplicationApi from '../../../api/application.js'
import DeviceApi from '../../../api/devices.js'
import SnapshotApi from '../../../api/snapshots.js'
import EmptyState from '../../../components/EmptyState.vue'
import SectionTopMenu from '../../../components/SectionTopMenu.vue'
import SnapshotViewerDialog from '../../../components/dialogs/SnapshotViewerDialog.vue'
import UserCell from '../../../components/tables/cells/UserCell.vue'
import permissionsMixin from '../../../mixins/Permissions.js'
import Alerts from '../../../services/alerts.js'
Expand All @@ -82,6 +86,7 @@ export default {
SectionTopMenu,
EmptyState,
SnapshotCreateDialog,
SnapshotViewerDialog,
PlusSmIcon
},
mixins: [permissionsMixin],
Expand All @@ -105,7 +110,7 @@ export default {
computed: {
...mapState('account', ['teamMembership', 'features']),
showContextMenu: function () {
return this.hasPermission('device:snapshot:delete')
return this.hasPermission('snapshot:delete') || this.hasPermission('snapshot:export')
},
columns () {
const cols = [
Expand Down Expand Up @@ -222,7 +227,7 @@ export default {
kind: 'danger',
confirmLabel: 'Delete'
}, async () => {
await DeviceApi.deleteSnapshot(this.device.id, snapshot.id)
await SnapshotApi.deleteSnapshot(snapshot.id)
const index = this.snapshots.indexOf(snapshot)
this.snapshots.splice(index, 1)
Alerts.emit('Successfully deleted snapshot.', 'confirmation')
Expand All @@ -241,7 +246,7 @@ export default {
},
onSnapshotFailed (err) {
console.error(err)
Alerts.emit('Failed to create snapshot of device.', 'error')
Alerts.emit('Failed to create snapshot of device.', 'warning')
this.busyMakingSnapshot = false
},
onSnapshotCancel () {
Expand Down Expand Up @@ -288,6 +293,14 @@ export default {
Alerts.emit('Failed to apply snapshot: ' + err.toString(), 'warning', 7500)
}
})
},
showViewSnapshotDialog (snapshot) {
SnapshotApi.getFullSnapshot(snapshot.id).then((data) => {
this.$refs.snapshotViewerDialog.show(data)
}).catch(err => {
console.error(err)
Alerts.emit('Failed to get snapshot.', 'warning')
})
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/pages/instance/Snapshots/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</template>
<template v-if="showContextMenu" #context-menu="{row}">
<ff-list-item v-if="hasPermission('project:snapshot:rollback')" label="Rollback" @click="showRollbackDialog(row)" />
<ff-list-item v-if="hasPermission('snapshot:full')" label="View Snapshot" @click="showViewSnapshotDialog(row)" />
<ff-list-item v-if="hasPermission('project:snapshot:export')" label="Download Snapshot" @click="showDownloadSnapshotDialog(row)" />
<ff-list-item v-if="hasPermission('project:snapshot:read')" label="Download package.json" @click="downloadSnapshotPackage(row)" />
<ff-list-item v-if="hasPermission('project:snapshot:set-target')" label="Set as Device Target" @click="showDeviceTargetDialog(row)" />
Expand Down Expand Up @@ -50,6 +51,7 @@
</template>
<SnapshotCreateDialog ref="snapshotCreateDialog" data-el="dialog-create-snapshot" :project="instance" @snapshot-created="snapshotCreated" />
<SnapshotExportDialog ref="snapshotExportDialog" data-el="dialog-export-snapshot" :project="instance" />
<SnapshotViewerDialog ref="snapshotViewerDialog" data-el="dialog-view-snapshot" />
</div>
</template>

Expand All @@ -60,9 +62,11 @@ import { mapState } from 'vuex'
import InstanceApi from '../../../api/instances.js'
import SnapshotApi from '../../../api/projectSnapshots.js'
import SnapshotsApi from '../../../api/snapshots.js'
import EmptyState from '../../../components/EmptyState.vue'
import SectionTopMenu from '../../../components/SectionTopMenu.vue'
import SnapshotViewerDialog from '../../../components/dialogs/SnapshotViewerDialog.vue'
import UserCell from '../../../components/tables/cells/UserCell.vue'
import permissionsMixin from '../../../mixins/Permissions.js'
import Alerts from '../../../services/alerts.js'
Expand All @@ -81,6 +85,7 @@ export default {
EmptyState,
SnapshotCreateDialog,
SnapshotExportDialog,
SnapshotViewerDialog,
PlusSmIcon
},
mixins: [permissionsMixin],
Expand All @@ -102,7 +107,7 @@ export default {
computed: {
...mapState('account', ['teamMembership']),
showContextMenu: function () {
return this.hasPermission('project:snapshot:rollback') || this.hasPermission('project:snapshot:set-target') || this.hasPermission('project:snapshot:delete') || this.hasPermission('project:snapshot:export')
return this.hasPermission('project:snapshot:rollback') || this.hasPermission('project:snapshot:set-target') || this.hasPermission('project:snapshot:delete') || this.hasPermission('project:snapshot:export') || this.hasPermission('snapshot:full')
},
columns () {
const cols = [
Expand Down Expand Up @@ -259,6 +264,14 @@ export default {
},
showDownloadSnapshotDialog (snapshot) {
this.$refs.snapshotExportDialog.show(snapshot)
},
showViewSnapshotDialog (snapshot) {
SnapshotsApi.getFullSnapshot(snapshot.id).then((data) => {
this.$refs.snapshotViewerDialog.show(data)
}).catch(err => {
console.error(err)
Alerts.emit('Failed to get snapshot.', 'warning')
})
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/ui-components/components/DialogBox.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<div ref="container" class="ff-dialog-container" :class="'ff-dialog-container--' + (open ? 'open' : 'closed')">
<div class="ff-dialog-box">
<div class="ff-dialog-box" :class="boxClass">
<div class="ff-dialog-header" data-sentry-unmask>{{ header }}</div>
<div ref="content" class="ff-dialog-content">
<div ref="content" class="ff-dialog-content" :class="contentClass">
<slot></slot>
</div>
<div class="ff-dialog-actions">
Expand Down Expand Up @@ -38,6 +38,14 @@ export default {
closeOnConfirm: {
type: Boolean,
default: true
},
boxClass: {
type: String,
default: ''
},
contentClass: {
type: String,
default: ''
}
},
emits: ['cancel', 'confirm'],
Expand Down
23 changes: 23 additions & 0 deletions test/e2e/frontend/cypress/fixtures/snapshots/device-snapshots.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"count": 1,
"snapshots": [
{
"createdAt": "2024-01-01T11:22:33.444Z",
"updatedAt": "2024-01-01T11:22:33.444Z",
"ownerType": "device",
"deviceId": "2",
"device": {
"id": "2",
"name": "device-2",
"type": "type-1",
"lastSeenMs": null,
"status": "running",
"mode": "autonomous",
"isDeploying": false
},
"id": "1",
"name": "application device-2 snapshot-1",
"description": "a device snapshot"
}
]
}
Loading

0 comments on commit 6c8efd2

Please sign in to comment.