Browse Source

feat(iot-hub): add device package install tracking with entity cleanup

Backend:
- Expand DeviceInstalledItemDescriptor with createdEntityIds and dashboardId
- Add POST /api/iot-hub/device/register endpoint to save installed item
- Implement reverse-order entity deletion for DEVICE uninstall
- Update DEVICE case in updateItemVersion to clean old entities

Frontend:
- Expand DeviceInstalledItemDescriptor TS model
- Add registerDeviceInstall API method
- Wire wizard to collect entity IDs and register after completion
- Enable dashboard navigation for installed device packages
pull/15347/head
Andrii Shvaika 6 months ago
parent
commit
9e75cb2e29
  1. 10
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  2. 89
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
  4. 7
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java
  5. 12
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  6. 53
      ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts
  7. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts
  8. 2
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts

10
application/src/main/java/org/thingsboard/server/controller/IotHubController.java

@ -31,6 +31,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -64,6 +65,15 @@ public class IotHubController extends BaseController {
return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request); return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request);
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/device/register")
@ResponseBody
public InstallItemVersionResult registerDeviceInstall(
@RequestParam String versionId,
@RequestBody DeviceInstalledItemDescriptor descriptor) throws ThingsboardException {
return iotHubService.registerDeviceInstall(getCurrentUser(), versionId, descriptor);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/installedItems/{installedItemId}/update/{versionId}") @PostMapping("/installedItems/{installedItemId}/update/{versionId}")
@ResponseBody @ResponseBody

89
application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java

@ -24,8 +24,12 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.CalculatedFieldInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.CalculatedFieldInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor;
@ -45,12 +49,14 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService;
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.rule.TbRuleChainService;
@ -58,6 +64,8 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -80,6 +88,8 @@ public class DefaultIotHubService implements IotHubService {
private final DashboardService dashboardService; private final DashboardService dashboardService;
private final CalculatedFieldService calculatedFieldService; private final CalculatedFieldService calculatedFieldService;
private final DeviceProfileService deviceProfileService; private final DeviceProfileService deviceProfileService;
private final DeviceService deviceService;
private final TbDeviceService tbDeviceService;
private final SolutionService solutionService; private final SolutionService solutionService;
@Override @Override
@ -283,7 +293,12 @@ public class DefaultIotHubService implements IotHubService {
case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData); case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData);
case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData); case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData);
case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData); case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData);
case "DEVICE" -> updateDeviceProfile(user, tenantId, fileData); case "DEVICE" -> {
DeviceInstalledItemDescriptor dd = (DeviceInstalledItemDescriptor) descriptor;
deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user);
dd.setCreatedEntityIds(null);
dd.setDashboardId(null);
}
case "SOLUTION_TEMPLATE" -> { case "SOLUTION_TEMPLATE" -> {
SolutionTemplateInstalledItemDescriptor stDescriptor = (SolutionTemplateInstalledItemDescriptor) descriptor; SolutionTemplateInstalledItemDescriptor stDescriptor = (SolutionTemplateInstalledItemDescriptor) descriptor;
solutionService.deleteSolution(tenantId, stDescriptor.getCreatedEntityIds(), user); solutionService.deleteSolution(tenantId, stDescriptor.getCreatedEntityIds(), user);
@ -474,6 +489,74 @@ public class DefaultIotHubService implements IotHubService {
} }
} }
@Override
public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) {
TenantId tenantId = user.getTenantId();
log.info("[{}] Registering device package install for version: {}", tenantId, versionId);
try {
JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
String itemName = versionInfo.get("name").asText();
UUID itemId = UUID.fromString(versionInfo.get("itemId").asText());
String version = versionInfo.get("version").asText();
IotHubInstalledItem installedItem = new IotHubInstalledItem();
installedItem.setTenantId(tenantId);
installedItem.setItemId(itemId);
installedItem.setItemVersionId(UUID.fromString(versionId));
installedItem.setItemName(itemName);
installedItem.setItemType("DEVICE");
installedItem.setVersion(version);
installedItem.setDescriptor(descriptor);
iotHubInstalledItemService.save(tenantId, installedItem);
iotHubRestClient.reportVersionInstalled(versionId);
log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version);
return InstallItemVersionResult.success(descriptor);
} catch (Exception e) {
log.error("[{}] Failed to register device package install: {}", tenantId, versionId, e);
return InstallItemVersionResult.error(e.getMessage());
}
}
private void deleteDevicePackageEntities(TenantId tenantId, List<EntityId> createdEntityIds, SecurityUser user) {
if (createdEntityIds == null || createdEntityIds.isEmpty()) {
return;
}
List<EntityId> reversed = new ArrayList<>(createdEntityIds);
Collections.reverse(reversed);
for (EntityId entityId : reversed) {
try {
deleteDevicePackageEntity(tenantId, entityId, user);
} catch (Exception e) {
log.error("[{}] Failed to delete device package entity: {}", tenantId, entityId, e);
}
}
}
private void deleteDevicePackageEntity(TenantId tenantId, EntityId entityId, SecurityUser user) {
switch (entityId.getEntityType()) {
case DEVICE -> {
var device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId()));
if (device != null) tbDeviceService.delete(device, user);
}
case DEVICE_PROFILE -> {
var profile = deviceProfileService.findDeviceProfileById(tenantId, new DeviceProfileId(entityId.getId()));
if (profile != null) tbDeviceProfileService.delete(profile, user);
}
case DASHBOARD -> {
var dashboard = dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId()));
if (dashboard != null) tbDashboardService.delete(dashboard, user);
}
case RULE_CHAIN -> {
var ruleChain = ruleChainService.findRuleChainById(tenantId, new RuleChainId(entityId.getId()));
if (ruleChain != null) tbRuleChainService.delete(ruleChain, user);
}
default -> log.warn("[{}] Unsupported entity type for device package delete: {}", tenantId, entityId.getEntityType());
}
}
@Override @Override
public void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId) { public void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId) {
TenantId tenantId = user.getTenantId(); TenantId tenantId = user.getTenantId();
@ -505,8 +588,8 @@ public class DefaultIotHubService implements IotHubService {
tbRuleChainService.delete(ruleChain, user); tbRuleChainService.delete(ruleChain, user);
} }
} }
} else if (descriptor instanceof DeviceInstalledItemDescriptor) { } else if (descriptor instanceof DeviceInstalledItemDescriptor dd) {
// no entity to delete for now deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user);
} else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) { } else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) {
solutionService.deleteSolution(tenantId, st.getCreatedEntityIds(), user); solutionService.deleteSolution(tenantId, st.getCreatedEntityIds(), user);
} }

3
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.iot_hub;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
public interface IotHubService { public interface IotHubService {
@ -26,5 +27,7 @@ public interface IotHubService {
UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request); UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request);
InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor);
void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId); void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId);
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java

@ -16,8 +16,15 @@
package org.thingsboard.server.common.data.iot_hub; package org.thingsboard.server.common.data.iot_hub;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
@Data @Data
public class DeviceInstalledItemDescriptor implements IotHubInstalledItemDescriptor { public class DeviceInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private List<EntityId> createdEntityIds;
private DashboardId dashboardId;
} }

12
ui-ngx/src/app/core/http/iot-hub-api.service.ts

@ -112,6 +112,18 @@ export class IotHubApiService {
); );
} }
public registerDeviceInstall(
versionId: string,
descriptor: { createdEntityIds: { entityType: string; id: string }[]; dashboardId?: { id: string } },
config?: IotHubRequestConfig
): Observable<InstallItemVersionResult> {
return this.http.post<InstallItemVersionResult>(
`/api/iot-hub/device/register?versionId=${versionId}`,
descriptor,
{ params: this.buildParams(config) }
);
}
public installItemVersion(versionId: string, config?: IotHubRequestConfig, data?: any): Observable<InstallItemVersionResult> { public installItemVersion(versionId: string, config?: IotHubRequestConfig, data?: any): Observable<InstallItemVersionResult> {
return this.http.post<InstallItemVersionResult>( return this.http.post<InstallItemVersionResult>(
`/api/iot-hub/versions/${versionId}/install`, `/api/iot-hub/versions/${versionId}/install`,

53
ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts

@ -371,15 +371,22 @@ export class TbDeviceInstallDialogComponent implements OnInit {
} }
} }
// All done — report install // All done — register install with created entity IDs
ws.progressDone = true; ws.progressDone = true;
ws.completed = true; ws.completed = true;
try { try {
const createdEntityIds = this.collectCreatedEntityIds();
const dashboardId = this.findCreatedDashboardId();
await firstValueFrom( await firstValueFrom(
this.data.iotHubApiService.reportVersionInstalled(this.data.item.id as string, { ignoreLoading: true }) this.data.iotHubApiService.registerDeviceInstall(
this.data.item.id as string,
{ createdEntityIds, dashboardId },
{ ignoreLoading: true }
)
); );
} catch (_e) { } catch (_e) {
// Non-critical // Non-critical — entities are created, tracking registration failed
console.error('Failed to register device install', _e);
} }
// Auto-advance to next step after a short pause // Auto-advance to next step after a short pause
@ -447,6 +454,46 @@ export class TbDeviceInstallDialogComponent implements OnInit {
return match ? { id: match.id.id, name: match.name } : null; return match ? { id: match.id.id, name: match.name } : null;
} }
private collectCreatedEntityIds(): { entityType: string; id: string }[] {
const ids: { entityType: string; id: string }[] = [];
for (const ws of this.wizardSteps) {
if (ws.type === 'progress' && ws.entitySteps) {
for (const ep of ws.entitySteps) {
if (ep.status === 'success' && ep.entityOutput) {
const entityType = this.stepTypeToEntityType(ep.step.type);
if (entityType) {
ids.push({ entityType, id: ep.entityOutput.id });
}
}
}
}
}
return ids;
}
private findCreatedDashboardId(): { id: string } | undefined {
for (const ws of this.wizardSteps) {
if (ws.type === 'progress' && ws.entitySteps) {
for (const ep of ws.entitySteps) {
if (ep.step.type === InstallStepType.DASHBOARD && ep.status === 'success' && ep.entityOutput) {
return { id: ep.entityOutput.id };
}
}
}
}
return undefined;
}
private stepTypeToEntityType(stepType: InstallStepType): string | null {
switch (stepType) {
case InstallStepType.DEVICE_PROFILE: return 'DEVICE_PROFILE';
case InstallStepType.DEVICE: return 'DEVICE';
case InstallStepType.DASHBOARD: return 'DASHBOARD';
case InstallStepType.RULE_CHAIN: return 'RULE_CHAIN';
default: return null;
}
}
private delay(ms: number): Promise<void> { private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }

2
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts

@ -170,6 +170,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
case 'DASHBOARD': return descriptor.dashboardId?.id; case 'DASHBOARD': return descriptor.dashboardId?.id;
case 'CALCULATED_FIELD': return descriptor.entityId?.id; case 'CALCULATED_FIELD': return descriptor.entityId?.id;
case 'RULE_CHAIN': return descriptor.ruleChainId?.id; case 'RULE_CHAIN': return descriptor.ruleChainId?.id;
case 'DEVICE': return descriptor.dashboardId?.id ?? null;
case 'SOLUTION_TEMPLATE': return descriptor.dashboardId?.id; case 'SOLUTION_TEMPLATE': return descriptor.dashboardId?.id;
default: return null; default: return null;
} }
@ -182,6 +183,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
case 'DASHBOARD': return EntityType.DASHBOARD; case 'DASHBOARD': return EntityType.DASHBOARD;
case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType; case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType;
case 'RULE_CHAIN': return EntityType.RULE_CHAIN; case 'RULE_CHAIN': return EntityType.RULE_CHAIN;
case 'DEVICE': return descriptor.dashboardId ? EntityType.DASHBOARD : null;
case 'SOLUTION_TEMPLATE': return EntityType.DASHBOARD; case 'SOLUTION_TEMPLATE': return EntityType.DASHBOARD;
default: return null; default: return null;
} }

2
ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts

@ -39,6 +39,8 @@ export interface RuleChainInstalledItemDescriptor {
export interface DeviceInstalledItemDescriptor { export interface DeviceInstalledItemDescriptor {
type: 'DEVICE'; type: 'DEVICE';
createdEntityIds?: { entityType: string; id: string }[];
dashboardId?: { id: string };
} }
export interface SolutionTemplateInstalledItemDescriptor { export interface SolutionTemplateInstalledItemDescriptor {

Loading…
Cancel
Save