Browse Source

Merge branch 'feature/rule-chain-set-as-default' into feature/4.3/rule-chain-set-as-default

pull/15646/head
dshvaika 5 months ago
parent
commit
589ea69a17
  1. 52
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  2. 2
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java
  3. 55
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html
  4. 8
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss
  5. 124
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts
  6. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -21,12 +21,14 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -51,6 +53,7 @@ import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
@ -59,6 +62,7 @@ import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
@ -88,6 +92,8 @@ public class DefaultIotHubService implements IotHubService {
private final RuleChainService ruleChainService;
private final TbRuleChainService tbRuleChainService;
private final TbDeviceProfileService tbDeviceProfileService;
private final AssetProfileService assetProfileService;
private final TbAssetProfileService tbAssetProfileService;
private final IotHubInstalledItemService iotHubInstalledItemService;
private final WidgetTypeService widgetTypeService;
private final DashboardService dashboardService;
@ -118,7 +124,7 @@ public class DefaultIotHubService implements IotHubService {
case "DASHBOARD" -> installDashboard(user, tenantId, fileData);
case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data);
case "ALARM_RULE" -> installAlarmRule(user, tenantId, fileData, data);
case "RULE_CHAIN" -> installRuleChain(tenantId, fileData);
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);
@ -224,7 +230,8 @@ public class DefaultIotHubService implements IotHubService {
return descriptor;
}
private RuleChainInstalledItemDescriptor installRuleChain(TenantId tenantId, byte[] fileData) throws Exception {
private RuleChainInstalledItemDescriptor installRuleChain(
SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception {
JsonNode json = JacksonUtil.toJsonNode(new String(fileData));
RuleChain ruleChain;
@ -250,11 +257,52 @@ public class DefaultIotHubService implements IotHubService {
ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration);
log.debug("[{}] Rule chain installed: {}", tenantId, savedRuleChain.getName());
RuleChainInstalledItemDescriptor descriptor = new RuleChainInstalledItemDescriptor();
descriptor.setRuleChainId(savedRuleChain.getId());
if (data != null && data.has("entityId") && !data.get("entityId").isNull()) {
EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class);
setAsDefaultRuleChain(user, tenantId, entityId, savedRuleChain.getId());
descriptor.setTargetProfileId(entityId);
}
return descriptor;
}
private void setAsDefaultRuleChain(SecurityUser user, TenantId tenantId,
EntityId entityId, RuleChainId ruleChainId) throws Exception {
switch (entityId.getEntityType()) {
case DEVICE_PROFILE -> {
DeviceProfile profile = deviceProfileService.findDeviceProfileById(
tenantId, (DeviceProfileId) entityId);
if (profile == null) {
throw new IllegalArgumentException(
"Device profile not found: " + entityId.getId());
}
profile.setDefaultRuleChainId(ruleChainId);
tbDeviceProfileService.save(profile, user);
log.debug("[{}] Set rule chain {} as default on device profile {}",
tenantId, ruleChainId.getId(), entityId.getId());
}
case ASSET_PROFILE -> {
AssetProfile profile = assetProfileService.findAssetProfileById(
tenantId, (AssetProfileId) entityId);
if (profile == null) {
throw new IllegalArgumentException(
"Asset profile not found: " + entityId.getId());
}
profile.setDefaultRuleChainId(ruleChainId);
tbAssetProfileService.save(profile, user);
log.debug("[{}] Set rule chain {} as default on asset profile {}",
tenantId, ruleChainId.getId(), entityId.getId());
}
default -> throw new IllegalArgumentException(
"Rule chain can only be set as default on device or asset profile, got: "
+ entityId.getEntityType());
}
}
private DeviceInstalledItemDescriptor installDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
DeviceProfile deviceProfile;
try {

2
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java

@ -16,11 +16,13 @@
package org.thingsboard.server.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
@Data
public class RuleChainInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private RuleChainId ruleChainId;
private EntityId targetProfileId;
}

55
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html

@ -19,20 +19,35 @@
@switch (state) {
@case ('confirm') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
@if (item.type === ItemType.RULE_CHAIN) {
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.rule-chain-install-desc' | translate }}</p>
} @else {
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
}
}
@case ('select-entity') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.select-entity-for-cf' | translate:{ name: item.name } }}</p>
@if (activeSelectEntityConfig?.promptKey) {
<p class="tb-iot-hub-install-desc"
[innerHTML]="activeSelectEntityConfig.promptKey | translate:{ name: item.name }"></p>
}
<tb-entity-select
[(ngModel)]="selectedEntityId"
[allowedEntityTypes]="cfEntityTypes"
[defaultEntityType]="defaultCfEntityType"
[allowedEntityTypes]="activeSelectEntityConfig.allowed"
[defaultEntityType]="activeSelectEntityConfig.defaultType"
[filterAllowedEntityTypes]="false"
appearance="outline"
required>
[required]="activeSelectEntityConfig.required">
</tb-entity-select>
}
@case ('confirm-overwrite') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.rule-chain-overwrite-title' | translate }}</h2>
<p class="tb-iot-hub-install-desc"
[innerHTML]="'iot-hub.rule-chain-overwrite-body' | translate:{
profile: pendingOverwrite?.profileName,
existing: pendingOverwrite?.existingRuleChainName
}"></p>
}
@case ('installing') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
@ -58,11 +73,37 @@
@switch (state) {
@case ('confirm') {
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" (click)="install()">{{ 'iot-hub.install' | translate }}</button>
@if (item.type === ItemType.RULE_CHAIN) {
<button mat-stroked-button (click)="install()">{{ 'iot-hub.install' | translate }}</button>
<button mat-flat-button color="primary" (click)="installAsEntityProfileDefault()">
{{ 'iot-hub.rule-chain-install-as-default' | translate }}
</button>
} @else {
<button mat-flat-button color="primary" (click)="install()">{{ 'iot-hub.install' | translate }}</button>
}
}
@case ('select-entity') {
<button mat-button (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" [disabled]="!selectedEntityId" (click)="doInstall()">{{ 'iot-hub.install' | translate }}</button>
@if (item.type === ItemType.RULE_CHAIN) {
<button mat-button (click)="selectEntityBack()">{{ 'action.back' | translate }}</button>
<button mat-flat-button color="primary"
[disabled]="!selectedEntityId"
(click)="onSelectEntityInstall()">
{{ 'iot-hub.install' | translate }}
</button>
} @else {
<button mat-flat-button color="primary"
[disabled]="activeSelectEntityConfig?.required && !selectedEntityId"
(click)="onSelectEntityInstall()">
{{ 'iot-hub.install' | translate }}
</button>
}
}
@case ('confirm-overwrite') {
<button mat-button (click)="confirmOverwriteCancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" (click)="confirmOverwriteReplace()">
{{ 'iot-hub.rule-chain-overwrite-replace' | translate }}
</button>
}
@case ('installing') {
<button mat-button disabled>{{ 'action.cancel' | translate }}</button>

8
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.scss

@ -14,6 +14,14 @@
* limitations under the License.
*/
@import "../../../../../scss/constants";
tb-entity-select {
@media #{$mat-gt-sm} {
min-width: 512px;
}
}
.tb-iot-hub-install-title {
font-size: 20px;
font-weight: 600;

124
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts

@ -29,12 +29,37 @@ import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { resolveEntityDetailsUrl } from './iot-hub-components.models';
import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { AssetProfileService } from '@core/http/asset-profile.service';
import { RuleChainService } from '@core/http/rule-chain.service';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
interface SelectEntityConfig {
allowed: EntityType[];
defaultType: EntityType;
required: boolean;
promptKey?: string;
}
interface PendingOverwrite {
entityId: EntityId;
profileName: string;
existingRuleChainName: string;
}
export interface IotHubInstallDialogData {
item: MpItemVersionView;
}
export type InstallState = 'select-entity' | 'confirm' | 'installing' | 'success' | 'error';
export type InstallState =
| 'select-entity'
| 'confirm-overwrite'
| 'confirm'
| 'installing'
| 'success'
| 'error';
@Component({
selector: 'tb-iot-hub-install-dialog',
@ -53,8 +78,26 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
entityDetailsUrl: string | null = null;
selectedEntityId: EntityId | null = null;
cfEntityTypes: EntityType[] = [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE];
defaultCfEntityType = EntityType.DEVICE_PROFILE;
pendingOverwrite: PendingOverwrite | null = null;
private readonly selectEntityConfig: Partial<Record<ItemType, SelectEntityConfig>> = {
[ItemType.CALCULATED_FIELD]: {
allowed: [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE],
defaultType: EntityType.DEVICE_PROFILE,
required: true,
promptKey: 'iot-hub.select-entity-for-cf',
},
[ItemType.RULE_CHAIN]: {
allowed: [EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE],
defaultType: EntityType.DEVICE_PROFILE,
required: true,
promptKey: 'iot-hub.select-profile-for-rule-chain',
},
};
get activeSelectEntityConfig(): SelectEntityConfig | null {
return this.selectEntityConfig[this.item.type] ?? null;
}
constructor(
protected store: Store<AppState>,
@ -63,7 +106,10 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
@Inject(MAT_DIALOG_DATA) public data: IotHubInstallDialogData,
private dialog: MatDialog,
private translate: TranslateService,
private iotHubApiService: IotHubApiService
private iotHubApiService: IotHubApiService,
private deviceProfileService: DeviceProfileService,
private assetProfileService: AssetProfileService,
private ruleChainService: RuleChainService
) {
super(store, router, dialogRef);
this.item = data.item;
@ -82,6 +128,76 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
this.doInstall();
}
installAsEntityProfileDefault(): void {
this.state = 'select-entity';
}
selectEntityBack(): void {
this.selectedEntityId = null;
this.state = 'confirm';
}
onSelectEntityInstall(): void {
if (!this.selectedEntityId) {
return;
}
if (this.item.type !== ItemType.RULE_CHAIN) {
this.doInstall();
return;
}
this.resolveOverwrite(this.selectedEntityId).subscribe({
next: (pending) => {
if (pending) {
this.pendingOverwrite = pending;
this.state = 'confirm-overwrite';
} else {
this.pendingOverwrite = null;
this.doInstall();
}
},
error: (err) => {
this.state = 'error';
this.errorMessage = err?.error?.message || err?.message ||
this.translate.instant('iot-hub.install-error', { name: this.item.name });
}
});
}
confirmOverwriteReplace(): void {
this.doInstall();
}
confirmOverwriteCancel(): void {
this.pendingOverwrite = null;
this.state = 'select-entity';
}
private resolveOverwrite(profileEntityId: EntityId): Observable<PendingOverwrite | null> {
const lookupProfile$: Observable<{ name: string; defaultRuleChainId: RuleChainId | null }> =
profileEntityId.entityType === EntityType.DEVICE_PROFILE
? this.deviceProfileService.getDeviceProfile(profileEntityId.id, { ignoreLoading: true }).pipe(
map(p => ({ name: p.name, defaultRuleChainId: p.defaultRuleChainId ?? null }))
)
: this.assetProfileService.getAssetProfile(profileEntityId.id, { ignoreLoading: true }).pipe(
map(p => ({ name: p.name, defaultRuleChainId: p.defaultRuleChainId ?? null }))
);
return lookupProfile$.pipe(
switchMap(profile => {
if (!profile.defaultRuleChainId) {
return of<PendingOverwrite | null>(null);
}
return this.ruleChainService.getRuleChain(profile.defaultRuleChainId.id, { ignoreLoading: true }).pipe(
map(existing => ({
entityId: profileEntityId,
profileName: profile.name,
existingRuleChainName: existing.name,
}))
);
})
);
}
doInstall(): void {
this.state = 'installing';
const versionId = this.item.id as string;

6
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4301,6 +4301,12 @@
"remove-desc-solution-template": "Removing this solution template will also remove all created entities including dashboards, rule chains, devices, and other associated resources within your tenant",
"remove-anyway": "Remove anyway",
"select-entity-for-cf": "Select entity for calculated field '{{name}}'",
"rule-chain-install-desc": "Installing this will automatically create the rule chain within your tenant. Use 'Set for profile' to also set it as default for Device or Asset profile.",
"rule-chain-install-as-default": "Set for profile",
"select-profile-for-rule-chain": "Select profile for rule chain '{{name}}'",
"rule-chain-overwrite-title": "Replace existing default rule chain?",
"rule-chain-overwrite-body": "Profile <b>{{profile}}</b> currently uses <b>{{existing}}</b> as its default rule chain. Installing will replace it with this rule chain.",
"rule-chain-overwrite-replace": "Replace",
"install": "Install",
"connect": "Connect",
"connect-device": "Connect device",

Loading…
Cancel
Save