Skip to content

Commit b86e18a

Browse files
committed
Merge remote-tracking branch 'origin/main' into 6649_add-remote-instance-immersive-view-instance-controls
2 parents 13613c8 + a4e7894 commit b86e18a

9 files changed

Lines changed: 70 additions & 18 deletions

File tree

forge/db/controllers/Device.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ module.exports = {
7070
* Sends the project id, snapshot hash and settings hash to the device
7171
* so that the device can determine what/if it needs to update
7272
* @param {forge.db.models.Device} device The device to send an "update" command to
73+
* @param {boolean} forceUpdate If true, the update command will instruct the agent to apply updates even if in developerMode. This require vTBD of the device-agent.
7374
*/
74-
sendDeviceUpdateCommand: async function (app, device) {
75+
sendDeviceUpdateCommand: async function (app, device, forceUpdate = false) {
7576
if (app.comms) {
7677
// ensure the device has all associations loaded
7778
let team = device.Team
@@ -105,6 +106,9 @@ module.exports = {
105106
mode: device.mode,
106107
licensed: app.license.active()
107108
}
109+
if (forceUpdate) {
110+
payload.forceUpdate = true
111+
}
108112
// if the device is assigned to an application but has no snapshot we need to send enough
109113
// info to start the device in application mode so that it can start node-red and
110114
// permit the user to generate new flows and submit a snapshot

forge/routes/api/device.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ module.exports = async function (app) {
475475
}
476476
}, async (request, reply) => {
477477
let sendDeviceUpdate = false
478+
let forceUpdate = false
478479
const device = request.device
479480
/** @type {import('../../auditLog/formatters').UpdatesCollection} */
480481
const updates = new app.auditLog.formatters.UpdatesCollection()
@@ -610,6 +611,10 @@ module.exports = async function (app) {
610611
await app.auditLog.Device.device.snapshot.targetSet(request.session.User, null, device, targetSnapshot)
611612
updates.push('targetSnapshotId', originalSnapshotId, device.targetSnapshotId)
612613
sendDeviceUpdate = true
614+
// If in developer mode, set the force flag to true otherwise the agent will ignore the update
615+
if (device.mode === 'developer') {
616+
forceUpdate = true
617+
}
613618
}
614619
if (request.body.name !== undefined && request.body.name !== device.name) {
615620
updates.push('name', device.name, request.body.name)
@@ -627,7 +632,7 @@ module.exports = async function (app) {
627632
await device.save()
628633
const updatedDevice = await app.db.models.Device.byId(device.id)
629634
if (sendDeviceUpdate) {
630-
await app.db.controllers.Device.sendDeviceUpdateCommand(updatedDevice)
635+
await app.db.controllers.Device.sendDeviceUpdateCommand(updatedDevice, forceUpdate)
631636
}
632637

633638
// check post op audit log action - create audit log entry if required

frontend/src/components/TeamTypeSelection.vue

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
v-for="type in types" :key="type.id"
66
:team-type="type"
77
:billingInterval="isAnnualBilling ? 'year' : 'month'"
8-
:enableCTA="!billingEnabled || !isAnnualBilling || !!type.annualBillingPrice"
8+
:enableCTA="!isBillingEnabled || !isAnnualBilling || !!type.annualBillingPrice"
99
/>
1010
</div>
1111
<div class="flex gap-6 justify-center relative z-10 flex-wrap mt-4">
12-
<div v-if="billingEnabled && annualBillingAvailable" class="text-sm font-medium text-gray-400 flex items-center gap-2">
12+
<div v-if="isBillingEnabled && annualBillingAvailable" class="text-sm font-medium text-gray-400 flex items-center gap-2">
1313
<span :class="{'text-gray-800': !isAnnualBilling }">Monthly</span>
1414
<ff-toggle-switch v-model="isAnnualBilling" />
1515
<span :class="{'text-gray-800': isAnnualBilling }">Yearly</span>
@@ -19,7 +19,7 @@
1919
</template>
2020

2121
<script>
22-
import { mapState } from 'vuex'
22+
import { mapGetters, mapState } from 'vuex'
2323
2424
import teamTypesApi from '../api/teamTypes.js'
2525
@@ -39,9 +39,7 @@ export default {
3939
},
4040
computed: {
4141
...mapState('account', ['user']),
42-
billingEnabled () {
43-
return true
44-
}
42+
...mapGetters('account', ['isBillingEnabled'])
4543
},
4644
async created () {
4745
const { types } = await teamTypesApi.getTeamTypes()

frontend/src/components/drawers/RightDrawer.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
:disabled="typeof action.disabled === 'function' ? action.disabled() : action.disabled"
2323
:has-left-icon="!!action.iconLeft"
2424
v-bind="action.bind"
25+
:title="typeof action.tooltip === 'function' ? action.tooltip() : action.tooltip"
2526
@click="action.handler"
2627
>
2728
<template v-if="!!action.iconLeft" #icon-left>

frontend/src/components/drawers/snapshots/SnapshotDetailsDrawer.vue

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
</information-well>
2525
</section>
2626

27-
<div class="flex-1">
27+
<div class="flex flex-col flex-1 gap-4">
2828
<section class="name">
2929
<div class="header flex flex-row justify-between">
3030
<span class="title font-bold">Name:</span>
@@ -196,6 +196,11 @@ export default defineComponent({
196196
required: false,
197197
default: true
198198
},
199+
canRestoreReason: {
200+
type: String,
201+
required: false,
202+
default: ''
203+
},
199204
instance: {
200205
type: Object,
201206
required: false,
@@ -206,6 +211,11 @@ export default defineComponent({
206211
required: false,
207212
default: false
208213
},
214+
isDeviceDevMode: {
215+
type: Boolean,
216+
required: false,
217+
default: false
218+
},
209219
snapshot: {
210220
type: Object,
211221
required: true
@@ -215,7 +225,7 @@ export default defineComponent({
215225
required: true
216226
}
217227
},
218-
emits: ['updated-snapshot', 'deleted-snapshot'],
228+
emits: ['restored-snapshot', 'updated-snapshot', 'deleted-snapshot'],
219229
setup () {
220230
const { hasPermission } = usePermissions()
221231
@@ -279,25 +289,30 @@ export default defineComponent({
279289
const currentTargetSnapshot = this.instance.targetSnapshot?.id
280290
281291
if (typeof currentTargetSnapshot === 'string' && currentTargetSnapshot === snapshot.id) {
282-
Alerts.emit('This snapshot is already deployed to this device.', 'info', 7500)
292+
Alerts.emit('This snapshot is already deployed to this remote instance.', 'info', 7500)
283293
return
284294
}
285295
286-
let body = `Are you sure you want to restore snapshot '${snapshot.name}' to this device?`
296+
let body = `Are you sure you want to restore snapshot '${snapshot.name}' to this remote instance?`
287297
if (snapshot.device?.id !== this.instance.id) {
288-
body = `Snapshot '${snapshot.name}' was not generated by this device. Are you sure you want to deploy it to this device?`
298+
body = `Snapshot '${snapshot.name}' was not generated by this remote instance. Are you sure you want to deploy it to this remote instance?`
299+
}
300+
if (this.isDevice && this.isDeviceDevMode) {
301+
body += `
302+
Any changes made to the remote instance whilst in developer mode will be lost.
303+
To avoid losses, you can cancel this operation and create a snapshot in the developer mode tab.`
289304
}
290305
291306
Dialog.show({
292-
header: `Restore Snapshot to device '${this.instance.name}'`,
307+
header: `Restore Snapshot to remote instance '${this.instance.name}'`,
293308
kind: 'danger',
294309
text: body,
295310
confirmLabel: 'Confirm'
296311
}, async () => {
297312
try {
298313
await DeviceApi.setSnapshotAsTarget(this.instance.id, snapshot.id)
299314
Alerts.emit('Successfully applied the snapshot.', 'confirmation')
300-
this.$emit('updated-snapshot', this.snapshot)
315+
this.$emit('restored-snapshot', this.snapshot)
301316
} catch (err) {
302317
Alerts.emit('Failed to apply snapshot: ' + err.toString(), 'warning', 7500)
303318
}
@@ -371,6 +386,9 @@ export default defineComponent({
371386
disabled: function () {
372387
return !context.canRestore
373388
},
389+
tooltip: function () {
390+
return context.canRestoreReason || ''
391+
},
374392
bind: {
375393
'data-action': 'restore'
376394
}

frontend/src/pages/device/VersionHistory/Snapshots/index.vue

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105

106106
<script>
107107
import { FilterIcon, PlusSmIcon, UploadIcon } from '@heroicons/vue/outline'
108+
import SemVer from 'semver'
108109
import { markRaw } from 'vue'
109110
import { mapActions, mapState } from 'vuex'
110111
@@ -334,7 +335,19 @@ export default {
334335
},
335336
// enable/disable snapshot actions
336337
canDeploy (_row) {
337-
return !this.developerMode && this.hasPermission('device:edit', { application: this.device.application })
338+
return (!this.developerMode || this.supportsDevModeSnapshotRestore()) && this.hasPermission('device:edit', { application: this.device.application })
339+
},
340+
canDeployReason (snapshot) {
341+
if (!this.hasPermission('device:edit', { application: this.device.application })) {
342+
return 'You do not have permission to deploy snapshots to this Remote Instance'
343+
}
344+
if (this.developerMode && !this.supportsDevModeSnapshotRestore()) {
345+
return 'Snapshots deploys to Developer Mode Remote Instances requires Device Agent v3.8.0 or later'
346+
}
347+
return ''
348+
},
349+
supportsDevModeSnapshotRestore () {
350+
return this.device.agentVersion && SemVer.gte(this.device.agentVersion, '3.8.0')
338351
},
339352
onRowSelected (snapshot) {
340353
this.openRightDrawer({
@@ -345,10 +358,18 @@ export default {
345358
instance: this.device,
346359
canSetDeviceTarget: false,
347360
canRestore: this.canDeploy(snapshot),
348-
isDevice: true
361+
canRestoreReason: this.canDeployReason(snapshot),
362+
isDevice: true,
363+
isDeviceDevMode: this.developerMode
349364
},
350365
on: {
351366
updatedSnapshot: () => this.fetchData(true),
367+
restoredSnapshot: () => {
368+
setTimeout(() => {
369+
this.$emit('device-updated')
370+
}, 100)
371+
this.fetchData(true)
372+
},
352373
deletedSnapshot: () => {
353374
this.closeRightDrawer()
354375
this.fetchData(true)

frontend/src/pages/device/VersionHistory/index.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
@show-import-snapshot-dialog="showImportSnapshotDialog"
6565
@show-create-snapshot-dialog="showCreateSnapshotDialog"
6666
@instance-updated="$emit('instance-updated')"
67+
@device-updated="$emit('device-updated')"
6768
/>
6869
</transition>
6970
</router-view>
@@ -120,7 +121,7 @@ export default {
120121
required: true
121122
}
122123
},
123-
emits: ['instance-updated'],
124+
emits: ['instance-updated', 'device-updated'],
124125
setup () {
125126
const { hasPermission } = usePermissions()
126127

frontend/src/pages/instance/VersionHistory/Snapshots/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ export default {
243243
props: { snapshot, snapshotList: this.snapshotList, instance: this.instance },
244244
on: {
245245
updatedSnapshot: () => this.fetchData(true),
246+
restoredSnapshot: () => this.fetchData(true),
246247
deletedSnapshot: () => {
247248
this.closeRightDrawer()
248249
this.fetchData(true)

frontend/src/store/modules/account/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ const getters = {
112112
!state.team?.type?.properties?.billing?.disabled &&
113113
!state.team?.billing?.active
114114
},
115+
isBillingEnabled (state) {
116+
return !!state.features.billing
117+
},
115118
isTrialAccount (state) {
116119
return !!state.team?.billing?.trial
117120
},

0 commit comments

Comments
 (0)