Skip to content

Commit

Permalink
Merge pull request #1926 from flowforge/feat-1911-start-stop-instances
Browse files Browse the repository at this point in the history
Application Instances: Live statuses
  • Loading branch information
joepavitt committed Apr 11, 2023
2 parents e2f1223 + 0e907ab commit fd915f2
Show file tree
Hide file tree
Showing 28 changed files with 520 additions and 410 deletions.
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
// plugin:n
"n/file-extension-in-import": "error",
"n/no-missing-import": "error",
"n/no-missing-require": "error"
"n/no-missing-require": "error",

// plugin:promise
"promise/catch-or-return": ["error", { "allowFinally": true }]
},
"overrides": [
// Frontend runs in the browser and builds with webpack
Expand Down
2 changes: 1 addition & 1 deletion forge/containers/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ module.exports = {
await this._subscriptionHandler.requireTrialProjectOrActiveSubscription(project)
}
if (this._driver.restartFlows) {
this._driver.restartFlows(project, options)
await this._driver.restartFlows(project, options)
}
},
revokeUserToken: async (project, token) => { // logout:nodered(step-2)
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/api/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const getTeamProjects = async (teamId) => {
r.link = { name: 'Application', params: { id: r.id } }
promises.push(client.get(`/api/v1/projects/${r.id}`).then(p => {
r.status = p.data.meta.state
r.flowLastUpdatedSince = daysSince(p.data.flowLastUpdatedAt)
}).catch(err => {
console.error('not found', err)
r.status = 'stopped'
Expand Down Expand Up @@ -96,6 +97,13 @@ const getTeamApplications = async (teamId) => {
*/
const getTeamApplicationsInstanceStatuses = async (teamId) => {
const result = await client.get(`/api/v1/teams/${teamId}/applications/status`)

result.data.applications.forEach((application) => {
application.instances.forEach((instance) => {
instance.flowLastUpdatedSince = daysSince(instance.flowLastUpdatedAt)
})
})

return result.data
}

Expand Down
8 changes: 1 addition & 7 deletions frontend/src/components/DevicesBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@ export default {
deletingDevice: false,
nextCursor: null,
devices: new Map(),
checkInterval: null,
teamInstances: null
checkInterval: null
}
},
computed: {
Expand Down Expand Up @@ -380,10 +379,6 @@ export default {
this.loading = false
},
async updateTeamInstances () {
this.teamInstances = (await teamApi.getTeamProjects(this.team.id)).projects // TODO Currently fetches projects not instances
},
deviceAction (action, deviceId) {
const device = this.devices.get(deviceId)
if (action === 'edit') {
Expand Down Expand Up @@ -428,7 +423,6 @@ export default {
Alerts.emit('Successfully removed the device from the instance.', 'confirmation')
})
} else if (action === 'assignToProject') {
this.updateTeamInstances()
this.$refs.deviceAssignInstanceDialog.show(device)
}
}
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/components/InstanceStatusPolling.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script>
import InstanceApi from '../api/instances.js'
const instanceTransitionStates = [
'loading',
'installing',
'starting',
'stopping',
'restarting',
'suspending',
'importing'
]
export default {
props: {
instance: {
type: Object,
required: true
}
},
emits: ['instance-updated'],
data () {
return {
checkInterval: null,
checkWaitTime: 1000
}
},
watch: {
instance: 'checkForUpdateIfNeeded',
'instance.pendingStateChange': 'checkForUpdateIfNeeded',
'instance.meta.state': 'checkForUpdateIfNeeded'
},
mounted () {
this.checkForUpdateIfNeeded()
},
unmounted () {
clearTimeout(this.checkInterval)
},
methods: {
checkForUpdateIfNeeded () {
clearTimeout(this.checkInterval)
if (!this.shouldCheckForUpdate(this.instance)) {
this.checkWaitTime = 1000
return
}
this.scheduleUpdate(this.instance.id)
},
scheduleUpdate () {
this.checkInterval = setTimeout(async () => {
this.checkWaitTime *= 1.15
if (this.instance.id) {
const data = await InstanceApi.getInstance(this.instance.id)
this.$emit('instance-updated', data)
}
}, this.checkWaitTime)
},
shouldCheckForUpdate (instance) {
// Server has not received state change request yet, no need to check for update
if (instance.optimisticStateChange) {
return false
}
// If instance is in a transition state, check for update
if (instance.meta?.state && instanceTransitionStates.includes(instance.meta.state)) {
return true
}
// Otherwise, if instance is known to have a pending state change, check for update
if (instance.pendingStateChange) {
return true
}
return false
}
}
}
</script>
<!-- eslint-disable-next-line vue/valid-template-root -->
<template />
61 changes: 61 additions & 0 deletions frontend/src/components/StatusBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<div v-if="!status" class="forge-badge"><RefreshIcon class="w-4 h-4 animate-spin" /></div>
<div
v-else
class="forge-badge"
:class="['forge-status-' + status, pendingChange ? 'opacity-40' : '']"
>
<ExclamationCircleIcon v-if="status === 'error'" class="w-4 h-4" />
<ExclamationIcon v-if="status === 'suspended'" class="w-4 h-4" />
<PlayIcon v-if="status === 'running'" class="w-4 h-4" />
<StopIcon v-if="status === 'stopping' || status === 'suspending'" class="w-4 h-4" />
<RefreshIcon v-if="status === 'restarting'" class="w-4 h-4" />
<DownloadIcon v-if="status === 'importing'" class="w-4 h-4" />
<DotsCircleHorizontalIcon v-if="status === 'starting'" class="w-4 h-4" />
<CloudUploadIcon v-if="status === 'loading'" class="w-4 h-4" />
<CloudDownloadIcon v-if="status === 'installing'" class="w-4 h-4" />
<SupportIcon v-if="status === 'safe'" class="w-4 h-4" />
<span class="ml-1">{{ status }}</span>
</div>
</template>

<script>
import {
CloudDownloadIcon,
CloudUploadIcon,
DotsCircleHorizontalIcon,
DownloadIcon,
ExclamationCircleIcon,
ExclamationIcon,
PlayIcon,
RefreshIcon,
StopIcon,
SupportIcon
} from '@heroicons/vue/outline'
export default {
name: 'InstanceStatusBadge',
components: {
CloudDownloadIcon,
CloudUploadIcon,
DotsCircleHorizontalIcon,
DownloadIcon,
ExclamationCircleIcon,
ExclamationIcon,
PlayIcon,
RefreshIcon,
StopIcon,
SupportIcon
},
props: {
status: {
type: String,
default: null
},
pendingChange: {
type: Boolean,
default: false
}
}
}
</script>
2 changes: 1 addition & 1 deletion frontend/src/pages/application/Activity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default {
this.users = (await TeamAPI.getTeamMembers(this.team.id)).members
},
async loadEntries (params = new URLSearchParams(), cursor = undefined) {
const applicationId = this.application.id
const applicationId = this.application?.id
if (applicationId) {
// TODO Currently this filter effectively does nothing as each application contains only one instance
// And the API only supports one set of instance logs at a time regardless
Expand Down
7 changes: 1 addition & 6 deletions frontend/src/pages/application/Logs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/>
</ff-dropdown>
<router-link v-if="instance?.meta" :to="{ name: 'Instance', params: { id: instance.id }}">
<InstanceStatusBadge :status="instance.meta?.state" :pendingStateChange="instance.pendingStateChange" class="ml-2" />
<InstanceStatusBadge :instance="instance" class="ml-2" />
</router-link>
</div>
</template>
Expand Down Expand Up @@ -46,7 +46,6 @@ export default {
required: true
}
},
emits: ['instances-enable-polling', 'instances-disable-polling'],
data () {
return {
input: {
Expand All @@ -63,12 +62,8 @@ export default {
instances: 'selectFirstInstance'
},
mounted () {
this.$emit('instances-enable-polling')
this.selectFirstInstance()
},
unmounted () {
this.$emit('instances-disable-polling')
},
methods: {
selectFirstInstance () {
this.input.instanceId = this.instances[0]?.id
Expand Down
24 changes: 9 additions & 15 deletions frontend/src/pages/application/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@
#context-menu="{row}"
>
<ff-list-item
:disabled="row.pendingStateChange || row.projectRunning"
:disabled="row.pendingStateChange || row.running"
label="Start"
@click.stop="$emit('instance-start', row)"
/>
<ff-list-item
:disabled="!row.projectNotSuspended"
:disabled="!row.notSuspended"
label="Restart"
@click.stop="$emit('instance-restart', row)"
/>
<ff-list-item
:disabled="!row.projectNotSuspended"
:disabled="!row.notSuspended"
kind="danger"
label="Suspend"
@click.stop="$emit('instance-suspend', row)"
Expand Down Expand Up @@ -80,7 +80,7 @@ import SectionTopMenu from '../../components/SectionTopMenu.vue'
import permissionsMixin from '../../mixins/Permissions.js'
import InstanceStatusBadge from '../instance/components/InstanceStatusBadge.vue'
import InstanceEditorLink from '../instance/components/cells/InstanceEditorLink.vue'
import InstanceEditorLinkCell from '../instance/components/cells/InstanceEditorLink.vue'
import DeploymentName from './components/cells/DeploymentName.vue'
import LastSeen from './components/cells/LastSeen.vue'
Expand All @@ -103,23 +103,23 @@ export default {
required: true
}
},
emits: ['instance-delete', 'instance-suspend', 'instance-restart', 'instance-start', 'instances-enable-polling', 'instances-disable-polling'],
emits: ['instance-delete', 'instance-suspend', 'instance-restart', 'instance-start'],
computed: {
...mapState('account', ['team', 'teamMembership']),
cloudColumns () {
return [
{ label: 'Name', class: ['w-64'], component: { is: markRaw(DeploymentName), map: { disabled: 'editorDisabled' } } },
{ label: 'Name', class: ['w-64'], component: { is: markRaw(DeploymentName) } },
{ label: 'Instance Status', class: ['w-48'], component: { is: markRaw(InstanceStatusBadge), map: { status: 'meta.state' } } },
{ label: 'Last Deployed', class: ['w-48'], component: { is: markRaw(LastSeen), map: { lastSeenSince: 'flowLastUpdatedSince' } } },
{ label: 'Deployment Status', class: ['w-48'], component: { is: markRaw(InstanceStatusBadge), map: { status: 'meta.state' } } },
{ label: '', class: ['w-20'], component: { is: markRaw(InstanceEditorLink), map: { disabled: 'editorDisabled' } } }
{ label: '', class: ['w-20'], component: { is: markRaw(InstanceEditorLinkCell) } }
]
},
cloudRows () {
return this.instances.map((instance) => {
instance.running = instance.meta?.state === 'running'
instance.notSuspended = instance.meta?.state !== 'suspended'
instance.editorDisabled = !instance.running || this.isVisitingAdmin
instance.disabled = !instance.running || this.isVisitingAdmin
return instance
})
Expand All @@ -128,12 +128,6 @@ export default {
return this.teamMembership.role === Roles.Admin
}
},
mounted () {
this.$emit('instances-enable-polling')
},
unmounted () {
this.$emit('instances-disable-polling')
},
methods: {
selectedCloudRow (cloudInstance) {
this.$router.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<span
class="flex"
class="flex items-center"
>
<CloudIcon class="w-6 mr-2 text-gray-500" />
<div class="flex flex-col space-y-1">
Expand Down
Loading

0 comments on commit fd915f2

Please sign in to comment.