Browse Source

Merge remote-tracking branch 'origin/lts-4.2' into release-4.2

release-4.2
Viacheslav Klimov 1 month ago
parent
commit
d24bd033e0
Failed to extract signature
  1. 23
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  2. 19
      application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
  4. 26
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java
  6. 46
      application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java
  7. 4
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html
  8. 16
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  10. 21
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts
  11. 7
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss
  12. 10
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  13. 12
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.ts
  14. 10
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html
  15. 17
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html
  16. 1
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss
  17. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts
  18. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  19. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  20. 8
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts
  21. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html
  22. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts
  23. 3
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  24. 2
      ui-ngx/src/app/modules/home/home.component.html
  25. 3
      ui-ngx/src/app/modules/home/home.component.ts
  26. 8
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
  27. 31
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  28. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html
  29. 12
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts
  30. 6
      ui-ngx/src/app/shared/models/units/volume-flow.ts
  31. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -64,6 +64,7 @@ import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.entitiy.device.TbDeviceService; 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.install.ProjectInfo;
import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -112,6 +113,7 @@ public class DefaultIotHubService implements IotHubService {
private final DeviceService deviceService; private final DeviceService deviceService;
private final TbDeviceService tbDeviceService; private final TbDeviceService tbDeviceService;
private final SolutionService solutionService; private final SolutionService solutionService;
private final ProjectInfo projectInfo;
// Field names of the marketplace version JSON payload. Both the install path and the // Field names of the marketplace version JSON payload. Both the install path and the
// install-plan resolver parse the same shape, so the contract lives here in one place. // install-plan resolver parse the same shape, so the contract lives here in one place.
@ -196,10 +198,11 @@ public class DefaultIotHubService implements IotHubService {
} }
try { try {
iotHubRestClient.reportVersionInstalled(versionId); InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(),
projectInfo.getProjectVersion(), projectInfo.getProductType());
iotHubRestClient.reportVersionInstalled(versionId, report);
} catch (Exception e) { } catch (Exception e) {
// Counter ping is best-effort — do not fail the install if it errors. log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage());
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); log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType);
return installedItem; return installedItem;
@ -611,6 +614,13 @@ public class DefaultIotHubService implements IotHubService {
} }
} }
static InstallReport buildInstallReport(UUID tenantId, UUID userId, String tbVersion, String edition) {
String salt = tenantId.toString();
String tenantHash = sha256(salt + tenantId);
String userHash = sha256(salt + userId);
return new InstallReport(tenantHash, userHash, tbVersion, edition);
}
@Override @Override
public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) { public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) {
TenantId tenantId = user.getTenantId(); TenantId tenantId = user.getTenantId();
@ -650,10 +660,11 @@ public class DefaultIotHubService implements IotHubService {
} }
try { try {
iotHubRestClient.reportVersionInstalled(versionId); InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(),
projectInfo.getProjectVersion(), projectInfo.getProductType());
iotHubRestClient.reportVersionInstalled(versionId, report);
} catch (Exception e) { } catch (Exception e) {
// Counter ping is best-effort — do not fail the install if it errors. log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage());
log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage());
} }
log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version);

19
application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java

@ -0,0 +1,19 @@
/**
* 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;
public record InstallReport(String tenantHash, String userHash, String tbVersion, String edition) {
}

4
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java

@ -120,9 +120,9 @@ public class IotHubRestClient {
}); });
} }
public void reportVersionInstalled(String versionId) { public void reportVersionInstalled(String versionId, InstallReport report) {
String url = baseUrl + "/api/versions/" + versionId + "/install"; String url = baseUrl + "/api/versions/" + versionId + "/install";
log.debug("Reporting IoT Hub version installed: {}", url); log.debug("Reporting IoT Hub version installed: {}", url);
restTemplate.postForObject(url, null, Void.class); restTemplate.postForObject(url, report, Void.class);
} }
} }

26
application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java

@ -393,12 +393,12 @@ public class DefaultSolutionService implements SolutionService {
provisionAlarmRules(ctx); provisionAlarmRules(ctx);
provisionCalculatedFields(ctx);
Set<CompletableFuture<Void>> telemetryLoading = launchEmulators(ctx, devices, assets); Set<CompletableFuture<Void>> telemetryLoading = launchEmulators(ctx, devices, assets);
waitForTelemetryCompletion(telemetryLoading); waitForTelemetryCompletion(telemetryLoading);
provisionCalculatedFields(ctx);
ctx.getSolutionInstructions().setDetails(prepareInstructions(ctx, request)); ctx.getSolutionInstructions().setDetails(prepareInstructions(ctx, request));
List<ReferenceableEntityDefinition> ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); List<ReferenceableEntityDefinition> ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {});
@ -1297,27 +1297,7 @@ public class DefaultSolutionService implements SolutionService {
List<CalculatedFieldDefinition> cfs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "calculated_fields.json", new TypeReference<>() { List<CalculatedFieldDefinition> cfs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "calculated_fields.json", new TypeReference<>() {
}); });
cfs.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "calculated_fields", CalculatedFieldDefinition.class)); cfs.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "calculated_fields", CalculatedFieldDefinition.class));
cfs.forEach(cf -> ctx.register(createCalculatedField(cf, ctx)));
List<CalculatedFieldDefinition> createOnly = new ArrayList<>();
TreeMap<Integer, List<CalculatedFieldDefinition>> ordered = new TreeMap<>();
for (CalculatedFieldDefinition cf : cfs) {
if (cf.getReprocessingOrder() == null || cf.getReprocessingOrder() < 0) {
createOnly.add(cf);
} else {
ordered.computeIfAbsent(cf.getReprocessingOrder(), integer -> new ArrayList<>()).add(cf);
}
}
createOnly.forEach(cf -> ctx.register(createCalculatedField(cf, ctx)));
for (Map.Entry<Integer, List<CalculatedFieldDefinition>> entry : ordered.entrySet()) {
List<CalculatedFieldDefinition> cfDefs = entry.getValue();
for (CalculatedFieldDefinition cfDef : cfDefs) {
CalculatedField calculatedField = createCalculatedField(cfDef, ctx);
ctx.register(calculatedField);
}
}
} }
private CalculatedField createCalculatedField(CalculatedField cf, SolutionInstallContext ctx) { private CalculatedField createCalculatedField(CalculatedField cf, SolutionInstallContext ctx) {

2
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java

@ -23,6 +23,4 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class CalculatedFieldDefinition extends CalculatedField { public class CalculatedFieldDefinition extends CalculatedField {
private Integer reprocessingOrder;
} }

46
application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java

@ -0,0 +1,46 @@
/**
* 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 org.junit.jupiter.api.Test;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InstallReportTest {
private static String sha256(String s) throws Exception {
MessageDigest d = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(d.digest(s.getBytes(StandardCharsets.UTF_8)));
}
@Test
void buildInstallReport_hashesMatchSaltedFormula() throws Exception {
UUID tenantId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
InstallReport r = DefaultIotHubService.buildInstallReport(tenantId, userId, "4.2.0", "CE");
assertEquals(sha256(tenantId.toString() + tenantId), r.tenantHash());
assertEquals(sha256(tenantId.toString() + userId), r.userHash());
assertEquals("4.2.0", r.tbVersion());
assertEquals("CE", r.edition());
}
}

4
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html

@ -69,7 +69,7 @@
<ng-template #userComment> <ng-template #userComment>
<div class="user-comment flex flex-row items-center justify-start gap-2" <div class="user-comment flex flex-row items-center justify-start gap-2"
*ngIf="!displayDataElement.edit; else commentEditing" *ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)" (mouseenter)="onCommentMouseEnter(i)"
(mouseleave)="onCommentMouseLeave(i)"> (mouseleave)="onCommentMouseLeave(i)">
<div *ngIf="displayDataElement.userExists; else userDeleted" <div *ngIf="displayDataElement.userExists; else userDeleted"
class="user-avatar flex flex-row items-center justify-center self-start xs:!hidden" class="user-avatar flex flex-row items-center justify-center self-start xs:!hidden"
@ -98,11 +98,13 @@
<div class="action-buttons flex flex-row xs:flex-col" <div class="action-buttons flex flex-row xs:flex-col"
[class.show-buttons]="displayDataElement.showActions"> [class.show-buttons]="displayDataElement.showActions">
<button mat-icon-button <button mat-icon-button
*ngIf="displayDataElement.canEdit"
type="button" type="button"
(click)="editComment(displayDataElement.commentId)"> (click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon> <mat-icon class="material-icons">edit</mat-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
*ngIf="displayDataElement.canDelete"
type="button" type="button"
(click)="deleteComment(displayDataElement.commentId)"> (click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons">delete</mat-icon> <mat-icon class="material-icons">delete</mat-icon>

16
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts

@ -22,6 +22,7 @@ import { AlarmCommentService } from '@core/http/alarm-comment.service';
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
@ -44,6 +45,8 @@ interface AlarmCommentsDisplayData {
editedTime?: string; editedTime?: string;
editedDateAgo?: string; editedDateAgo?: string;
showActions?: boolean; showActions?: boolean;
canEdit?: boolean;
canDelete?: boolean;
commentText?: string; commentText?: string;
isSystemComment?: boolean; isSystemComment?: boolean;
avatarBgColor?: string; avatarBgColor?: string;
@ -135,6 +138,11 @@ export class AlarmCommentComponent implements OnInit {
displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss'); displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss');
displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn) + '\n'; displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn) + '\n';
displayDataElement.showActions = false; displayDataElement.showActions = false;
const isCommentAuthor = this.authUser.userId === alarmComment.userId?.id;
// Mirrors backend AlarmCommentController#deleteAlarmComment / checkUserPermission:
// author may edit and delete own comments; tenant admin may delete any comment.
displayDataElement.canEdit = isCommentAuthor;
displayDataElement.canDelete = isCommentAuthor || this.authUser.authority === Authority.TENANT_ADMIN;
displayDataElement.isSystemComment = false; displayDataElement.isSystemComment = false;
displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName, displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName,
40, 60); 40, 60);
@ -243,11 +251,11 @@ export class AlarmCommentComponent implements OnInit {
return this.alarmCommentSortOrder.direction === Direction.ASC; return this.alarmCommentSortOrder.direction === Direction.ASC;
} }
onCommentMouseEnter(commentId: string, displayDataIndex: number): void { onCommentMouseEnter(displayDataIndex: number): void {
if (!this.editMode) { if (!this.editMode) {
const alarmUserId = this.getAlarmCommentById(commentId).userId.id; const displayDataElement = this.displayData[displayDataIndex];
if (this.authUser.userId === alarmUserId) { if (displayDataElement.canEdit || displayDataElement.canDelete) {
this.displayData[displayDataIndex].showActions = true; displayDataElement.showActions = true;
} }
} }
} }

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -50,7 +50,7 @@ tb-dashboard-widget-select {
.tb-widget-select-title-row { .tb-widget-select-title-row {
gap: 8px; gap: 8px;
width: 100%; width: 100%;
min-height: 40px; min-height: 42px;
.tb-widget-select-title { .tb-widget-select-title {
font-size: 24px; font-size: 24px;

21
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-actions.service.ts

@ -17,13 +17,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Observable, of, EMPTY } from 'rxjs'; import { Observable, of, EMPTY } from 'rxjs';
import { filter, mergeMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models';
import { DeviceInstalledItemDescriptor, IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import { DeviceInstalledItemDescriptor, IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { TbIotHubAddItemDialogComponent, IotHubAddItemDialogData, IotHubAddItemDialogResult } from './iot-hub-add-item-dialog.component'; import { TbIotHubAddItemDialogComponent, IotHubAddItemDialogData, IotHubAddItemDialogResult } from './iot-hub-add-item-dialog.component';
import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData, IotHubItemDetailDialogMode } from './iot-hub-item-detail-dialog.component'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData, IotHubItemDetailDialogMode } from './iot-hub-item-detail-dialog.component';
@ -38,7 +36,6 @@ export class IotHubActionsService {
constructor( constructor(
private dialog: MatDialog, private dialog: MatDialog,
private iotHubApiService: IotHubApiService,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService private translate: TranslateService
) {} ) {}
@ -94,11 +91,11 @@ export class IotHubActionsService {
}).afterClosed(); }).afterClosed();
} }
updateItem(installedItem: IotHubInstalledItem, version: string, versionId: string): Observable<string> { updateItem(installedItem: IotHubInstalledItem, version: string, versionId: string): Observable<string | boolean> {
if (!installedItem) { if (!installedItem) {
return EMPTY; return of(false);
} }
return this.dialog.open(TbIotHubUpdateDialogComponent, { return this.dialog.open<TbIotHubUpdateDialogComponent, IotHubUpdateDialogData, string | boolean>(TbIotHubUpdateDialogComponent, {
panelClass: ['tb-dialog'], panelClass: ['tb-dialog'],
disableClose: true, disableClose: true,
autoFocus: false, autoFocus: false,
@ -108,7 +105,7 @@ export class IotHubActionsService {
itemType: installedItem.itemType as ItemType, itemType: installedItem.itemType as ItemType,
version, version,
versionId versionId
} as IotHubUpdateDialogData }
}).afterClosed(); }).afterClosed();
} }
@ -116,16 +113,12 @@ export class IotHubActionsService {
if (!installedItem) { if (!installedItem) {
return of(false); return of(false);
} }
return this.dialog.open(TbIotHubDeleteDialogComponent, { return this.dialog.open<TbIotHubDeleteDialogComponent, IotHubDeleteDialogData, boolean>(TbIotHubDeleteDialogComponent, {
panelClass: ['tb-dialog'], panelClass: ['tb-dialog'],
disableClose: true, disableClose: true,
autoFocus: false, autoFocus: false,
data: { itemName: installedItem.itemName, itemType: installedItem.itemType } as IotHubDeleteDialogData data: { installedItemId: installedItem.id.id, itemName: installedItem.itemName, itemType: installedItem.itemType }
}).afterClosed().pipe( }).afterClosed();
filter(confirmed => !!confirmed),
mergeMap(() => this.iotHubApiService.deleteInstalledItem(installedItem.id.id)),
mergeMap(() => of(true))
);
} }
installDevice(item: MpItemVersionView): Observable<string> { installDevice(item: MpItemVersionView): Observable<string> {

7
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss

@ -359,14 +359,17 @@
} }
} }
// Filter chips row Design: 32px tall, no padding, centered
.tb-iot-hub-sort-row { .tb-iot-hub-sort-row {
height: 32px; min-height: 32px;
align-items: center; align-items: center;
padding: 0; padding: 0;
margin-top: 16px; margin-top: 16px;
margin-bottom: 12px; margin-bottom: 12px;
::ng-deep .mat-mdc-chip-set .mdc-evolution-chip-set__chips {
row-gap: 8px;
}
.mat-mdc-chip { .mat-mdc-chip {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;

10
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts

@ -580,8 +580,7 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
updateItem(item: MpItemVersionView): void { updateItem(item: MpItemVersionView): void {
const installedItem = this.getInstalledItem(item); const installedItem = this.getInstalledItem(item);
if (!installedItem) { return; } this.iotHubActions.updateItem(installedItem, item.version, item.id).subscribe(result => {
this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => {
if (result === 'updated') { if (result === 'updated') {
this.reloadInstalledItems(); this.reloadInstalledItems();
} }
@ -590,9 +589,10 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
deleteInstalledItem(item: MpItemVersionView): void { deleteInstalledItem(item: MpItemVersionView): void {
const installedItem = this.getInstalledItem(item); const installedItem = this.getInstalledItem(item);
if (!installedItem) { return; } this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => {
this.iotHubActions.deleteItem(installedItem).subscribe(() => { if (deleted) {
this.reloadInstalledItems(); this.reloadInstalledItems();
}
}); });
} }

12
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-delete-dialog.component.ts

@ -20,8 +20,11 @@ import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { IotHubInstalledItem } from 'src/app/shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
export interface IotHubDeleteDialogData { export interface IotHubDeleteDialogData {
installedItemId: string;
itemName: string; itemName: string;
itemType?: string; itemType?: string;
} }
@ -38,13 +41,18 @@ export class TbIotHubDeleteDialogComponent extends DialogComponent<TbIotHubDelet
protected store: Store<AppState>, protected store: Store<AppState>,
protected router: Router, protected router: Router,
protected dialogRef: MatDialogRef<TbIotHubDeleteDialogComponent, boolean>, protected dialogRef: MatDialogRef<TbIotHubDeleteDialogComponent, boolean>,
@Inject(MAT_DIALOG_DATA) public data: IotHubDeleteDialogData @Inject(MAT_DIALOG_DATA) public data: IotHubDeleteDialogData,
private iotHubApiService: IotHubApiService
) { ) {
super(store, router, dialogRef); super(store, router, dialogRef);
} }
confirm(): void { confirm(): void {
this.dialogRef.close(true); this.iotHubApiService.deleteInstalledItem(this.data.installedItemId).subscribe(
() => {
this.dialogRef.close(true);
}
);
} }
cancel(): void { cancel(): void {

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

@ -157,7 +157,7 @@
<button mat-button [disabled]="resolvingPlan" (click)="cancel()">{{ 'action.cancel' | translate }}</button> <button mat-button [disabled]="resolvingPlan" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="install()"> <button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="install()">
@if (resolvingPlan) { @if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner> <mat-spinner matButtonIcon diameter="18" class="mr-2"></mat-spinner>
} }
{{ 'iot-hub.install' | translate }} {{ 'iot-hub.install' | translate }}
</button> </button>
@ -169,7 +169,7 @@
[disabled]="ruleChainInstallForm.invalid || resolvingPlan" [disabled]="ruleChainInstallForm.invalid || resolvingPlan"
(click)="onRuleChainInstall()"> (click)="onRuleChainInstall()">
@if (resolvingPlan) { @if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner> <mat-spinner matButtonIcon diameter="18" class="mr-2"></mat-spinner>
} }
{{ 'iot-hub.install' | translate }} {{ 'iot-hub.install' | translate }}
</button> </button>
@ -178,7 +178,7 @@
[disabled]="(activeSelectEntityConfig.required && !selectedEntityId) || resolvingPlan" [disabled]="(activeSelectEntityConfig.required && !selectedEntityId) || resolvingPlan"
(click)="onEntitySelectInstall()"> (click)="onEntitySelectInstall()">
@if (resolvingPlan) { @if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner> <mat-spinner matButtonIcon diameter="18" class="mr-2"></mat-spinner>
} }
{{ 'iot-hub.install' | translate }} {{ 'iot-hub.install' | translate }}
</button> </button>
@ -188,7 +188,7 @@
<button mat-button [disabled]="resolvingPlan" (click)="confirmOverwriteCancel()">{{ 'action.cancel' | translate }}</button> <button mat-button [disabled]="resolvingPlan" (click)="confirmOverwriteCancel()">{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="confirmOverwriteReplace()"> <button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="confirmOverwriteReplace()">
@if (resolvingPlan) { @if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner> <mat-spinner matButtonIcon diameter="18" class="mr-2"></mat-spinner>
} }
{{ 'iot-hub.rule-chain-overwrite-replace' | translate }} {{ 'iot-hub.rule-chain-overwrite-replace' | translate }}
</button> </button>
@ -204,7 +204,7 @@
@case ('installing') { @case ('installing') {
<button mat-button disabled>{{ 'action.cancel' | translate }}</button> <button mat-button disabled>{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" disabled> <button mat-flat-button color="primary" disabled>
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner> <mat-spinner matButtonIcon diameter="18" class="mr-2"></mat-spinner>
{{ 'iot-hub.installing' | translate }} {{ 'iot-hub.installing' | translate }}
</button> </button>
} }

17
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.html

@ -135,14 +135,8 @@
<tr mat-row *matRowDef="let _row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let _row; columns: displayedColumns;"></tr>
</table> </table>
@if (isLoading) {
<div class="flex flex-1 items-center justify-center py-12">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
@if (!isLoading && dataSource.length === 0) { @if (!isLoading && dataSource.length === 0) {
<div class="tb-iot-hub-empty-state flex flex-1 flex-col items-center justify-center py-20 text-center"> <div class="tb-iot-hub-empty-state flex flex-1 flex-col items-center justify-center text-center">
<div class="tb-no-data-bg"></div> <div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-installed-items' | translate }}</h3> <h3>{{ 'iot-hub.no-installed-items' | translate }}</h3>
<p>{{ 'iot-hub.no-installed-items-text' | translate }}</p> <p>{{ 'iot-hub.no-installed-items-text' | translate }}</p>
@ -166,4 +160,13 @@
(page)="onPageChange($event)" (page)="onPageChange($event)"
[showFirstLastButtons]="true"> [showFirstLastButtons]="true">
</mat-paginator> </mat-paginator>
@if (isLoading) {
<div class="absolute inset-0 backdrop-grayscale" style="background: rgba(255, 255, 255, 0.72); z-index: 100;">
<div class="flex h-full items-center justify-center">
<mat-spinner diameter="40"></mat-spinner>
</div>
</div>
}
</div> </div>

1
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.scss

@ -25,6 +25,7 @@
// Table + paginator container border // Table + paginator container border
.tb-installed-table-container { .tb-installed-table-container {
position: relative;
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
} }

4
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-installed-items-table.component.ts

@ -156,8 +156,8 @@ export class TbIotHubInstalledItemsTableComponent implements OnInit, OnChanges,
} }
deleteItem(item: IotHubInstalledItem): void { deleteItem(item: IotHubInstalledItem): void {
this.iotHubActions.deleteItem(item).subscribe(confirmed => { this.iotHubActions.deleteItem(item).subscribe(deleted => {
if (confirmed) { if (deleted) {
this.loadData(); this.loadData();
} }
}); });

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html

@ -96,7 +96,7 @@
</div> </div>
} @else { } @else {
<!-- Widget/Dashboard/Solution: image + description side by side --> <!-- Widget/Dashboard/Solution: image + description side by side -->
<div class="flex gap-6"> <div class="flex gap-6 lt-md:flex-col">
<div class="dlg-preview" <div class="dlg-preview"
[class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1"> [class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1">
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) { @if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {

4
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts

@ -220,8 +220,8 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
} }
deleteItem(): void { deleteItem(): void {
this.iotHubActions.deleteItem(this.installedItem).subscribe(confirmed => { this.iotHubActions.deleteItem(this.installedItem).subscribe(deleted => {
if (confirmed) { if (deleted) {
this.dialogRef.close('deleted'); this.dialogRef.close('deleted');
} }
}); });

8
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts

@ -247,7 +247,6 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
updateItem(item: MpItemVersionView): void { updateItem(item: MpItemVersionView): void {
const installedItem = this.getInstalledItem(item); const installedItem = this.getInstalledItem(item);
if (!installedItem) { return; }
this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => {
if (result === 'updated') { if (result === 'updated') {
this.reloadInstalledItems(); this.reloadInstalledItems();
@ -257,9 +256,10 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
deleteInstalledItem(item: MpItemVersionView): void { deleteInstalledItem(item: MpItemVersionView): void {
const installedItem = this.getInstalledItem(item); const installedItem = this.getInstalledItem(item);
if (!installedItem) { return; } this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => {
this.iotHubActions.deleteItem(installedItem).subscribe(() => { if (deleted) {
this.reloadInstalledItems(); this.reloadInstalledItems();
}
}); });
} }

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.html

@ -51,7 +51,7 @@
@case ('updating') { @case ('updating') {
<button mat-button disabled>{{ 'action.cancel' | translate }}</button> <button mat-button disabled>{{ 'action.cancel' | translate }}</button>
<button mat-flat-button color="primary" disabled> <button mat-flat-button color="primary" disabled>
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner> <mat-spinner matButtonIcon diameter="18" class="mr-2"></mat-spinner>
{{ 'iot-hub.updating' | translate }} {{ 'iot-hub.updating' | translate }}
</button> </button>
} }

4
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts

@ -46,7 +46,7 @@ export type UpdateState = 'confirm' | 'updating' | 'success' | 'error';
templateUrl: './iot-hub-update-dialog.component.html', templateUrl: './iot-hub-update-dialog.component.html',
styleUrls: ['./iot-hub-install-dialog.component.scss'] styleUrls: ['./iot-hub-install-dialog.component.scss']
}) })
export class TbIotHubUpdateDialogComponent extends DialogComponent<TbIotHubUpdateDialogComponent> { export class TbIotHubUpdateDialogComponent extends DialogComponent<TbIotHubUpdateDialogComponent, string | boolean> {
ItemType = ItemType; ItemType = ItemType;
@ -58,7 +58,7 @@ export class TbIotHubUpdateDialogComponent extends DialogComponent<TbIotHubUpdat
constructor( constructor(
protected store: Store<AppState>, protected store: Store<AppState>,
protected router: Router, protected router: Router,
protected dialogRef: MatDialogRef<TbIotHubUpdateDialogComponent>, protected dialogRef: MatDialogRef<TbIotHubUpdateDialogComponent, string | boolean>,
@Inject(MAT_DIALOG_DATA) public data: IotHubUpdateDialogData, @Inject(MAT_DIALOG_DATA) public data: IotHubUpdateDialogData,
private dialog: MatDialog, private dialog: MatDialog,
private dialogService: DialogService, private dialogService: DialogService,

3
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -683,6 +683,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
private reInitImpl() { private reInitImpl() {
this.displayNoData = false;
this.onDestroy(); this.onDestroy();
if (!this.typeParameters.useCustomDatasources) { if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe({ this.createDefaultSubscription().subscribe({
@ -726,7 +727,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
} }
if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
this.displayNoData = false;
this.reInit(); this.reInit();
} }
} }
@ -740,7 +740,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds); subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds);
} }
if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
this.displayNoData = false;
this.reInit(); this.reInit();
} }
} }

2
ui-ngx/src/app/modules/home/home.component.html

@ -79,7 +79,7 @@
<mat-progress-bar color="warn" style="z-index: 10; margin-bottom: -4px; width: 100%;" mode="indeterminate" <mat-progress-bar color="warn" style="z-index: 10; margin-bottom: -4px; width: 100%;" mode="indeterminate"
*ngIf="!hideLoadingBar && (isLoading$ | async)"> *ngIf="!hideLoadingBar && (isLoading$ | async)">
</mat-progress-bar> </mat-progress-bar>
<div tb-toast class="tb-main-content flex flex-1 flex-col"> <div #mainContent tb-toast class="tb-main-content flex flex-1 flex-col">
<router-outlet (activate)="activeComponentChanged($event)"></router-outlet> <router-outlet (activate)="activeComponentChanged($event)"></router-outlet>
</div> </div>
</div> </div>

3
ui-ngx/src/app/modules/home/home.component.ts

@ -58,6 +58,8 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
@ViewChild('sidenav') @ViewChild('sidenav')
sidenav: MatSidenav; sidenav: MatSidenav;
@ViewChild('mainContent', { static: true }) mainContent: ElementRef<HTMLElement>;
@ViewChild('searchInput') searchInputField: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef;
fullscreenEnabled = screenfull.isEnabled; fullscreenEnabled = screenfull.isEnabled;
@ -136,6 +138,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
activeComponentChanged(activeComponent: any) { activeComponentChanged(activeComponent: any) {
this.activeComponentService.setCurrentActiveComponent(activeComponent); this.activeComponentService.setCurrentActiveComponent(activeComponent);
this.mainContent?.nativeElement?.scrollTo({ top: 0, left: 0 });
if (!this.activeComponent) { if (!this.activeComponent) {
setTimeout(() => { setTimeout(() => {
this.updateActiveComponent(activeComponent); this.updateActiveComponent(activeComponent);

8
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss

@ -757,6 +757,14 @@
padding: 140px 16px 40px; padding: 140px 16px 40px;
} }
.tb-iot-hub-hero-cluster {
top: 15%;
}
.tb-iot-hub-hero-title {
font-size: 28px;
}
.tb-iot-hub-categories { .tb-iot-hub-categories {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

31
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts

@ -381,7 +381,6 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
updateItem(item: MpItemVersionView): void { updateItem(item: MpItemVersionView): void {
const installedItem = this.findInstalledItem(item); const installedItem = this.findInstalledItem(item);
if (!installedItem) { return; }
this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => { this.iotHubActions.updateItem(installedItem, item.version, item.id as string).subscribe(result => {
if (result === 'updated') { if (result === 'updated') {
this.reloadInstalledItems(item.type); this.reloadInstalledItems(item.type);
@ -404,20 +403,22 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
deleteInstalledItem(item: MpItemVersionView): void { deleteInstalledItem(item: MpItemVersionView): void {
const installedItem = this.findInstalledItem(item); const installedItem = this.findInstalledItem(item);
if (!installedItem) { return; } if (!installedItem) { return; }
this.iotHubActions.deleteItem(installedItem).subscribe(() => { this.iotHubActions.deleteItem(installedItem).subscribe((deleted) => {
this.installedItemsCount = Math.max(0, this.installedItemsCount - 1); if (deleted) {
if (item.type === ItemType.WIDGET) { this.installedItemsCount = Math.max(0, this.installedItemsCount - 1);
this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id); if (item.type === ItemType.WIDGET) {
} else if (item.type === ItemType.SOLUTION_TEMPLATE) { this.installedWidgets = this.installedWidgets.filter(i => i.id.id !== installedItem.id.id);
this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id); } else if (item.type === ItemType.SOLUTION_TEMPLATE) {
} else if (item.type === ItemType.DEVICE && this.installedDeviceCounts[item.itemId]) { this.installedSolutionTemplates = this.installedSolutionTemplates.filter(i => i.id.id !== installedItem.id.id);
this.installedDeviceCounts[item.itemId] = Math.max(0, this.installedDeviceCounts[item.itemId] - 1); } else if (item.type === ItemType.DEVICE && this.installedDeviceCounts[item.itemId]) {
} else if (item.type === ItemType.CALCULATED_FIELD && this.installedCalcFieldCounts[item.itemId]) { this.installedDeviceCounts[item.itemId] = Math.max(0, this.installedDeviceCounts[item.itemId] - 1);
this.installedCalcFieldCounts[item.itemId] = Math.max(0, this.installedCalcFieldCounts[item.itemId] - 1); } else if (item.type === ItemType.CALCULATED_FIELD && this.installedCalcFieldCounts[item.itemId]) {
} else if (item.type === ItemType.ALARM_RULE && this.installedAlarmRuleCounts[item.itemId]) { this.installedCalcFieldCounts[item.itemId] = Math.max(0, this.installedCalcFieldCounts[item.itemId] - 1);
this.installedAlarmRuleCounts[item.itemId] = Math.max(0, this.installedAlarmRuleCounts[item.itemId] - 1); } else if (item.type === ItemType.ALARM_RULE && this.installedAlarmRuleCounts[item.itemId]) {
} else if (item.type === ItemType.RULE_CHAIN && this.installedRuleChainCounts[item.itemId]) { this.installedAlarmRuleCounts[item.itemId] = Math.max(0, this.installedAlarmRuleCounts[item.itemId] - 1);
this.installedRuleChainCounts[item.itemId] = Math.max(0, this.installedRuleChainCounts[item.itemId] - 1); } else if (item.type === ItemType.RULE_CHAIN && this.installedRuleChainCounts[item.itemId]) {
this.installedRuleChainCounts[item.itemId] = Math.max(0, this.installedRuleChainCounts[item.itemId] - 1);
}
} }
}); });
} }

4
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.html

@ -28,9 +28,9 @@
@if (!updatesChecked) { @if (!updatesChecked) {
<button mat-flat-button color="primary" (click)="checkForUpdates()" [disabled]="isCheckingUpdates"> <button mat-flat-button color="primary" (click)="checkForUpdates()" [disabled]="isCheckingUpdates">
@if (isCheckingUpdates) { @if (isCheckingUpdates) {
<mat-spinner diameter="18"></mat-spinner> <mat-spinner matButtonIcon class="mr-2" diameter="18"></mat-spinner>
} @else { } @else {
<mat-icon>update</mat-icon> <mat-icon matButtonIcon>update</mat-icon>
} }
{{ 'iot-hub.check-for-updates' | translate }} {{ 'iot-hub.check-for-updates' | translate }}
</button> </button>

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

@ -140,8 +140,16 @@ export const getInstalledItemUrl = (descriptor?: IotHubInstalledItemDescriptor):
let query: string | null = null; let query: string | null = null;
switch (descriptor.type) { switch (descriptor.type) {
case 'DEVICE': case 'DEVICE':
entityId = descriptor.dashboardId?.id; if (descriptor.dashboardId) {
entityType = EntityType.DASHBOARD; entityId = descriptor.dashboardId?.id;
entityType = EntityType.DASHBOARD;
} else if (descriptor.createdEntityIds) {
const found = descriptor.createdEntityIds.find(id => id.entityType === EntityType.DEVICE);
if (found) {
entityId = found.id;
entityType = EntityType.DEVICE;
}
}
break; break;
case 'WIDGET': case 'WIDGET':
entityId = descriptor.widgetTypeId?.id; entityId = descriptor.widgetTypeId?.id;

6
ui-ngx/src/app/shared/models/units/volume-flow.ts

@ -25,6 +25,7 @@ export type VolumeFlowMetricUnits =
| 'L/min' | 'L/min'
| 'L/hr' | 'L/hr'
| 'm³/s' | 'm³/s'
| 'm³/min'
| 'm³/hr'; | 'm³/hr';
export type VolumeFlowImperialUnits = export type VolumeFlowImperialUnits =
@ -67,6 +68,11 @@ const METRIC: TbMeasureUnits<VolumeFlowMetricUnits> = {
tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'], tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'],
to_anchor: 1000, to_anchor: 1000,
}, },
'm³/min': {
name: 'unit.cubic-meters-per-minute',
tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'],
to_anchor: 1000 / 60,
},
'm³/hr': { 'm³/hr': {
name: 'unit.cubic-meters-per-hour', name: 'unit.cubic-meters-per-hour',
tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'], tags: ['airflow', 'ventilation', 'HVAC', 'gas flow rate'],

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

@ -6743,6 +6743,7 @@
"cubic-foot-per-minute": "Cubic foot per minute", "cubic-foot-per-minute": "Cubic foot per minute",
"cubic-meters-per-hour": "Cubic meters per hour", "cubic-meters-per-hour": "Cubic meters per hour",
"cubic-meters-per-second": "Cubic meters per second", "cubic-meters-per-second": "Cubic meters per second",
"cubic-meters-per-minute": "Cubic meters per minute",
"liter-per-second": "Liter per second", "liter-per-second": "Liter per second",
"liter-per-minute": "Liter per minute", "liter-per-minute": "Liter per minute",
"gallons-per-minute": "Gallons per minute", "gallons-per-minute": "Gallons per minute",

Loading…
Cancel
Save