Skip to content

Commit

Permalink
Merge pull request #4647 from FlowFuse/visual-timeline-of-version-his…
Browse files Browse the repository at this point in the history
…tory

Visual timeline of version history
  • Loading branch information
cstns authored Oct 22, 2024
2 parents 607e2eb + d9c1207 commit 48a8c0f
Show file tree
Hide file tree
Showing 48 changed files with 1,863 additions and 413 deletions.
16 changes: 16 additions & 0 deletions frontend/src/api/projectHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import paginateUrl from '../utils/paginateUrl.js'

import client from './client.js'

const getHistory = async (instanceId, cursor = undefined, limit = 10) => {
const url = paginateUrl(`/api/v1/projects/${instanceId}/history`, cursor, limit)

return await client.get(url)
.then(res => {
return res.data
})
}

export default {
getHistory
}
2 changes: 1 addition & 1 deletion frontend/src/components/DevicesBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ import DeviceLink from '../pages/application/components/cells/DeviceLink.vue'
import Snapshot from '../pages/application/components/cells/Snapshot.vue'
import DeviceLastSeenBadge from '../pages/device/components/DeviceLastSeenBadge.vue'
import SnapshotAssignDialog from '../pages/instance/Snapshots/dialogs/SnapshotAssignDialog.vue'
import SnapshotAssignDialog from '../pages/instance/VersionHistory/Snapshots/dialogs/SnapshotAssignDialog.vue'
import InstanceStatusBadge from '../pages/instance/components/InstanceStatusBadge.vue'
import DeviceAssignApplicationDialog from '../pages/team/Devices/dialogs/DeviceAssignApplicationDialog.vue'
import DeviceAssignInstanceDialog from '../pages/team/Devices/dialogs/DeviceAssignInstanceDialog.vue'
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/SectionTopMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="ff-section-header flex flex-nowrap border-b border-gray-400 mb-4 sm:mb-2 text-gray-500 justify-between h-12">
<div class="w-full wrapper flex items-center md:w-auto mb-2 gap-x-2">
<div class="w-full wrapper flex md:w-auto mb-2 gap-x-2">
<div class="flex gap-2 items-center">
<slot name="hero">
<div class="flex">
Expand All @@ -9,7 +9,7 @@
</slot>
<InformationCircleIcon v-if="hasInfoDialog" class="min-w-[20px] ff-icon text-gray-800 cursor-pointer hover:text-blue-700" @click="openInfoDialog()" />
</div>
<span v-if="info" class="hidden sm:block text-gray-400 info">{{ info }}</span>
<div v-if="info" class="hidden sm:block text-gray-400 info">{{ info }}</div>
<div class="actions">
<ul v-if="options.length > 0" class="flex">
<li v-for="item in options" :key="item.name" class="mr-8 pt-1 flex">
Expand Down Expand Up @@ -88,12 +88,12 @@ export default {
<style lang="scss">
.wrapper {
flex: 1;
align-items: baseline;
.info {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
align-self:center;
}
.actions {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/dialogs/SnapshotEditDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</template>
<template #actions>
<ff-button kind="secondary" data-action="dialog-cancel" :disabled="submitted" @click="cancel()">Cancel</ff-button>
<ff-button :kind="kind" data-action="dialog-confirm" :disabled="!formValid" @click="confirm()">Update</ff-button>
<ff-button kind="primary" data-action="dialog-confirm" :disabled="!formValid" @click="confirm()">Update</ff-button>
</template>
</ff-dialog>
</template>
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/composables/Ux.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/**
*
* @param {Element} $el
* @param {'auto' || 'instant' || 'smooth'} behavior
* @param {number} top - Specifies the number of pixels along the Y axis to scroll the window or element.
* @param {number} left - Specifies the number of pixels along the X axis to scroll the window or element.
*/
export function scrollTo ($el, {
behavior = 'smooth',
top = 0,
left = 0
} = {}) {
$el.scrollTo({ behavior, top, left })
}

/**
*
* @param {Element} $el
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/mixins/Features.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default {
return this.isStaticAssetFeatureEnabledForPlatform && this.isStaticAssetsFeatureEnabledForTeam
},
isHTTPBearerTokensFeatureEnabledForTeam () {
return this.settings.features.httpBearerTokens && this.team.type.properties.features.teamHttpSecurity
return this.settings?.features.httpBearerTokens && this.team.type.properties.features.teamHttpSecurity
},
isBOMFeatureEnabledForPlatform () {
return !!this.features.bom
Expand All @@ -53,6 +53,15 @@ export default {
},
isBOMFeatureEnabled () {
return this.isBOMFeatureEnabledForPlatform && this.isBOMFeatureEnabledForTeam
},
isTimelineFeatureEnabledForPlatform () {
return !!this.features.projectHistory
},
isTimelineFeatureEnabledForTeam () {
return !!this.team?.type?.properties?.features?.projectHistory
},
isTimelineFeatureEnabled () {
return this.isTimelineFeatureEnabledForPlatform && this.isTimelineFeatureEnabledForTeam
}
}
}
116 changes: 116 additions & 0 deletions frontend/src/mixins/Snapshots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import InstanceApi from '../api/instances.js'
import SnapshotApi from '../api/projectSnapshots.js'
import SnapshotsApi from '../api/snapshots.js'
import { downloadData } from '../composables/Download.js'
import Alerts from '../services/alerts.js'
import Dialog from '../services/dialog.js'
import Product from '../services/product.js'

export default {
methods: {
showViewSnapshotDialog (snapshot) {
Product.capture('ff-snapshot-view', {
'snapshot-id': snapshot.id
}, {
instance: this.instance?.id
})
SnapshotsApi.getFullSnapshot(snapshot.id).then((data) => {
this.$refs.snapshotViewerDialog.show(data)
}).catch(err => {
console.error(err)
Alerts.emit('Failed to get snapshot.', 'warning')
})
},
// snapshot actions - rollback
showRollbackDialog (snapshot, alterLoadingState = false) {
return new Promise((resolve) => {
Dialog.show({
header: 'Restore 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?`,
confirmLabel: 'Confirm'
}, async () => {
if (alterLoadingState) this.loading = true
return SnapshotApi.rollbackSnapshot(this.instance.id, snapshot.id)
.then(() => {
Alerts.emit('Successfully deployed snapshot.', 'confirmation')
resolve()
})
})
})
},
showCompareSnapshotDialog (snapshot) {
Product.capture('ff-snapshot-compare', {
'snapshot-id': snapshot.id
}, {
instance: this.instance?.id
})
SnapshotsApi.getFullSnapshot(snapshot.id)
.then((data) => this.$refs.snapshotCompareDialog.show(data, this.snapshotList))
.catch(err => {
console.error(err)
Alerts.emit('Failed to get snapshot.', 'warning')
})
},
showDownloadSnapshotDialog (snapshot) {
this.$refs.snapshotExportDialog.show(snapshot)
},
async downloadSnapshotPackage (snapshot) {
Product.capture('ff-snapshot-download', {
'snapshot-id': snapshot.id
}, {
instance: this.instance?.id
})
const ss = await SnapshotsApi.getSummary(snapshot.id)
const owner = ss.device || ss.project
const ownerType = ss.device ? 'device' : 'instance'
const packageJSON = {
name: `${owner.safeName || owner.name}`.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase(),
description: `${ownerType} snapshot, ${snapshot.name} - ${snapshot.description}`,
private: true,
version: '0.0.0-' + snapshot.id,
dependencies: ss.modules || {}
}
downloadData(packageJSON, 'package.json')
},
// snapshot actions - delete
showDeleteSnapshotDialog (snapshot) {
return new Promise((resolve) => {
Dialog.show({
header: 'Delete Snapshot',
text: 'Are you sure you want to delete this snapshot?',
kind: 'danger',
confirmLabel: 'Delete'
}, async () => {
await SnapshotsApi.deleteSnapshot(snapshot.id)
if (this.snapshots) {
const index = this.snapshots.indexOf(snapshot)
this.snapshots.splice(index, 1)
}
Alerts.emit('Successfully deleted snapshot.', 'confirmation')
resolve()
})
})
},
showEditSnapshotDialog (snapshot) {
this.$refs.snapshotEditDialog.show(snapshot)
},
// snapshot actions - set as device target
showDeviceTargetDialog (snapshot) {
Dialog.show({
header: 'Set Device Target Snapshot',
text: `Are you sure you want to set this snapshot as the device target?
All devices assigned to this instance will be restarted on this snapshot.`,
confirmLabel: 'Set Target'
}, async () => {
await InstanceApi.updateInstanceDeviceSettings(this.instance.id, {
targetSnapshot: snapshot.id
})
Alerts.emit('Successfully set device target.', 'confirmation')
this.$emit('instance-updated')
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@
<FormRow v-model="input.properties.features.staticAssets" type="checkbox">Static Assets</FormRow>
<FormRow v-model="input.properties.features.bom" type="checkbox">Bill of Materials / Dependencies</FormRow>
<FormRow v-model="input.properties.features.teamBroker" type="checkbox">Team Broker</FormRow>
<FormRow v-model="input.properties.features.projectHistory" type="checkbox">Version History Timeline</FormRow>
<!-- to make the grid work nicely, only needed if there is an odd number of checkbox features above-->
<span />
<!-- <span />-->
<FormRow v-model="input.properties.features.fileStorageLimit">Persistent File storage limit (Mb)</FormRow>
<FormRow v-model="input.properties.features.contextLimit">Persistent Context storage limit (Mb)</FormRow>
</div>
Expand Down Expand Up @@ -236,6 +237,9 @@ export default {
if (this.input.properties.features.customHostnames === undefined) {
this.input.properties.features.customHostnames = false
}
if (this.input.properties.features.projectHistory === undefined) {
this.input.properties.features.projectHistory = false
}
} else {
this.editDisabled = false
this.input = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default {
snapshotsApi.exportSnapshot(this.snapshot.id, opts).then((data) => {
return data
}).then(data => {
const snapshotDate = this.snapshot.updatedAt.replace(/[-:]/g, '').replace(/\..*$/, '').replace('T', '-')
const snapshotDate = data.updatedAt.replace(/[-:]/g, '').replace(/\..*$/, '').replace('T', '-')
downloadData(data, `snapshot-${this.snapshot.id}-${snapshotDate}.json`)
alerts.emit('Snapshot exported.', 'confirmation')
this.$refs.dialog.close()
Expand Down
18 changes: 15 additions & 3 deletions frontend/src/pages/instance/Editor/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ export default {
computed: {
navigation () {
if (!this.instance.id) return []
let versionHistoryRoute
if (!this.isTimelineFeatureEnabled) {
versionHistoryRoute = {
name: 'instance-editor-snapshots',
params: { id: this.instance.id }
}
} else {
versionHistoryRoute = {
name: 'instance-editor-version-history',
params: { id: this.instance.id }
}
}
return [
{
label: 'Overview',
Expand All @@ -124,9 +136,9 @@ export default {
tag: 'instance-remote'
},
{
label: 'Snapshots',
to: { name: 'instance-editor-snapshots', params: { id: this.instance.id } },
tag: 'instance-snapshots'
label: 'Version History',
to: versionHistoryRoute,
tag: 'instance-version-history'
},
{
label: 'Assets',
Expand Down
40 changes: 32 additions & 8 deletions frontend/src/pages/instance/Editor/routes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import InstanceSettings from '../Settings/index.vue'
import InstanceSettingsRoutes from '../Settings/routes.js'
import VersionHistory from '../VersionHistory/index.vue'
import VersionHistoryRoutes from '../VersionHistory/routes.js'
import { children } from '../routes.js'

import InstanceEditor from './index.vue'
Expand All @@ -17,7 +19,7 @@ export default [
return { name: 'instance-editor-overview', params: { id: to.params.id } }
},
children: [
...children.filter(child => !['settings'].includes(child.path)).map(child => {
...children.filter(child => !['settings', 'version-history'].includes(child.path)).map(child => {
return {
...child,
name: child.name.replace('instance-', 'instance-editor-')
Expand All @@ -33,12 +35,34 @@ export default [
redirect: to => {
return { name: 'instance-editor-settings-general', params: { id: to.params.id } }
},
children: [...InstanceSettingsRoutes.map(child => {
return {
...child,
name: child.name.replace('instance-settings', 'instance-editor-settings')
}
})]
}]
children: [
...InstanceSettingsRoutes.map(child => {
return {
...child,
name: child.name.replace('instance-settings', 'instance-editor-settings')
}
})
]
},
{
path: 'version-history',
name: 'instance-editor-version-history',
component: VersionHistory,
meta: {
title: 'Instance - Version History'
},
redirect: to => {
return { name: 'instance-editor-version-history-timeline', params: { id: to.params.id } }
},
children: [
...VersionHistoryRoutes.map(child => {
return {
...child,
name: child.name.replace('instance-', 'instance-editor-')
}
})
]
}
]
}
]
Loading

0 comments on commit 48a8c0f

Please sign in to comment.