Skip to content

Commit 392a993

Browse files
authored
Merge pull request #6652 from FlowFuse/6649_add-remote-instance-immersive-view-instance-controls
Add remote instance immersive view instance controls
2 parents 7989b5e + 3d21b73 commit 392a993

16 files changed

Lines changed: 420 additions & 82 deletions

File tree

frontend/src/components/DropdownMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
:key="$index"
3030
:disabled="!item || item.disabled === true"
3131
>
32-
<template v-if="item == null">
32+
<template v-if="item == null || item.type === 'hr'">
3333
<hr>
3434
</template>
3535
<template v-else-if="item.disabled">

frontend/src/components/FinishSetup.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<ff-button :kind="minimalView ? 'tertiary': 'secondary'" data-action="finish-setup" @click="finishSetup">
2+
<ff-button :kind="kind" data-action="finish-setup" @click="finishSetup">
33
<template #icon-left><ExclamationIcon class="ff-icon" /></template>
44
Finish Setup
55
</ff-button>
@@ -24,6 +24,22 @@ export default {
2424
minimalView: {
2525
type: Boolean,
2626
default: false
27+
},
28+
isPrimary: {
29+
type: Boolean,
30+
default: false
31+
}
32+
},
33+
computed: {
34+
kind () {
35+
switch (true) {
36+
case this.isPrimary:
37+
return 'primary'
38+
case this.minimalView:
39+
return 'tertiary'
40+
default:
41+
return 'secondary'
42+
}
2743
}
2844
},
2945
methods: {

frontend/src/components/immersive-editor/RemoteInstanceEditorWrapper.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,15 @@ export default {
4747
this.device.editor.connected
4848
},
4949
computedStatus () {
50-
if (!this.device || !this.isEditorAvailable) {
51-
// forces the loading animation while loading
50+
switch (true) {
51+
case !this.device:
52+
case !this.isEditorAvailable:
53+
case this.device?.status === 'stopped':
54+
// forces the loading animation
5255
return 'loading'
56+
default:
57+
return this.device.status
5358
}
54-
55-
return this.device.status
5659
}
5760
}
5861
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import semver from 'semver'
2+
import { computed, ref } from 'vue'
3+
import { useRouter } from 'vue-router'
4+
import { useStore } from 'vuex'
5+
6+
import deviceApi from '../api/devices.js'
7+
import Alerts from '../services/alerts.js'
8+
import Dialog from '../services/dialog.js'
9+
import { DeviceStateMutator } from '../utils/DeviceStateMutator.js'
10+
import { createPollTimer } from '../utils/timers.js'
11+
12+
// constants
13+
const POLL_TIME = 5000
14+
const deviceTransitionStates = [
15+
'loading',
16+
'installing',
17+
'starting',
18+
'stopping',
19+
'restarting',
20+
'suspending',
21+
'importing'
22+
]
23+
24+
export function useDeviceHelper () {
25+
const $store = useStore()
26+
const $router = useRouter()
27+
28+
let deviceStateMutator = null
29+
const device = ref(null)
30+
const pollTimer = createPollTimer(onPoll, POLL_TIME, false)
31+
32+
// duplicated functionality because the pollTimer is not reactive
33+
const isPolling = ref(false)
34+
35+
const isInTransitionState = computed(() => deviceTransitionStates.includes(device.value.status))
36+
37+
const agentSupportsDeviceAccess = computed(() =>
38+
device.value?.agentVersion && semver.gte(device.value?.agentVersion, '0.8.0')
39+
)
40+
const agentSupportsActions = computed(() =>
41+
device.value?.agentVersion && semver.gte(device.value?.agentVersion, '2.3.0')
42+
)
43+
44+
function startPolling () {
45+
isPolling.value = true
46+
pollTimer.start()
47+
}
48+
49+
function stopPolling () {
50+
isPolling.value = false
51+
pollTimer.stop()
52+
}
53+
54+
function resumePolling () {
55+
isPolling.value = true
56+
pollTimer.resume()
57+
}
58+
59+
function pausePolling () {
60+
isPolling.value = false
61+
pollTimer.pause()
62+
}
63+
64+
async function onPoll () {
65+
try {
66+
return await fetchDevice()
67+
} catch (err) {
68+
if (err.response?.status === 404) {
69+
stopPolling()
70+
}
71+
throw err
72+
}
73+
}
74+
75+
function preActionChecks (message) {
76+
if (device.value.agentVersion && !agentSupportsActions.value) {
77+
// if agent version is present but is less than required version, show warning and halt
78+
Alerts.emit('Device Agent V2.3 or greater is required to perform this action.', 'warning')
79+
return false
80+
}
81+
if (!message) {
82+
// no message means silent operation, no need to show confirmation
83+
return true
84+
}
85+
if (!device.value?.agentVersion) {
86+
// if agent version is missing, be optimistic and give it a go, but show warning
87+
Alerts.emit(`${message}. NOTE: The device agent version is not known, the action may timeout`, 'warning')
88+
} else {
89+
Alerts.emit(message, 'confirmation')
90+
}
91+
return true
92+
}
93+
94+
async function restartDevice () {
95+
const preCheckOk = preActionChecks('Restarting device...')
96+
if (!preCheckOk) {
97+
return
98+
}
99+
100+
deviceStateMutator.setStateOptimistically('restarting')
101+
102+
try {
103+
await deviceApi.restartDevice(device.value)
104+
deviceStateMutator.setStateAsPendingFromServer()
105+
} catch (err) {
106+
let message = 'Device restart request failed.'
107+
if (err.response?.data?.error) {
108+
message = err.response.data.error
109+
}
110+
console.warn(message, err)
111+
Alerts.emit(message, 'warning')
112+
}
113+
}
114+
115+
function bindDevice (binding, shouldStartPolling = false) {
116+
device.value = binding
117+
deviceStateMutator = new DeviceStateMutator(binding)
118+
if (shouldStartPolling) { startPolling() }
119+
}
120+
121+
async function fetchDevice (deviceId = null) {
122+
try {
123+
device.value = await deviceApi.getDevice(deviceId || device.value?.id)
124+
} catch (err) {
125+
if (err.status === 403) {
126+
stopPolling()
127+
128+
return $router.push({ name: 'device-overview' })
129+
}
130+
}
131+
}
132+
133+
function showDeleteDialog () {
134+
Dialog.show({
135+
header: 'Delete Device',
136+
kind: 'danger',
137+
text: 'Are you sure you want to delete this device? Once deleted, there is no going back.',
138+
confirmLabel: 'Delete'
139+
}, async () => {
140+
try {
141+
await deviceApi.deleteDevice(device.value.id)
142+
Alerts.emit('Successfully deleted the device', 'confirmation')
143+
// Trigger a refresh of team info to resync following device changes
144+
await $store.dispatch('account/refreshTeam')
145+
await $router.push({ name: 'TeamDevices', params: { team_slug: $store.state.account.team.slug } })
146+
} catch (err) {
147+
Alerts.emit('Failed to delete device: ' + err.toString(), 'warning', 7500)
148+
}
149+
})
150+
}
151+
152+
return {
153+
agentSupportsDeviceAccess,
154+
agentSupportsActions,
155+
device,
156+
isPolling,
157+
isInTransitionState,
158+
bindDevice,
159+
startPolling,
160+
stopPolling,
161+
pausePolling,
162+
resumePolling,
163+
restartDevice,
164+
fetchDevice,
165+
showDeleteDialog
166+
}
167+
}

0 commit comments

Comments
 (0)