Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions forge/db/views/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = function (app) {
allOf: [{ $ref: 'SnapshotSummary' }]
},
status: { type: 'string' },
onlineStatus: { type: 'string', enum: ['online', 'offline', 'not-seen'] },
isDeploying: { type: 'boolean' },
agentVersion: { nullable: true, type: 'string' },
mode: { type: 'string' },
Expand All @@ -36,7 +37,7 @@ module.exports = function (app) {
localLoginEnabled: { type: 'boolean' }
},
// POST /devices/ extends this via allOf with credentials + meta.
required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'mode', 'isDeploying']
required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'onlineStatus', 'mode', 'isDeploying']
})

function device (device, { statusOnly = false } = {}) {
Expand All @@ -52,6 +53,7 @@ module.exports = function (app) {
lastSeenAt: result.lastSeenAt,
lastSeenMs: result.lastSeenAt ? (Date.now() - new Date(result.lastSeenAt).valueOf()) : null,
status: result.state || 'offline',
onlineStatus: device.status,
mode: result.mode || 'autonomous',
isDeploying: app.db.controllers.Device.isDeploying(device)
}
Expand All @@ -69,6 +71,7 @@ module.exports = function (app) {
targetSnapshot: app.db.views.ProjectSnapshot.snapshotSummary(device.targetSnapshot),
links: result.links,
status: result.state || 'offline',
onlineStatus: device.status,
agentVersion: result.agentVersion,
mode: result.mode || 'autonomous',
ownerType: result.ownerType,
Expand Down Expand Up @@ -106,14 +109,15 @@ module.exports = function (app) {
lastSeenAt: { nullable: true, type: 'string' },
lastSeenMs: { nullable: true, type: 'number' },
status: { type: 'string' },
onlineStatus: { type: 'string', enum: ['online', 'offline', 'not-seen'] },
mode: { type: 'string' },
isDeploying: { type: 'boolean' },
links: { $ref: 'LinksMeta' },
application: { $ref: 'ApplicationSummary' },
mostRecentAuditLogCreatedAt: { type: 'string' },
mostRecentAuditLogEvent: { type: 'string' }
},
required: ['id', 'ownerType', 'name', 'type', 'lastSeenAt', 'lastSeenMs', 'status', 'mode', 'isDeploying', 'links'],
required: ['id', 'ownerType', 'name', 'type', 'lastSeenAt', 'lastSeenMs', 'status', 'onlineStatus', 'mode', 'isDeploying', 'links'],
additionalProperties: false
})
app.addSchema({
Expand All @@ -134,6 +138,7 @@ module.exports = function (app) {
lastSeenAt: result.lastSeenAt,
lastSeenMs: result.lastSeenAt ? (Date.now() - new Date(result.lastSeenAt).valueOf()) : null,
status: result.state || 'offline',
onlineStatus: device.status,
mode: result.mode || 'autonomous',
isDeploying: app.db.controllers.Device.isDeploying(device),
links: result.links
Expand Down Expand Up @@ -163,11 +168,12 @@ module.exports = function (app) {
lastSeenAt: { nullable: true, type: 'string' },
lastSeenMs: { nullable: true, type: 'number' },
status: { type: 'string' },
onlineStatus: { type: 'string', enum: ['online', 'offline', 'not-seen'] },
mode: { type: 'string' },
isDeploying: { type: 'boolean' },
editor: { type: 'object', additionalProperties: true }
},
required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'mode', 'isDeploying'],
required: ['id', 'lastSeenAt', 'lastSeenMs', 'status', 'onlineStatus', 'mode', 'isDeploying'],
additionalProperties: false
})
app.addSchema({
Expand All @@ -186,6 +192,7 @@ module.exports = function (app) {
lastSeenAt: summary.lastSeenAt,
lastSeenMs: summary.lastSeenMs,
status: summary.status,
onlineStatus: summary.onlineStatus,
mode: summary.mode,
isDeploying: summary.isDeploying
}
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/components/DevicesBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,9 @@ import DeviceLink from '../pages/application/components/cells/DeviceLink.vue'
import Snapshot from '../pages/application/components/cells/Snapshot.vue'

import DeviceCreatedAtCell from '../pages/device/DeviceCreatedAtCell.vue'
import DeviceLastSeenCell from '../pages/device/components/DeviceLastSeenCell.vue'
import DeviceModeBadge from '../pages/device/components/DeviceModeBadge.vue'
import DeviceOnlineStatusCell from '../pages/device/components/DeviceOnlineStatusCell.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'
import DeviceCredentialsDialog from '../pages/team/Devices/dialogs/DeviceCredentialsDialog.vue'
Expand Down Expand Up @@ -478,9 +477,8 @@ export default {
{ label: 'Remote Instance', key: 'name', sortable: true, component: { is: markRaw(DeviceLink) } },
{ label: 'Type', key: 'type', class: ['w-48'], sortable: true },
{ label: 'Created', key: 'createdAt', class: ['w-48'], sortable: true, component: { is: markRaw(DeviceCreatedAtCell) } },
{ label: 'Last Seen', key: 'lastSeenAt', class: ['w-48'], sortable: true, component: { is: markRaw(DeviceLastSeenCell) } },
{ label: 'Mode', key: 'mode', class: ['w-30'], sortable: true, component: { is: markRaw(DeviceModeBadge) } },
{ label: 'Last Known Status', class: ['w-32'], component: { is: markRaw(InstanceStatusBadge), map: { instanceId: 'id' }, extraProps: { instanceType: 'device' } } }
{ label: 'Status', key: 'lastSeenAt', class: ['w-40'], sortable: true, component: { is: markRaw(DeviceOnlineStatusCell) } }
]

if (this.displayingTeam) {
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,23 @@
}
.forge-status-success,
.forge-status-connected,
.forge-status-running {
.forge-status-running,
.forge-status-online {
background-color: var(--ff-color-status-success-bg);
border-color: var(--ff-color-status-success-border);
color: var(--ff-color-status-success-text);
}
.forge-status-offline {
background-color: var(--ff-color-status-neutral-bg);
border-color: var(--ff-color-status-neutral-border);
color: var(--ff-color-status-neutral-text);
}
.forge-status-not-seen {
background-color: var(--ff-color-status-neutral-bg);
border-color: var(--ff-color-status-neutral-border);
color: var(--ff-color-status-neutral-text);
@apply border-dashed;
}

.forge-minimal-status-error,
.forge-minimal-status-crashed {
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/pages/device/components/DeviceOnlineStatusCell.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<div
v-ff-tooltip:left="tooltip"
class="forge-badge"
:class="'forge-status-' + onlineStatus"
:data-el="`device-online-status-${onlineStatus}`"
>
<SignalIcon v-if="onlineStatus === 'online'" class="w-4 h-4" />
<SignalSlashIcon v-else-if="onlineStatus === 'offline'" class="w-4 h-4" />
<MinusCircleIcon v-else class="w-4 h-4" />
<span class="ml-1 whitespace-nowrap">{{ label }}</span>
</div>
</template>

<script setup lang="ts">
import { MinusCircleIcon, SignalIcon, SignalSlashIcon } from '@heroicons/vue/24/outline'
import { computed } from 'vue'

import { capitalize } from '../../../composables/strings/String.js'
import daysSince from '../../../utils/daysSince'

type OnlineStatus = 'online' | 'offline' | 'not-seen'

const props = withDefaults(defineProps<{
onlineStatus?: OnlineStatus
status?: string | null
lastSeenAt?: string | null
}>(), {
onlineStatus: 'not-seen',
status: null,
lastSeenAt: null
})

const LABELS: Record<OnlineStatus, string> = {
online: 'Online',
offline: 'Offline',
'not-seen': 'Never Seen'
}

const label = computed(() => LABELS[props.onlineStatus] || 'Unknown')

const tooltip = computed(() => {
if (props.onlineStatus === 'not-seen' || !props.lastSeenAt) {
return 'Never seen'
}
const reported = props.status ? capitalize(props.status) : 'Unknown'
return `Last seen: ${daysSince(props.lastSeenAt, true)} · Last reported: ${reported}`
})
</script>
6 changes: 6 additions & 0 deletions frontend/src/types/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10394,6 +10394,8 @@ export interface components {
activeSnapshot?: components["schemas"]["SnapshotSummary"] | null;
targetSnapshot?: components["schemas"]["SnapshotSummary"] | null;
status: string;
/** @enum {string} */
onlineStatus: "online" | "offline" | "not-seen";
isDeploying: boolean;
agentVersion?: string | null;
mode: string;
Expand All @@ -10417,6 +10419,8 @@ export interface components {
lastSeenAt: string | null;
lastSeenMs: number | null;
status: string;
/** @enum {string} */
onlineStatus: "online" | "offline" | "not-seen";
mode: string;
isDeploying: boolean;
links: components["schemas"]["LinksMeta"];
Expand All @@ -10432,6 +10436,8 @@ export interface components {
lastSeenAt: string | null;
lastSeenMs: number | null;
status: string;
/** @enum {string} */
onlineStatus: "online" | "offline" | "not-seen";
mode: string;
isDeploying: boolean;
editor?: {
Expand Down
Loading