Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement API for Device Groups #3157

Merged
merged 104 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
477b6cd
Add migration for DeviceGroups
Steve-Mcl Dec 2, 2023
8581dff
Add DeviceGroups model
Steve-Mcl Dec 2, 2023
a825fbe
Add App Device Groups API
Steve-Mcl Dec 2, 2023
518138d
DeviceGroups API tests
Steve-Mcl Dec 2, 2023
9a8e72e
Add createApplicationDeviceGroup support method
Steve-Mcl Dec 2, 2023
ef462b2
Add DeviceGroup db view
Steve-Mcl Dec 3, 2023
b423241
re-order DeviceGroup index position
Steve-Mcl Dec 3, 2023
be47ffc
add DeviceGroup controller
Steve-Mcl Dec 3, 2023
9420f14
delete erronous import
Steve-Mcl Dec 3, 2023
8b61c24
Update TestModelFactory to use description instead
Steve-Mcl Dec 3, 2023
207f619
load DeviceGroup controller
Steve-Mcl Dec 4, 2023
7254305
fix failing postgres tests (byId sub-clause)
Steve-Mcl Dec 4, 2023
97ae431
improve test coverage
Steve-Mcl Dec 4, 2023
dbab7ef
Test device group pagination and 404 handling
Steve-Mcl Dec 4, 2023
8084693
Update forge/db/models/DeviceGroup.js
Steve-Mcl Dec 4, 2023
bee3280
extract literals to single common literal
Steve-Mcl Dec 4, 2023
20e7bfb
correct clause for device include
Steve-Mcl Dec 4, 2023
f854a42
Cascade deletion of Application to DeviceGroup
Steve-Mcl Dec 4, 2023
70047f4
Enable schemas
Steve-Mcl Dec 4, 2023
46fa018
remove helpful stuff
Steve-Mcl Dec 4, 2023
765a826
Add frontend APIs
Steve-Mcl Dec 8, 2023
94902b1
add graphics/icons
Steve-Mcl Dec 8, 2023
65a4724
add device group management pages
Steve-Mcl Dec 8, 2023
2abfd62
hyphenate devicegroup
Steve-Mcl Dec 8, 2023
9ee7c23
Let the model validate fields and bubble up
Steve-Mcl Dec 8, 2023
d9d7a5a
corrections to device group view schemas
Steve-Mcl Dec 8, 2023
177cce0
fix double blank line lint err
Steve-Mcl Dec 8, 2023
311e523
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 8, 2023
687a9e0
updates for devicegroup hyphenation
Steve-Mcl Dec 8, 2023
f6f48cd
include device.type for devicegroup manage tables
Steve-Mcl Dec 8, 2023
57672c2
include Device Group in Device for group management
Steve-Mcl Dec 8, 2023
262f00b
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 8, 2023
6d001bf
include deviceGroupSummary for group management
Steve-Mcl Dec 8, 2023
1c7a521
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 8, 2023
bca23aa
improve table layout
Steve-Mcl Dec 8, 2023
51ab25f
use deviceGroup for filtering group management
Steve-Mcl Dec 8, 2023
d34b22f
update concepts with Device Groups info
Steve-Mcl Dec 11, 2023
d3a826b
add application device groups FE API tests
Steve-Mcl Dec 11, 2023
65016ee
add missing FE test `getDeviceGroups`
Steve-Mcl Dec 11, 2023
493ae0c
Change doc tag from "Applications" to "Application Device Group"
Steve-Mcl Dec 15, 2023
3cbb137
Move Device Groups tests to own file
Steve-Mcl Dec 15, 2023
66667ca
Merge branch 'main' into 2997-device-groups-backend
Steve-Mcl Dec 15, 2023
6eda38a
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 15, 2023
fdefa11
move to Enterprise tier
Steve-Mcl Dec 15, 2023
b91634b
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 15, 2023
9197dae
move Device Groups to enterprise tier
Steve-Mcl Dec 15, 2023
e1a2cbd
Make device group "devices" default page
Steve-Mcl Dec 15, 2023
eb5c684
Only show available devices in "edit mode"
Steve-Mcl Dec 15, 2023
820dcb7
dont show check box col in non-edit mode
Steve-Mcl Dec 15, 2023
bafd32c
remove redundant code clean up
Steve-Mcl Dec 16, 2023
f72f943
table header oddity on tab change - fix
Steve-Mcl Dec 16, 2023
a3e8bfa
ensure transaction object is passed to updates
Steve-Mcl Dec 17, 2023
4642167
ensure group icon color correctly inherits style
Steve-Mcl Dec 18, 2023
2b4f26a
Create through table for device groups in pipeline
Steve-Mcl Dec 18, 2023
3a41862
backend plumbing for pipeline device groups
Steve-Mcl Dec 18, 2023
2135610
setup frontend apis for pipeline device groups
Steve-Mcl Dec 18, 2023
ff9f211
support info/warning/success in the StatusBadge for generalised use
Steve-Mcl Dec 18, 2023
2713390
implement front end for pipeline device groups
Steve-Mcl Dec 18, 2023
0f9aad2
update concepts docs for pipeline device groups
Steve-Mcl Dec 18, 2023
ec6e13e
correction to hero title when creating new pipeline
Steve-Mcl Dec 18, 2023
fd23685
fix some texts not displaying when playing a pipeline
Steve-Mcl Dec 18, 2023
fde19b5
fix polling - correction to event hookups
Steve-Mcl Dec 18, 2023
ff3cfdd
fix icon fill color inheritance
Steve-Mcl Dec 18, 2023
5ffe19f
revert temp debugging options
Steve-Mcl Dec 18, 2023
31c42c9
refactor pipeline stage updates and common error handling
Steve-Mcl Dec 18, 2023
617dd44
add model validator to ensure device groups in same app
Steve-Mcl Dec 18, 2023
d7fc2fd
update unit tests
Steve-Mcl Dec 18, 2023
2f94f04
check device-groups feature enabled
Steve-Mcl Dec 18, 2023
2694276
fix UI test - differentiate "Device" from "Device Group"
Steve-Mcl Dec 18, 2023
666440c
Add device group frontend e2e tests
Steve-Mcl Dec 18, 2023
e78ded9
Merge branch '2997-device-group-frontend' into 2989-assign-device-gro…
Steve-Mcl Dec 18, 2023
36a69f1
revert unnecessary change (not needed)
Steve-Mcl Dec 18, 2023
15c2772
really fix e2e test - only click the button without "Group"
Steve-Mcl Dec 18, 2023
ca2a0de
remove extraneous it.only
Steve-Mcl Dec 18, 2023
2d14a40
remove duplicate tests left over from splitting out to own file
Steve-Mcl Dec 18, 2023
14e4e8a
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 18, 2023
0a642a7
Merge branch '2997-device-group-frontend' into 2989-assign-device-gro…
Steve-Mcl Dec 18, 2023
fb419c4
walk pipeline stages, dont assume id order is correct!
Steve-Mcl Dec 19, 2023
a95acfa
fix intermittent PG test fails
Steve-Mcl Dec 19, 2023
ccb05c0
refactor sortStages to model static fn & add unit tests
Steve-Mcl Dec 19, 2023
af7d8f2
fix PG test bugs - ensure we wait the correct thing
Steve-Mcl Dec 19, 2023
a76f343
Merge branch 'main' into 2997-device-groups-backend
Steve-Mcl Dec 19, 2023
97c3ed0
Merge branch '2997-device-groups-backend' into 2997-device-group-fron…
Steve-Mcl Dec 19, 2023
bef6d99
Merge branch '2997-device-group-frontend' into 2989-assign-device-gro…
Steve-Mcl Dec 19, 2023
1d05674
Add device group feature flag in TeamType
knolleary Dec 19, 2023
d307835
Enable deviceGroups feature on test team type
knolleary Dec 19, 2023
8ca537e
Add device group events to audit log
Steve-Mcl Dec 19, 2023
7f7939f
Ensure deviceGroup team flag is passed up
knolleary Dec 19, 2023
d524e15
Clarify team-loading behaviour
knolleary Dec 20, 2023
bd9c7c2
Add loading graphic to device groups page
knolleary Dec 20, 2023
2ced020
Ensure team is loaded when loading application
knolleary Dec 20, 2023
390291b
Merge pull request #3235 from FlowFuse/2989-device-group-team-tier-en…
knolleary Dec 20, 2023
386a54e
Add audit logger for application.pipeline.stage-deployed
Steve-Mcl Dec 20, 2023
cf597c8
Add call to audit log for pipeline stage deployment
Steve-Mcl Dec 20, 2023
295f68d
add ui element
Steve-Mcl Dec 20, 2023
0a1110a
add all missing tests for team.application.* audit loggers
Steve-Mcl Dec 20, 2023
ef99669
Merge pull request #3239 from FlowFuse/2989-add-missing-audit-logger-…
Pezmc Dec 20, 2023
5b11555
Merge pull request #3229 from FlowFuse/2989-assign-device-group-to-pi…
knolleary Dec 20, 2023
137e2b8
Merge pull request #3192 from FlowFuse/2997-device-group-frontend
knolleary Dec 20, 2023
49aa7fa
Merge branch 'main' into 2997-device-groups-backend
knolleary Dec 20, 2023
f60cf2f
Update and rename 20231202-01-add-device-devicegroup.js to 20231220-0…
knolleary Dec 20, 2023
c29af2d
Move DeviceGroup controller function to DG Controller
knolleary Dec 20, 2023
8af208d
Merge branch '2997-device-groups-backend' into 2997-device-group-audi…
Steve-Mcl Dec 20, 2023
4796eca
Merge pull request #3236 from FlowFuse/2997-device-group-auditloggers
Pezmc Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/user/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,12 @@ the remote device.
To further simplify device registration, Provisioning Tokens can be created to allow
devices to automatically connect to a team without having to manually register them first.
The token can also be configured to assign a device directly to a Node-RED instance within the team.

### Device Groups

**Introduced in FlowFuse 1.15**

Device groups allow you to organise your Application devices into logical groups.
These groups can be the target of [DevOps Pipelines](#devops-pipeline) greatly simplifying
the deployments to one or hundreds of devices.

14 changes: 14 additions & 0 deletions forge/auditLog/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ module.exports = {
await log('application.device.snapshot.device-target-set', actionedBy, application?.id, generateBody({ error, device, snapshot }))
}
}
},
deviceGroup: {
async created (actionedBy, error, application, deviceGroup) {
await log('application.deviceGroup.created', actionedBy, application?.id, generateBody({ error, application, deviceGroup }))
},
async updated (actionedBy, error, application, deviceGroup, updates) {
await log('application.deviceGroup.updated', actionedBy, application?.id, generateBody({ error, application, deviceGroup, updates }))
},
async deleted (actionedBy, error, application, deviceGroup) {
await log('application.deviceGroup.deleted', actionedBy, application?.id, generateBody({ error, application, deviceGroup }))
},
async membersChanged (actionedBy, error, application, deviceGroup, updates, info) {
await log('application.deviceGroup.members.changed', actionedBy, application?.id, generateBody({ error, application, deviceGroup, updates, info }))
}
}
}

Expand Down
22 changes: 19 additions & 3 deletions forge/auditLog/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const isObject = (obj) => {
/**
* Generate a standard format body for the audit log display and database.
* Any items null or missing must not generate a property in the body
* @param {{ error?, team?, project?, sourceProject?, targetProject?, device?, sourceDevice?, targetDevice?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, role?, projectType?, info? } == {}} objects objects to include in body
* @returns {{ error?, team?, project?, sourceProject?, targetProject?, device?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, role?, projectType? info? }
* @param {{ error?, team?, project?, sourceProject?, targetProject?, device?, sourceDevice?, targetDevice?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, pipeline?, pipelineStage?, pipelineStageTarget?, role?, projectType?, info?, deviceGroup?, interval?, threshold? } == {}} objects objects to include in body
* @returns {{ error?, team?, project?, sourceProject?, targetProject?, device?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, pipeline?, pipelineStage?, pipelineStageTarget?, role?, projectType? info?, deviceGroup?, interval?, threshold? }}
*/
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, sourceDevice, targetDevice, user, stack, billingSession, subscription, license, updates, snapshot, pipeline, pipelineStage, role, projectType, info, interval, threshold } = {}) => {
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, sourceDevice, targetDevice, user, stack, billingSession, subscription, license, updates, snapshot, pipeline, pipelineStage, pipelineStageTarget, role, projectType, info, deviceGroup, interval, threshold } = {}) => {
const body = {}

if (isObject(error) || typeof error === 'string') {
Expand Down Expand Up @@ -69,6 +69,9 @@ const generateBody = ({ error, team, application, project, sourceProject, target
if (isObject(pipelineStage)) {
body.pipelineStage = pipelineStageObject(pipelineStage)
}
if (isObject(pipelineStageTarget)) {
body.pipelineStageTarget = pipelineStageObject(pipelineStageTarget)
}
if (isObject(role) || typeof role === 'number') {
body.role = roleObject(role)
}
Expand All @@ -80,6 +83,9 @@ const generateBody = ({ error, team, application, project, sourceProject, target
} else if (isStringWithLength(info)) {
body.info = { info }
}
if (isObject(deviceGroup)) {
body.deviceGroup = deviceGroupObject(deviceGroup)
}

if (interval) {
body.interval = interval
Expand Down Expand Up @@ -146,12 +152,14 @@ const formatLogEntry = (auditLogDbRow) => {
snapshot: body?.snapshot,
updates: body?.updates,
device: body?.device,
deviceGroup: body?.deviceGroup,
sourceDevice: body?.sourceDevice,
targetDevice: body?.targetDevice,
projectType: body?.projectType,
info: body?.info,
pipeline: body?.pipeline,
pipelineStage: body?.pipelineStage,
pipelineStageTarget: body?.pipelineStageTarget,
interval: body?.interval,
threshold: body?.threshold
})
Expand Down Expand Up @@ -306,6 +314,13 @@ const projectTypeObject = (projectType) => {
name: projectType?.name || null
}
}
const deviceGroupObject = (deviceGroup) => {
return {
id: deviceGroup?.id || null,
hashid: deviceGroup?.hashid || null,
name: deviceGroup?.name || null
}
}
/**
* Generates the `trigger` part of the audit log report
* @param {object|number|'system'} actionedBy A user object or a user id. NOTE: 0 or 'system' can be used to indicate "system" triggered the event
Expand Down Expand Up @@ -573,6 +588,7 @@ module.exports = {
teamObject,
projectObject,
deviceObject,
deviceGroupObject,
userObject,
stackObject,
billingSessionObject,
Expand Down
3 changes: 3 additions & 0 deletions forge/auditLog/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ module.exports = {
},
async stageAdded (actionedBy, error, team, application, pipeline, pipelineStage) {
await log('application.pipeline.stage-added', actionedBy, team?.id, generateBody({ error, team, application, pipeline, pipelineStage }))
},
async stageDeployed (actionedBy, error, team, application, pipeline, sourcePipelineStage, targetPipelineStage) {
await log('application.pipeline.stage-deployed', actionedBy, team?.id, generateBody({ error, team, application, pipeline, pipelineStage: sourcePipelineStage, pipelineStageTarget: targetPipelineStage }))
}
}
}
Expand Down
62 changes: 62 additions & 0 deletions forge/db/migrations/20231220-01-add-device-devicegroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable no-unused-vars */
/**
* Add DeviceGroups table that has an FK association with Applications and Devices
*/

const { Sequelize, QueryInterface } = require('sequelize')

module.exports = {
/**
* upgrade database
* @param {QueryInterface} context QueryInterface
*/
up: async (context) => {
await context.createTable('DeviceGroups', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING(255),
allowNull: false
},
description: {
type: Sequelize.TEXT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
},
ApplicationId: {
type: Sequelize.INTEGER,
references: {
model: 'Applications',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
}
})

// add a column to Devices table that references DeviceGroups
await context.addColumn('Devices', 'DeviceGroupId', {
type: Sequelize.INTEGER,
references: {
model: 'DeviceGroups',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
})
},

down: async (queryInterface, Sequelize) => {

}
}
1 change: 1 addition & 0 deletions forge/db/models/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
this.hasMany(M.Project)
this.hasMany(M.Project, { as: 'Instances' })
this.belongsTo(M.Team, { foreignKey: { allowNull: false } })
this.hasMany(M.DeviceGroup, { onDelete: 'CASCADE' })
},
finders: function (M) {
return {
Expand Down
11 changes: 9 additions & 2 deletions forge/db/models/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
ownerType: {
type: DataTypes.VIRTUAL(DataTypes.ENUM('instance', 'application', null)),
get () {
return this.Project?.id ? 'instance' : (this.Application?.hashid ? 'application' : null)
return this.ProjectId ? 'instance' : (this.ApplicationId ? 'application' : null)
}
},
isApplicationOwned: {
Expand All @@ -53,6 +53,7 @@ module.exports = {
this.belongsTo(M.ProjectSnapshot, { as: 'activeSnapshot' })
this.hasMany(M.DeviceSettings)
this.hasMany(M.ProjectSnapshot) // associate device at application level with snapshots
this.belongsTo(M.DeviceGroup, { foreignKey: { allowNull: true } }) // SEE: forge/db/models/DeviceGroup.js for the other side of this relationship
},
hooks: function (M, app) {
return {
Expand Down Expand Up @@ -208,7 +209,7 @@ module.exports = {
]
})
},
getAll: async (pagination = {}, where = {}, { includeInstanceApplication = false } = {}) => {
getAll: async (pagination = {}, where = {}, { includeInstanceApplication = false, includeDeviceGroup = false } = {}) => {
// Pagination
const limit = Math.min(parseInt(pagination.limit) || 100, 100)
if (pagination.cursor) {
Expand Down Expand Up @@ -307,6 +308,12 @@ module.exports = {
}
]

if (includeDeviceGroup) {
includes.push({
model: M.DeviceGroup,
attributes: ['hashid', 'id', 'name', 'description', 'ApplicationId']
})
}
const statusOnlyIncludes = projectInclude.include?.where ? [projectInclude] : []

const [rows, count] = await Promise.all([
Expand Down
117 changes: 117 additions & 0 deletions forge/db/models/DeviceGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* A DeviceGroup.
* A logical grouping of devices for the primary intent of group deployments in the pipeline stages.
* @namespace forge.db.models.DeviceGroup
*/

const { DataTypes, literal } = require('sequelize')

const { buildPaginationSearchClause } = require('../utils')
const nameValidator = { msg: 'Device Group name cannot be empty' }

module.exports = {
name: 'DeviceGroup',
schema: {
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: nameValidator,
notNull: nameValidator
}
},
description: { type: DataTypes.TEXT }
},
associations: function (M) {
this.belongsTo(M.Application, { onDelete: 'CASCADE' })
this.hasMany(M.Device)
},
finders: function (M) {
const self = this
const deviceCountLiteral = literal(`(
SELECT COUNT(*)
FROM "Devices" AS "device"
WHERE
"device"."DeviceGroupId" = "DeviceGroup"."id"
AND
"device"."ApplicationId" = "DeviceGroup"."ApplicationId"
)`)
return {
static: {
byId: async function (id) {
if (typeof id === 'string') {
id = M.DeviceGroup.decodeHashid(id)
}
// find one, include application, devices and device count
return self.findOne({
where: { id },
include: [
{ model: M.Application, attributes: ['hashid', 'id', 'name', 'TeamId'] },
{
model: M.Device,
attributes: ['hashid', 'id', 'name', 'type', 'TeamId', 'ApplicationId', 'ProjectId', 'ownerType'],
where: {
ApplicationId: literal('"Devices"."ApplicationId" = "Application"."id"')
},
required: false
}
],
attributes: {
include: [
[
deviceCountLiteral,
'deviceCount'
]
]
}
})
},
getAll: async (pagination = {}, where = {}) => {
const limit = parseInt(pagination.limit) || 1000
if (pagination.cursor) {
pagination.cursor = M.DeviceGroup.decodeHashid(pagination.cursor)
}
if (where.ApplicationId && typeof where.ApplicationId === 'string') {
where.ApplicationId = M.Application.decodeHashid(where.ApplicationId)
}
const [rows, count] = await Promise.all([
this.findAll({
where: buildPaginationSearchClause(pagination, where, ['DeviceGroup.name', 'DeviceGroup.description']),
attributes: {
include: [
[
deviceCountLiteral,
'deviceCount'
]
]
},
order: [['id', 'ASC']],
limit
}),
this.count({ where })
])
return {
meta: {
next_cursor: rows.length === limit ? rows[rows.length - 1].hashid : undefined
},
count,
groups: rows
}
}
},
instance: {
deviceCount: async function () {
return await M.Device.count({ where: { DeviceGroupId: this.id, ApplicationId: this.ApplicationId } })
},
getDevices: async function () {
return await M.Device.findAll({
where: {
DeviceGroupId: this.id,
ApplicationId: this.ApplicationId
}
})
}
}
}
}
}
1 change: 1 addition & 0 deletions forge/db/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const modelTypes = [
'AccessToken',
'AuthClient',
'Device',
'DeviceGroup',
'DeviceSettings',
'StorageFlow',
'StorageCredentials',
Expand Down
13 changes: 9 additions & 4 deletions forge/db/views/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ module.exports = function (app) {
team: { $ref: 'TeamSummary' },
instance: { $ref: 'InstanceSummary' },
application: { $ref: 'ApplicationSummary' },
editor: { type: 'object', additionalProperties: true }
editor: { type: 'object', additionalProperties: true },
deviceGroup: {
nullable: true,
allOf: [{ $ref: 'DeviceGroupSummary' }]
}
}
})

Expand All @@ -36,7 +40,7 @@ module.exports = function (app) {
return null
}

const result = device.toJSON()
const result = device.toJSON ? device.toJSON() : device

if (statusOnly) {
return {
Expand Down Expand Up @@ -64,7 +68,8 @@ module.exports = function (app) {
agentVersion: result.agentVersion,
mode: result.mode || 'autonomous',
ownerType: result.ownerType,
isDeploying: app.db.controllers.Device.isDeploying(device)
isDeploying: app.db.controllers.Device.isDeploying(device),
deviceGroup: device.DeviceGroup && app.db.views.DeviceGroup.deviceGroupSummary(device.DeviceGroup)
}
if (device.Team) {
filtered.team = app.db.views.Team.teamSummary(device.Team)
Expand Down Expand Up @@ -104,7 +109,7 @@ module.exports = function (app) {
})
function deviceSummary (device, { includeSnapshotIds = false } = {}) {
if (device) {
const result = device.toJSON()
const result = device.toJSON ? device.toJSON() : device
const filtered = {
id: result.hashid,
ownerType: result.ownerType,
Expand Down