From 7cca8463c98666d358bde17c83466308be862a36 Mon Sep 17 00:00:00 2001
From: dpinkevych
Date: Fri, 22 May 2026 13:15:26 +0300
Subject: [PATCH 1/5] add related items logic
---
.../server/controller/IotHubController.java | 19 ++
.../service/iot_hub/DefaultIotHubService.java | 254 +++++++++++++++---
.../server/service/iot_hub/InstallPlan.java | 32 +++
.../service/iot_hub/InstallPlanEntry.java | 42 +++
.../service/iot_hub/InstallPlanResult.java | 38 +++
.../service/iot_hub/IotHubRestClient.java | 19 ++
.../server/service/iot_hub/IotHubService.java | 4 +
.../src/app/core/http/iot-hub-api.service.ts | 17 +-
.../iot-hub-install-dialog.component.html | 108 +++++++-
.../iot-hub-install-dialog.component.scss | 89 ++++++
.../iot-hub-install-dialog.component.ts | 172 +++++++++---
.../iot-hub/iot-hub-installed-item.models.ts | 31 +++
.../models/iot-hub/iot-hub-version.models.ts | 2 +
.../assets/locale/locale.constant-en_US.json | 10 +
14 files changed, 755 insertions(+), 82 deletions(-)
create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java
create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java
create mode 100644 application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java
diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java
index 09a30f9266..2af21a4af5 100644
--- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java
@@ -46,6 +46,8 @@ import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.iot_hub.InstallItemVersionResult;
+import org.thingsboard.server.service.iot_hub.InstallPlan;
+import org.thingsboard.server.service.iot_hub.InstallPlanResult;
import org.thingsboard.server.service.iot_hub.UpdateItemVersionResult;
import org.thingsboard.server.service.iot_hub.IotHubService;
@@ -71,6 +73,23 @@ public class IotHubController extends BaseController {
return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request);
}
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/versions/{versionId}/installPlan")
+ @ResponseBody
+ public InstallPlan resolveInstallPlan(@PathVariable String versionId) throws ThingsboardException {
+ return iotHubService.resolveInstallPlan(getCurrentUser(), versionId);
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @PostMapping("/installPlan")
+ @ResponseBody
+ public InstallPlanResult installPlan(@RequestBody InstallPlanRequest body,
+ HttpServletRequest request) throws ThingsboardException {
+ return iotHubService.installPlan(getCurrentUser(), body.plan(), body.data(), request);
+ }
+
+ public record InstallPlanRequest(InstallPlan plan, JsonNode data) {}
+
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/device/register")
@ResponseBody
diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
index 5c08b4f12b..ace31effbe 100644
--- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
+++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
@@ -71,8 +71,11 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.HexFormat;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
@Service
@@ -103,48 +106,61 @@ public class DefaultIotHubService implements IotHubService {
public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) {
TenantId tenantId = user.getTenantId();
log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId);
-
try {
- JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
- String itemType = versionInfo.get("type").asText();
- String itemName = versionInfo.get("name").asText();
- UUID itemId = UUID.fromString(versionInfo.get("itemId").asText());
- String version = versionInfo.get("version").asText();
- log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType);
-
- byte[] fileData = iotHubRestClient.getVersionFileData(versionId);
- log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0);
-
- IotHubInstalledItemDescriptor descriptor = switch (itemType) {
- case "WIDGET" -> installWidget(user, tenantId, fileData);
- case "DASHBOARD" -> installDashboard(user, tenantId, fileData);
- case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data);
- case "ALARM_RULE" -> throw new IllegalArgumentException(
- "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages.");
- case "RULE_CHAIN" -> installRuleChain(user, tenantId, fileData, data);
- case "DEVICE" -> installDeviceProfile(user, tenantId, fileData);
- case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request);
- default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType);
- };
+ IotHubInstalledItem installedItem = doInstallVersion(user, versionId, data, request);
+ return InstallItemVersionResult.success(installedItem.getDescriptor());
+ } catch (Exception e) {
+ log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e);
+ return InstallItemVersionResult.error(e.getMessage());
+ }
+ }
- IotHubInstalledItem installedItem = new IotHubInstalledItem();
- installedItem.setTenantId(tenantId);
- installedItem.setItemId(itemId);
- installedItem.setItemVersionId(UUID.fromString(versionId));
- installedItem.setItemName(itemName);
- installedItem.setItemType(itemType);
- installedItem.setVersion(version);
- installedItem.setDescriptor(descriptor);
- iotHubInstalledItemService.save(tenantId, installedItem);
+ /**
+ * Fetch + apply a single marketplace version, persist the installed-item record, and ping
+ * the marketplace install counter. Throws on failure so callers (cascade install) can roll back.
+ */
+ private IotHubInstalledItem doInstallVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) throws Exception {
+ TenantId tenantId = user.getTenantId();
+ JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
+ String itemType = versionInfo.get("type").asText();
+ String itemName = versionInfo.get("name").asText();
+ UUID itemId = UUID.fromString(versionInfo.get("itemId").asText());
+ String version = versionInfo.get("version").asText();
+ log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType);
+
+ byte[] fileData = iotHubRestClient.getVersionFileData(versionId);
+ log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0);
+
+ IotHubInstalledItemDescriptor descriptor = switch (itemType) {
+ case "WIDGET" -> installWidget(user, tenantId, fileData);
+ case "DASHBOARD" -> installDashboard(user, tenantId, fileData);
+ case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data);
+ case "ALARM_RULE" -> throw new IllegalArgumentException(
+ "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages.");
+ case "RULE_CHAIN" -> installRuleChain(user, tenantId, fileData, data);
+ case "DEVICE" -> installDeviceProfile(user, tenantId, fileData);
+ case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request);
+ default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType);
+ };
+
+ IotHubInstalledItem installedItem = new IotHubInstalledItem();
+ installedItem.setTenantId(tenantId);
+ installedItem.setItemId(itemId);
+ installedItem.setItemVersionId(UUID.fromString(versionId));
+ installedItem.setItemName(itemName);
+ installedItem.setItemType(itemType);
+ installedItem.setVersion(version);
+ installedItem.setDescriptor(descriptor);
+ installedItem = iotHubInstalledItemService.save(tenantId, installedItem);
+ try {
iotHubRestClient.reportVersionInstalled(versionId);
- log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType);
-
- return InstallItemVersionResult.success(descriptor);
} catch (Exception e) {
- log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e);
- return InstallItemVersionResult.error(e.getMessage());
+ // Counter ping is best-effort — do not fail the install if it errors.
+ log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage());
}
+ log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType);
+ return installedItem;
}
private WidgetInstalledItemDescriptor installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
@@ -654,4 +670,170 @@ public class DefaultIotHubService implements IotHubService {
iotHubInstalledItemService.deleteById(tenantId, installedItemId);
log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName());
}
+
+ @Override
+ public InstallPlan resolveInstallPlan(SecurityUser user, String versionId) {
+ TenantId tenantId = user.getTenantId();
+ log.debug("[{}] Resolving install plan for version: {}", tenantId, versionId);
+
+ JsonNode rootVersion = iotHubRestClient.getVersionInfo(versionId);
+ if (rootVersion == null) {
+ throw new IllegalArgumentException("Marketplace version not found: " + versionId);
+ }
+
+ Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId));
+
+ // LinkedHashMap preserves insertion order; we add deps before the root so a single
+ // forward iteration yields the correct topological install sequence.
+ LinkedHashMap entries = new LinkedHashMap<>();
+ Set visiting = new HashSet<>();
+
+ collectDependencies(rootVersion, alreadyInstalledItemIds, entries, visiting, true);
+
+ return new InstallPlan(versionId, new ArrayList<>(entries.values()));
+ }
+
+ /**
+ * Depth-first walk over {@code relatedItems}. Children are added BEFORE the current
+ * version so the resulting LinkedHashMap iterates deps-first, root-last.
+ */
+ private void collectDependencies(JsonNode versionInfo,
+ Set alreadyInstalledItemIds,
+ LinkedHashMap entries,
+ Set visiting,
+ boolean root) {
+ String itemId = versionInfo.get("itemId").asText();
+ if (entries.containsKey(itemId)) {
+ return;
+ }
+ if (!visiting.add(itemId)) {
+ log.warn("Dependency cycle detected involving IoT Hub item {} — breaking", itemId);
+ return;
+ }
+
+ JsonNode related = versionInfo.get("relatedItems");
+ if (related != null && related.isArray()) {
+ for (JsonNode relatedNode : related) {
+ String relatedItemId = relatedNode.asText();
+ if (relatedItemId == null || relatedItemId.isEmpty() || entries.containsKey(relatedItemId)) {
+ continue;
+ }
+ JsonNode relatedVersion;
+ try {
+ relatedVersion = iotHubRestClient.getPublishedVersionByItemId(relatedItemId);
+ } catch (Exception e) {
+ log.warn("Failed to fetch related item {}: {}", relatedItemId, e.getMessage());
+ entries.put(relatedItemId, missingEntry(relatedItemId, e.getMessage()));
+ continue;
+ }
+ if (relatedVersion == null) {
+ log.warn("Related IoT Hub item {} is missing or unpublished — recording in plan", relatedItemId);
+ entries.put(relatedItemId, missingEntry(relatedItemId, "Item not found or not published"));
+ continue;
+ }
+ collectDependencies(relatedVersion, alreadyInstalledItemIds, entries, visiting, false);
+ }
+ }
+
+ visiting.remove(itemId);
+
+ InstallPlanEntry entry = new InstallPlanEntry();
+ entry.setItemId(itemId);
+ entry.setVersionId(versionInfo.get("id").asText());
+ entry.setName(versionInfo.hasNonNull("name") ? versionInfo.get("name").asText() : null);
+ entry.setType(versionInfo.hasNonNull("type") ? versionInfo.get("type").asText() : null);
+ entry.setVersion(versionInfo.hasNonNull("version") ? versionInfo.get("version").asText() : null);
+ entry.setRoot(root);
+ boolean alreadyInstalled;
+ try {
+ alreadyInstalled = alreadyInstalledItemIds.contains(UUID.fromString(itemId));
+ } catch (IllegalArgumentException ex) {
+ alreadyInstalled = false;
+ }
+ entry.setStatus(alreadyInstalled
+ ? InstallPlanEntry.Status.ALREADY_INSTALLED
+ : InstallPlanEntry.Status.WILL_INSTALL);
+ entries.put(itemId, entry);
+ }
+
+ private static InstallPlanEntry missingEntry(String itemId, String errorMessage) {
+ InstallPlanEntry entry = new InstallPlanEntry();
+ entry.setItemId(itemId);
+ entry.setStatus(InstallPlanEntry.Status.MISSING);
+ entry.setErrorMessage(errorMessage);
+ return entry;
+ }
+
+ @Override
+ public InstallPlanResult installPlan(SecurityUser user, InstallPlan plan, JsonNode data, HttpServletRequest request) {
+ TenantId tenantId = user.getTenantId();
+ if (plan == null || plan.getEntries() == null || plan.getEntries().isEmpty()) {
+ return new InstallPlanResult(false, false, "Install plan is empty", null, new ArrayList<>(), new ArrayList<>());
+ }
+
+ InstallPlanResult result = new InstallPlanResult();
+ List resultEntries = new ArrayList<>();
+ List missingItemIds = new ArrayList<>();
+ List rollbackIds = new ArrayList<>();
+ IotHubInstalledItemDescriptor rootDescriptor = null;
+
+ for (InstallPlanEntry entry : plan.getEntries()) {
+ InstallPlanEntry resultEntry = cloneEntry(entry);
+ switch (entry.getStatus()) {
+ case MISSING -> {
+ missingItemIds.add(entry.getItemId());
+ resultEntries.add(resultEntry);
+ }
+ case ALREADY_INSTALLED -> resultEntries.add(resultEntry);
+ case WILL_INSTALL -> {
+ try {
+ // Only the root entry receives the user's install data (target profile entityId);
+ // transitive deps install with defaults.
+ JsonNode entryData = entry.isRoot() ? data : null;
+ IotHubInstalledItem installed = doInstallVersion(user, entry.getVersionId(), entryData, request);
+ rollbackIds.add(installed.getId());
+ if (entry.isRoot()) {
+ rootDescriptor = installed.getDescriptor();
+ }
+ resultEntries.add(resultEntry);
+ } catch (Exception e) {
+ log.error("[{}] Cascade install failed at entry {} ({}): {}", tenantId,
+ entry.getName(), entry.getVersionId(), e.getMessage(), e);
+ resultEntry.setErrorMessage(e.getMessage());
+ resultEntries.add(resultEntry);
+ rollbackInstalledItems(user, rollbackIds);
+ result.setSuccess(false);
+ result.setRolledBack(true);
+ result.setErrorMessage("Failed to install '" + entry.getName() + "': " + e.getMessage());
+ result.setEntries(resultEntries);
+ result.setMissingItemIds(missingItemIds);
+ return result;
+ }
+ }
+ }
+ }
+
+ result.setSuccess(true);
+ result.setRolledBack(false);
+ result.setRootDescriptor(rootDescriptor);
+ result.setEntries(resultEntries);
+ result.setMissingItemIds(missingItemIds);
+ return result;
+ }
+
+ private void rollbackInstalledItems(SecurityUser user, List installedIds) {
+ for (int i = installedIds.size() - 1; i >= 0; i--) {
+ IotHubInstalledItemId id = installedIds.get(i);
+ try {
+ deleteInstalledItem(user, id);
+ } catch (Exception e) {
+ log.error("[{}] Failed to roll back installed item {}: {}", user.getTenantId(), id, e.getMessage(), e);
+ }
+ }
+ }
+
+ private static InstallPlanEntry cloneEntry(InstallPlanEntry src) {
+ return new InstallPlanEntry(src.getItemId(), src.getVersionId(), src.getName(), src.getType(),
+ src.getVersion(), src.getStatus(), src.isRoot(), src.getErrorMessage());
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java
new file mode 100644
index 0000000000..12c268a6b4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.iot_hub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class InstallPlan {
+
+ private String rootVersionId;
+ private List entries;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java
new file mode 100644
index 0000000000..c073ff8e0e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.iot_hub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class InstallPlanEntry {
+
+ public enum Status {
+ WILL_INSTALL,
+ ALREADY_INSTALLED,
+ MISSING
+ }
+
+ private String itemId;
+ private String versionId;
+ private String name;
+ private String type;
+ private String version;
+ private Status status;
+ private boolean root;
+ private String errorMessage;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java
new file mode 100644
index 0000000000..2c3f49e071
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.iot_hub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class InstallPlanResult {
+
+ private boolean success;
+ private boolean rolledBack;
+ private String errorMessage;
+ private IotHubInstalledItemDescriptor rootDescriptor;
+ private List entries = new ArrayList<>();
+ private List missingItemIds = new ArrayList<>();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
index 1155bcf0ad..2de1fc8635 100644
--- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
+++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
@@ -18,8 +18,10 @@ package org.thingsboard.server.service.iot_hub;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
+import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.queue.util.TbCoreComponent;
@@ -39,6 +41,23 @@ public class IotHubRestClient {
return restTemplate.getForObject(url, JsonNode.class);
}
+ /**
+ * Returns the latest published version of an item, or {@code null} if the marketplace
+ * responds with 404 (item missing or never published). Other 4xx/5xx still propagate.
+ */
+ public JsonNode getPublishedVersionByItemId(String itemId) {
+ String url = baseUrl + "/api/items/" + itemId + "/published";
+ log.debug("Fetching IoT Hub published version for item: {}", url);
+ try {
+ return restTemplate.getForObject(url, JsonNode.class);
+ } catch (HttpClientErrorException e) {
+ if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return null;
+ }
+ throw e;
+ }
+ }
+
public byte[] getVersionFileData(String versionId) {
String url = baseUrl + "/api/versions/" + versionId + "/fileData";
log.debug("Fetching IoT Hub version file data: {}", url);
diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
index e2f0ff0c39..fac0f94b10 100644
--- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
+++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
@@ -30,4 +30,8 @@ public interface IotHubService {
InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor);
void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId);
+
+ InstallPlan resolveInstallPlan(SecurityUser user, String versionId);
+
+ InstallPlanResult installPlan(SecurityUser user, InstallPlan plan, JsonNode data, HttpServletRequest request);
}
diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts
index 12878ac0fc..3a98526b60 100644
--- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts
+++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts
@@ -22,7 +22,7 @@ import { PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { CreatorView } from '@shared/models/iot-hub/iot-hub-creator.models';
-import { IotHubInstalledItem, InstallItemVersionResult, UpdateItemVersionResult, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models';
+import { IotHubInstalledItem, InstallItemVersionResult, InstallPlan, InstallPlanResult, UpdateItemVersionResult, ItemPublishedVersionInfo } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { ItemType, ItemTypeFilterInfo, WidgetCategory } from '@shared/models/iot-hub/iot-hub-item.models';
import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
@@ -162,6 +162,21 @@ export class IotHubApiService {
);
}
+ public resolveInstallPlan(versionId: string, config?: IotHubRequestConfig): Observable {
+ return this.http.get(
+ `/api/iot-hub/versions/${versionId}/installPlan`,
+ { params: this.buildParams(config) }
+ );
+ }
+
+ public installPlan(plan: InstallPlan, data?: any, config?: IotHubRequestConfig): Observable {
+ return this.http.post(
+ `/api/iot-hub/installPlan`,
+ { plan, data: data ?? null },
+ { params: this.buildParams(config) }
+ );
+ }
+
public updateItemVersion(installedItemId: string, versionId: string, config?: IotHubRequestConfig, force?: boolean): Observable {
let params = this.buildParams(config);
if (force) {
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html
index 19a43fcb30..46e7f08264 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html
@@ -48,6 +48,45 @@
existing: pendingOverwrite?.existingRuleChainName
}">
}
+ @case ('plan') {
+ {{ 'iot-hub.install-plan-title' | translate:{ name: item.name } }}
+
+ @if (planSummary.willInstall > 0) {
+ {{ 'iot-hub.install-plan-will-install' | translate:{ count: planSummary.willInstall } }}
+ }
+ @if (planSummary.alreadyInstalled > 0) {
+ {{ 'iot-hub.install-plan-will-update' | translate:{ count: planSummary.alreadyInstalled } }}
+ }
+
+ @if (planSummary.missing > 0) {
+ {{ 'iot-hub.install-plan-missing-warning' | translate:{ count: planSummary.missing } }}
+ }
+
+ }
@case ('installing') {
{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}
{{ 'iot-hub.install-desc' | translate }}
@@ -56,6 +95,20 @@
{{ 'iot-hub.install-success-title' | translate }}
{{ 'iot-hub.install-success-desc' | translate:{ name: item.name } }}
}
+ @case ('partial') {
+ {{ 'iot-hub.install-partial-title' | translate }}
+ {{ 'iot-hub.install-partial-desc' | translate:{ name: item.name } }}
+
+ @for (entry of missingEntries; track entry.itemId) {
+ -
+ {{ entry.name || entry.itemId }}
+ @if (entry.errorMessage) {
+ {{ entry.errorMessage }}
+ }
+
+ }
+
+ }
@case ('error') {
{{ 'iot-hub.install-error-title' | translate }}
{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}
@@ -72,39 +125,66 @@
@switch (state) {
@case ('confirm') {
-
+
@if (item.type === ItemType.RULE_CHAIN) {
-
-