Browse Source

Merge pull request #8247 from thingsboard/feature/menu-improvements

Menu improvements
pull/8255/head
Igor Kulikov 3 years ago
committed by GitHub
parent
commit
464e87fe89
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java
  2. 1
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
  3. 11
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java
  4. 17
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java
  5. 2
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  6. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  7. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java
  8. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java
  9. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java
  10. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java
  11. 5
      ui-ngx/angular.json
  12. 1
      ui-ngx/package.json
  13. 9
      ui-ngx/src/app/app.component.ts
  14. 12
      ui-ngx/src/app/core/auth/auth.actions.ts
  15. 42
      ui-ngx/src/app/core/auth/auth.effects.ts
  16. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  17. 22
      ui-ngx/src/app/core/auth/auth.reducer.ts
  18. 36
      ui-ngx/src/app/core/auth/auth.selectors.ts
  19. 18
      ui-ngx/src/app/core/auth/auth.service.ts
  20. 1
      ui-ngx/src/app/core/auth/public-api.ts
  21. 2
      ui-ngx/src/app/core/core.state.ts
  22. 6
      ui-ngx/src/app/core/http/alarm.service.ts
  23. 1
      ui-ngx/src/app/core/http/public-api.ts
  24. 54
      ui-ngx/src/app/core/http/user-settings.service.ts
  25. 41
      ui-ngx/src/app/core/services/active-component.service.ts
  26. 4
      ui-ngx/src/app/core/services/menu.models.ts
  27. 392
      ui-ngx/src/app/core/services/menu.service.ts
  28. 14
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  29. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.html
  30. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss
  31. 20
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts
  32. 6
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  33. 7
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  34. 2
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  35. 8
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  36. 12
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  37. 8
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html
  38. 6
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  39. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  40. 6
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  41. 12
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts
  42. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts
  43. 6
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts
  44. 2
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  45. 7
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss
  46. 34
      ui-ngx/src/app/modules/home/components/router-tabs.component.html
  47. 46
      ui-ngx/src/app/modules/home/components/router-tabs.component.scss
  48. 106
      ui-ngx/src/app/modules/home/components/router-tabs.component.ts
  49. 2
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html
  50. 7
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.scss
  51. 2
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  52. 7
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  53. 2
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  54. 6
      ui-ngx/src/app/modules/home/home.component.html
  55. 26
      ui-ngx/src/app/modules/home/home.component.ts
  56. 6
      ui-ngx/src/app/modules/home/menu/menu-link.component.html
  57. 9
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.html
  58. 2
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss
  59. 20
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts
  60. 1
      ui-ngx/src/app/modules/home/menu/side-menu.component.scss
  61. 4
      ui-ngx/src/app/modules/home/models/services.map.ts
  62. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  63. 262
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  64. 5
      ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss
  65. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  66. 2
      ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html
  67. 58
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  68. 34
      ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts
  69. 14
      ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts
  70. 12
      ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts
  71. 16
      ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts
  72. 438
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  73. 2
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  74. 53
      ui-ngx/src/app/modules/home/pages/entities/entities-routing.module.ts
  75. 30
      ui-ngx/src/app/modules/home/pages/entities/entities.module.ts
  76. 14
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts
  77. 54
      ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts
  78. 30
      ui-ngx/src/app/modules/home/pages/features/features.module.ts
  79. 2
      ui-ngx/src/app/modules/home/pages/home-pages.models.ts
  80. 8
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  81. 135
      ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts
  82. 30
      ui-ngx/src/app/modules/home/pages/notification/notification.module.ts
  83. 14
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts
  84. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  85. 20
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts
  86. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  87. 5
      ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html
  88. 9
      ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts
  89. 28
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  90. 2
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts
  91. 33
      ui-ngx/src/app/shared/components/breadcrumb.component.html
  92. 19
      ui-ngx/src/app/shared/components/breadcrumb.component.ts
  93. 7
      ui-ngx/src/app/shared/models/alarm.models.ts
  94. 1
      ui-ngx/src/app/shared/models/asset.models.ts
  95. 5
      ui-ngx/src/app/shared/models/device.models.ts
  96. 16
      ui-ngx/src/app/shared/models/entity-type.models.ts
  97. 1
      ui-ngx/src/app/shared/models/public-api.ts
  98. 23
      ui-ngx/src/app/shared/models/user-settings.models.ts
  99. 26
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  100. 1
      ui-ngx/src/assets/mdi.svg

1
application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java

@ -425,6 +425,7 @@ public abstract class BaseAssetProfileControllerTest extends AbstractControllerT
Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator); Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator);
List<AssetProfileInfo> assetProfileInfos = assetProfiles.stream().map(assetProfile -> new AssetProfileInfo(assetProfile.getId(), List<AssetProfileInfo> assetProfileInfos = assetProfiles.stream().map(assetProfile -> new AssetProfileInfo(assetProfile.getId(),
assetProfile.getTenantId(),
assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList()); assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList());
Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos); Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos);

1
application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java

@ -546,6 +546,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator);
List<DeviceProfileInfo> deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), List<DeviceProfileInfo> deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(),
deviceProfile.getTenantId(),
deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(), deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(),
deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList());

11
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java

@ -24,6 +24,7 @@ import lombok.Value;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID; import java.util.UUID;
@ -41,22 +42,28 @@ public class DeviceProfileInfo extends EntityInfo {
@ApiModelProperty(position = 6, value = "Type of the transport used to connect the device. Default transport supports HTTP, CoAP and MQTT.") @ApiModelProperty(position = 6, value = "Type of the transport used to connect the device. Default transport supports HTTP, CoAP and MQTT.")
private final DeviceTransportType transportType; private final DeviceTransportType transportType;
@ApiModelProperty(position = 7, value = "Tenant id.")
private final TenantId tenantId;
@JsonCreator @JsonCreator
public DeviceProfileInfo(@JsonProperty("id") EntityId id, public DeviceProfileInfo(@JsonProperty("id") EntityId id,
@JsonProperty("tenantId") TenantId tenantId,
@JsonProperty("name") String name, @JsonProperty("name") String name,
@JsonProperty("image") String image, @JsonProperty("image") String image,
@JsonProperty("defaultDashboardId") DashboardId defaultDashboardId, @JsonProperty("defaultDashboardId") DashboardId defaultDashboardId,
@JsonProperty("type") DeviceProfileType type, @JsonProperty("type") DeviceProfileType type,
@JsonProperty("transportType") DeviceTransportType transportType) { @JsonProperty("transportType") DeviceTransportType transportType) {
super(id, name); super(id, name);
this.tenantId = tenantId;
this.image = image; this.image = image;
this.defaultDashboardId = defaultDashboardId; this.defaultDashboardId = defaultDashboardId;
this.type = type; this.type = type;
this.transportType = transportType; this.transportType = transportType;
} }
public DeviceProfileInfo(UUID uuid, String name, String image, UUID defaultDashboardId, DeviceProfileType type, DeviceTransportType transportType) { public DeviceProfileInfo(UUID uuid, UUID tenantId, String name, String image, UUID defaultDashboardId, DeviceProfileType type, DeviceTransportType transportType) {
super(EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE_PROFILE, uuid), name); super(EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE_PROFILE, uuid), name);
this.tenantId = new TenantId(tenantId);
this.image = image; this.image = image;
this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null; this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null;
this.type = type; this.type = type;
@ -64,7 +71,7 @@ public class DeviceProfileInfo extends EntityInfo {
} }
public DeviceProfileInfo(DeviceProfile profile) { public DeviceProfileInfo(DeviceProfile profile) {
this(profile.getId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId(), this(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId(),
profile.getType(), profile.getTransportType()); profile.getType(), profile.getTransportType());
} }

17
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID; import java.util.UUID;
@ -39,24 +40,30 @@ public class AssetProfileInfo extends EntityInfo {
@ApiModelProperty(position = 4, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to asset details.") @ApiModelProperty(position = 4, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to asset details.")
private final DashboardId defaultDashboardId; private final DashboardId defaultDashboardId;
@ApiModelProperty(position = 5, value = "Tenant id.")
private final TenantId tenantId;
@JsonCreator @JsonCreator
public AssetProfileInfo(@JsonProperty("id") EntityId id, public AssetProfileInfo(@JsonProperty("id") EntityId id,
@JsonProperty("name") String name, @JsonProperty("tenantId") TenantId tenantId,
@JsonProperty("image") String image, @JsonProperty("name") String name,
@JsonProperty("defaultDashboardId") DashboardId defaultDashboardId) { @JsonProperty("image") String image,
@JsonProperty("defaultDashboardId") DashboardId defaultDashboardId) {
super(id, name); super(id, name);
this.tenantId = tenantId;
this.image = image; this.image = image;
this.defaultDashboardId = defaultDashboardId; this.defaultDashboardId = defaultDashboardId;
} }
public AssetProfileInfo(UUID uuid, String name, String image, UUID defaultDashboardId) { public AssetProfileInfo(UUID uuid, UUID tenantId, String name, String image, UUID defaultDashboardId) {
super(EntityIdFactory.getByTypeAndUuid(EntityType.ASSET_PROFILE, uuid), name); super(EntityIdFactory.getByTypeAndUuid(EntityType.ASSET_PROFILE, uuid), name);
this.tenantId = new TenantId(tenantId);
this.image = image; this.image = image;
this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null; this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null;
} }
public AssetProfileInfo(AssetProfile profile) { public AssetProfileInfo(AssetProfile profile) {
this(profile.getId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId()); this(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId());
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -293,7 +293,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService<AssetPr
}; };
private AssetProfileInfo toAssetProfileInfo(AssetProfile profile) { private AssetProfileInfo toAssetProfileInfo(AssetProfile profile) {
return profile == null ? null : new AssetProfileInfo(profile.getId(), profile.getName(), profile.getImage(), return profile == null ? null : new AssetProfileInfo(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(),
profile.getDefaultDashboardId()); profile.getDefaultDashboardId());
} }

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -315,7 +315,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
}; };
private DeviceProfileInfo toDeviceProfileInfo(DeviceProfile profile) { private DeviceProfileInfo toDeviceProfileInfo(DeviceProfile profile) {
return profile == null ? null : new DeviceProfileInfo(profile.getId(), profile.getName(), profile.getImage(), return profile == null ? null : new DeviceProfileInfo(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(),
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType()); profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
} }

6
dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java

@ -28,7 +28,7 @@ import java.util.UUID;
public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity, UUID>, ExportableEntityRepository<AssetProfileEntity> { public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity, UUID>, ExportableEntityRepository<AssetProfileEntity> {
@Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.name, a.image, a.defaultDashboardId) " + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " +
"FROM AssetProfileEntity a " + "FROM AssetProfileEntity a " +
"WHERE a.id = :assetProfileId") "WHERE a.id = :assetProfileId")
AssetProfileInfo findAssetProfileInfoById(@Param("assetProfileId") UUID assetProfileId); AssetProfileInfo findAssetProfileInfoById(@Param("assetProfileId") UUID assetProfileId);
@ -39,7 +39,7 @@ public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
Pageable pageable); Pageable pageable);
@Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.name, a.image, a.defaultDashboardId) " + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " +
"FROM AssetProfileEntity a WHERE " + "FROM AssetProfileEntity a WHERE " +
"a.tenantId = :tenantId AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") "a.tenantId = :tenantId AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<AssetProfileInfo> findAssetProfileInfos(@Param("tenantId") UUID tenantId, Page<AssetProfileInfo> findAssetProfileInfos(@Param("tenantId") UUID tenantId,
@ -50,7 +50,7 @@ public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity
"WHERE a.tenantId = :tenantId AND a.isDefault = true") "WHERE a.tenantId = :tenantId AND a.isDefault = true")
AssetProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); AssetProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId);
@Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.name, a.image, a.defaultDashboardId) " + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " +
"FROM AssetProfileEntity a " + "FROM AssetProfileEntity a " +
"WHERE a.tenantId = :tenantId AND a.isDefault = true") "WHERE a.tenantId = :tenantId AND a.isDefault = true")
AssetProfileInfo findDefaultAssetProfileInfo(@Param("tenantId") UUID tenantId); AssetProfileInfo findDefaultAssetProfileInfo(@Param("tenantId") UUID tenantId);

8
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java

@ -29,7 +29,7 @@ import java.util.UUID;
public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEntity, UUID>, ExportableEntityRepository<DeviceProfileEntity> { public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEntity, UUID>, ExportableEntityRepository<DeviceProfileEntity> {
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d " + "FROM DeviceProfileEntity d " +
"WHERE d.id = :deviceProfileId") "WHERE d.id = :deviceProfileId")
DeviceProfileInfo findDeviceProfileInfoById(@Param("deviceProfileId") UUID deviceProfileId); DeviceProfileInfo findDeviceProfileInfoById(@Param("deviceProfileId") UUID deviceProfileId);
@ -40,14 +40,14 @@ public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEnti
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
Pageable pageable); Pageable pageable);
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d WHERE " + "FROM DeviceProfileEntity d WHERE " +
"d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId, Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
Pageable pageable); Pageable pageable);
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d WHERE " + "FROM DeviceProfileEntity d WHERE " +
"d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId, Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId,
@ -59,7 +59,7 @@ public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEnti
"WHERE d.tenantId = :tenantId AND d.isDefault = true") "WHERE d.tenantId = :tenantId AND d.isDefault = true")
DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId);
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d " + "FROM DeviceProfileEntity d " +
"WHERE d.tenantId = :tenantId AND d.isDefault = true") "WHERE d.tenantId = :tenantId AND d.isDefault = true")
DeviceProfileInfo findDefaultDeviceProfileInfo(@Param("tenantId") UUID tenantId); DeviceProfileInfo findDefaultDeviceProfileInfo(@Param("tenantId") UUID tenantId);

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java

@ -254,7 +254,7 @@ public abstract class BaseAssetProfileServiceTest extends AbstractServiceTest {
Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator); Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator);
List<AssetProfileInfo> assetProfileInfos = assetProfiles.stream() List<AssetProfileInfo> assetProfileInfos = assetProfiles.stream()
.map(assetProfile -> new AssetProfileInfo(assetProfile.getId(), .map(assetProfile -> new AssetProfileInfo(assetProfile.getId(), assetProfile.getTenantId(),
assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList()); assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList());
Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos); Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos);

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java

@ -353,7 +353,7 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest {
Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator);
List<DeviceProfileInfo> deviceProfileInfos = deviceProfiles.stream() List<DeviceProfileInfo> deviceProfileInfos = deviceProfiles.stream()
.map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), .map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), deviceProfile.getTenantId(),
deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(), deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(),
deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList());

5
ui-ngx/angular.json

@ -29,6 +29,11 @@
"assets": [ "assets": [
"src/thingsboard.ico", "src/thingsboard.ico",
"src/assets", "src/assets",
{
"glob": "*.svg",
"input": "./node_modules/@mdi/svg/svg/",
"output": "/assets/mdi/"
},
{ {
"glob": "worker-html.js", "glob": "worker-html.js",
"input": "./node_modules/ace-builds/src-noconflict/", "input": "./node_modules/ace-builds/src-noconflict/",

1
ui-ngx/package.json

@ -36,6 +36,7 @@
"@material-ui/core": "4.12.3", "@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2", "@material-ui/icons": "4.11.2",
"@material-ui/pickers": "3.3.10", "@material-ui/pickers": "3.3.10",
"@mdi/svg": "^7.1.96",
"@messageformat/core": "^3.0.1", "@messageformat/core": "^3.0.1",
"@ngrx/effects": "^15.3.0", "@ngrx/effects": "^15.3.0",
"@ngrx/store": "^15.3.0", "@ngrx/store": "^15.3.0",

9
ui-ngx/src/app/app.component.ts

@ -47,8 +47,13 @@ export class AppComponent implements OnInit {
console.log(`ThingsBoard Version: ${env.tbVersion}`); console.log(`ThingsBoard Version: ${env.tbVersion}`);
this.matIconRegistry.addSvgIconSetInNamespace('mdi', this.matIconRegistry.addSvgIconResolver((name, namespace) => {
this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg')); if (namespace === 'mdi') {
return this.domSanitizer.bypassSecurityTrustResourceUrl(`./assets/mdi/${name}.svg`);
} else {
return null;
}
});
this.matIconRegistry.addSvgIconLiteral( this.matIconRegistry.addSvgIconLiteral(
'google-logo', 'google-logo',

12
ui-ngx/src/app/core/auth/auth.actions.ts

@ -24,7 +24,8 @@ export enum AuthActionTypes {
LOAD_USER = '[Auth] Load User', LOAD_USER = '[Auth] Load User',
UPDATE_USER_DETAILS = '[Auth] Update User Details', UPDATE_USER_DETAILS = '[Auth] Update User Details',
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id', UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository' UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository',
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section'
} }
export class ActionAuthAuthenticated implements Action { export class ActionAuthAuthenticated implements Action {
@ -61,5 +62,12 @@ export class ActionAuthUpdateHasRepository implements Action {
constructor(readonly payload: { hasRepository: boolean }) {} constructor(readonly payload: { hasRepository: boolean }) {}
} }
export class ActionPreferencesUpdateOpenedMenuSection implements Action {
readonly type = AuthActionTypes.UPDATE_OPENED_MENU_SECTION;
constructor(readonly payload: { path: string; opened: boolean }) {}
}
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository; ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository |
ActionPreferencesUpdateOpenedMenuSection;

42
ui-ngx/src/app/core/auth/auth.effects.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2023 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.
///
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UserSettingsService } from '@core/http/user-settings.service';
import { mergeMap, withLatestFrom } from 'rxjs/operators';
import { AuthActions, AuthActionTypes } from '@core/auth/auth.actions';
import { selectAuthState } from '@core/auth/auth.selectors';
@Injectable()
export class AuthEffects {
constructor(
private actions$: Actions<AuthActions>,
private store: Store<AppState>,
private userSettingsService: UserSettingsService
) {
}
persistOpenedMenuSections = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.UPDATE_OPENED_MENU_SECTION,
),
withLatestFrom(this.store.pipe(select(selectAuthState))),
mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections }))
), {dispatch: false});
}

2
ui-ngx/src/app/core/auth/auth.models.ts

@ -15,6 +15,7 @@
/// ///
import { AuthUser, User } from '@shared/models/user.model'; import { AuthUser, User } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
export interface SysParamsState { export interface SysParamsState {
userTokenAccessEnabled: boolean; userTokenAccessEnabled: boolean;
@ -22,6 +23,7 @@ export interface SysParamsState {
edgesSupportEnabled: boolean; edgesSupportEnabled: boolean;
hasRepository: boolean; hasRepository: boolean;
tbelEnabled: boolean; tbelEnabled: boolean;
userSettings: UserSettings;
} }
export interface AuthPayload extends SysParamsState { export interface AuthPayload extends SysParamsState {

22
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -16,6 +16,7 @@
import { AuthPayload, AuthState } from './auth.models'; import { AuthPayload, AuthState } from './auth.models';
import { AuthActions, AuthActionTypes } from './auth.actions'; import { AuthActions, AuthActionTypes } from './auth.actions';
import { initialUserSettings } from '@shared/models/user-settings.models';
const emptyUserAuthState: AuthPayload = { const emptyUserAuthState: AuthPayload = {
authUser: null, authUser: null,
@ -25,7 +26,8 @@ const emptyUserAuthState: AuthPayload = {
allowedDashboardIds: [], allowedDashboardIds: [],
edgesSupportEnabled: false, edgesSupportEnabled: false,
hasRepository: false, hasRepository: false,
tbelEnabled: false tbelEnabled: false,
userSettings: initialUserSettings
}; };
export const initialState: AuthState = { export const initialState: AuthState = {
@ -35,10 +37,10 @@ export const initialState: AuthState = {
...emptyUserAuthState ...emptyUserAuthState
}; };
export function authReducer( export const authReducer = (
state: AuthState = initialState, state: AuthState = initialState,
action: AuthActions action: AuthActions
): AuthState { ): AuthState => {
switch (action.type) { switch (action.type) {
case AuthActionTypes.AUTHENTICATED: case AuthActionTypes.AUTHENTICATED:
return { ...state, isAuthenticated: true, ...action.payload }; return { ...state, isAuthenticated: true, ...action.payload };
@ -59,7 +61,19 @@ export function authReducer(
case AuthActionTypes.UPDATE_HAS_REPOSITORY: case AuthActionTypes.UPDATE_HAS_REPOSITORY:
return { ...state, ...action.payload}; return { ...state, ...action.payload};
case AuthActionTypes.UPDATE_OPENED_MENU_SECTION:
const openedMenuSections = new Set(state.userSettings.openedMenuSections);
if (action.payload.opened) {
if (!openedMenuSections.has(action.payload.path)) {
openedMenuSections.add(action.payload.path);
}
} else {
openedMenuSections.delete(action.payload.path);
}
const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
return { ...state, ...{ userSettings }};
default: default:
return state; return state;
} }
} };

36
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -20,6 +20,7 @@ import { AppState } from '../core.state';
import { AuthState } from './auth.models'; import { AuthState } from './auth.models';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
export const selectAuthState = createFeatureSelector< AuthState>( export const selectAuthState = createFeatureSelector< AuthState>(
'auth' 'auth'
@ -65,18 +66,45 @@ export const selectTbelEnabled = createSelector(
(state: AuthState) => state.tbelEnabled (state: AuthState) => state.tbelEnabled
); );
export function getCurrentAuthState(store: Store<AppState>): AuthState { export const selectUserSettings = createSelector(
selectAuthState,
(state: AuthState) => state.userSettings
);
export const selectOpenedMenuSections = createSelector(
selectAuthState,
(state: AuthState) => state.userSettings.openedMenuSections
);
export const getCurrentAuthState = (store: Store<AppState>): AuthState => {
let state: AuthState; let state: AuthState;
store.pipe(select(selectAuth), take(1)).subscribe( store.pipe(select(selectAuth), take(1)).subscribe(
val => state = val val => state = val
); );
return state; return state;
} };
export function getCurrentAuthUser(store: Store<AppState>): AuthUser { export const getCurrentAuthUser = (store: Store<AppState>): AuthUser => {
let authUser: AuthUser; let authUser: AuthUser;
store.pipe(select(selectAuthUser), take(1)).subscribe( store.pipe(select(selectAuthUser), take(1)).subscribe(
val => authUser = val val => authUser = val
); );
return authUser; return authUser;
} };
export const getCurrentUserSettings = (store: Store<AppState>): UserSettings => {
let userSettings: UserSettings;
store.pipe(select(selectUserSettings), take(1)).subscribe(
val => userSettings = val
);
return userSettings;
};
export const getCurrentOpenedMenuSections = (store: Store<AppState>): string[] => {
let openedMenuSections: string[];
store.pipe(select(selectOpenedMenuSections), take(1)).subscribe(
val => openedMenuSections = val
);
return openedMenuSections;
};

18
ui-ngx/src/app/core/auth/auth.service.ts

@ -48,6 +48,8 @@ import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models';
import { isMobileApp } from '@core/utils'; import { isMobileApp } from '@core/utils';
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models'; import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models';
import { UserPasswordPolicy } from '@shared/models/settings.models'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { UserSettings } from '@shared/models/user-settings.models';
import { UserSettingsService } from '@core/http/user-settings.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -66,6 +68,7 @@ export class AuthService {
private dashboardService: DashboardService, private dashboardService: DashboardService,
private adminService: AdminService, private adminService: AdminService,
private translate: TranslateService, private translate: TranslateService,
private userSettingsService: UserSettingsService,
private dialog: MatDialog private dialog: MatDialog
) { ) {
} }
@ -127,7 +130,8 @@ export class AuthService {
} }
public checkTwoFaVerificationCode(providerType: TwoFactorAuthProviderType, verificationCode: number): Observable<LoginResponse> { public checkTwoFaVerificationCode(providerType: TwoFactorAuthProviderType, verificationCode: number): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`/api/auth/2fa/verification/check?providerType=${providerType}&verificationCode=${verificationCode}`, return this.http.post<LoginResponse>
(`/api/auth/2fa/verification/check?providerType=${providerType}&verificationCode=${verificationCode}`,
null, defaultHttpOptions(false, true)).pipe( null, defaultHttpOptions(false, true)).pipe(
tap((loginResponse: LoginResponse) => { tap((loginResponse: LoginResponse) => {
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true);
@ -490,12 +494,17 @@ export class AuthService {
} }
} }
private loadUserSettings(): Observable<UserSettings> {
return this.userSettingsService.loadUserSettings();
}
private loadSystemParams(authPayload: AuthPayload): Observable<SysParamsState> { private loadSystemParams(authPayload: AuthPayload): Observable<SysParamsState> {
const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
this.fetchAllowedDashboardIds(authPayload), this.fetchAllowedDashboardIds(authPayload),
this.loadIsEdgesSupportEnabled(), this.loadIsEdgesSupportEnabled(),
this.loadHasRepository(authPayload.authUser), this.loadHasRepository(authPayload.authUser),
this.loadTbelEnabled(authPayload.authUser), this.loadTbelEnabled(authPayload.authUser),
this.loadUserSettings(),
this.timeService.loadMaxDatapointsLimit()]; this.timeService.loadMaxDatapointsLimit()];
return forkJoin(sources) return forkJoin(sources)
.pipe(map((data) => { .pipe(map((data) => {
@ -504,10 +513,9 @@ export class AuthService {
const edgesSupportEnabled: boolean = data[2] as boolean; const edgesSupportEnabled: boolean = data[2] as boolean;
const hasRepository: boolean = data[3] as boolean; const hasRepository: boolean = data[3] as boolean;
const tbelEnabled: boolean = data[4] as boolean; const tbelEnabled: boolean = data[4] as boolean;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled}; const userSettings = data[5] as UserSettings;
}, catchError((err) => { return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled, userSettings};
return of({}); }, catchError((err) => of({}))));
})));
} }
public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable<LoginResponse> { public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable<LoginResponse> {

1
ui-ngx/src/app/core/auth/public-api.ts

@ -15,6 +15,7 @@
/// ///
export * from './auth.actions'; export * from './auth.actions';
export * from './auth.effects';
export * from './auth.models'; export * from './auth.models';
export * from './auth.reducer'; export * from './auth.reducer';
export * from './auth.selectors'; export * from './auth.selectors';

2
ui-ngx/src/app/core/core.state.ts

@ -32,6 +32,7 @@ import { SettingsEffects } from '@app/core/settings/settings.effects';
import { NotificationState } from '@app/core/notification/notification.models'; import { NotificationState } from '@app/core/notification/notification.models';
import { notificationReducer } from '@app/core/notification/notification.reducer'; import { notificationReducer } from '@app/core/notification/notification.reducer';
import { NotificationEffects } from '@app/core/notification/notification.effects'; import { NotificationEffects } from '@app/core/notification/notification.effects';
import { AuthEffects } from '@core/auth/auth.effects';
export const reducers: ActionReducerMap<AppState> = { export const reducers: ActionReducerMap<AppState> = {
load: loadReducer, load: loadReducer,
@ -49,6 +50,7 @@ if (!env.production) {
} }
export const effects: Type<any>[] = [ export const effects: Type<any>[] = [
AuthEffects,
SettingsEffects, SettingsEffects,
NotificationEffects NotificationEffects
]; ];

6
ui-ngx/src/app/core/http/alarm.service.ts

@ -78,6 +78,12 @@ export class AlarmService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getAllAlarms(query: AlarmQuery,
config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/alarms${query.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus, public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus,
config?: RequestConfig): Observable<AlarmSeverity> { config?: RequestConfig): Observable<AlarmSeverity> {
let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`; let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`;

1
ui-ngx/src/app/core/http/public-api.ts

@ -39,4 +39,5 @@ export * from './tenant.service';
export * from './tenant-profile.service'; export * from './tenant-profile.service';
export * from './ui-settings.service'; export * from './ui-settings.service';
export * from './user.service'; export * from './user.service';
export * from './user-settings.service';
export * from './widget.service'; export * from './widget.service';

54
ui-ngx/src/app/core/http/user-settings.service.ts

@ -0,0 +1,54 @@
///
/// Copyright © 2016-2023 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.
///
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig } from '@core/http/http-utils';
@Injectable({
providedIn: 'root'
})
export class UserSettingsService {
constructor(
private http: HttpClient
) {}
public loadUserSettings(): Observable<UserSettings> {
return this.http.get<UserSettings>('/api/user/settings', defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true})).pipe(
map((settings) => (!settings || !Object.keys(settings).length) ? initialUserSettings : settings)
);
}
public saveUserSettings(userSettings: UserSettings): Observable<UserSettings> {
return this.http.post<UserSettings>('/api/user/settings', userSettings,
defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}));
}
public putUserSettings(userSettingsData: Partial<UserSettings>): Observable<void> {
return this.http.put<void>('/api/user/settings', userSettingsData,
defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}));
}
public deleteUserSettings(paths: string[]) {
return this.http.delete(`/api/user/settings/${paths.join(',')}`,
defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}));
}
}

41
ui-ngx/src/app/core/services/active-component.service.ts

@ -0,0 +1,41 @@
///
/// Copyright © 2016-2023 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.
///
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ActiveComponentService {
private activeComponent: any;
private activeComponentChangedSubject: Subject<any> = new Subject<any>();
public getCurrentActiveComponent(): any {
return this.activeComponent;
}
public setCurrentActiveComponent(component: any): void {
this.activeComponent = component;
this.activeComponentChangedSubject.next(component);
}
public onActiveComponentChanged(): Observable<any> {
return this.activeComponentChangedSubject.asObservable();
}
}

4
ui-ngx/src/app/core/services/menu.models.ts

@ -24,8 +24,10 @@ export interface MenuSection extends HasUUID{
path: string; path: string;
icon: string; icon: string;
isMdiIcon?: boolean; isMdiIcon?: boolean;
height?: string;
pages?: Array<MenuSection>; pages?: Array<MenuSection>;
opened?: boolean;
disabled?: boolean;
rootOnly?: boolean;
} }
export interface HomeSection { export interface HomeSection {

392
ui-ngx/src/app/core/services/menu.service.ts

@ -15,26 +15,28 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AuthService } from '../auth/auth.service';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state'; import { AppState } from '../core.state';
import { selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
import { take } from 'rxjs/operators'; import { filter, take } from 'rxjs/operators';
import { HomeSection, MenuSection } from '@core/services/menu.models'; import { HomeSection, MenuSection } from '@core/services/menu.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { guid } from '@core/utils'; import { guid } from '@core/utils';
import { AuthState } from '@core/auth/auth.models'; import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MenuService { export class MenuService {
currentMenuSections: Array<MenuSection>;
menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]); menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]);
homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]); homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]);
constructor(private store: Store<AppState>, private authService: AuthService) { constructor(private store: Store<AppState>,
private router: Router) {
this.store.pipe(select(selectIsAuthenticated)).subscribe( this.store.pipe(select(selectIsAuthenticated)).subscribe(
(authenticated: boolean) => { (authenticated: boolean) => {
if (authenticated) { if (authenticated) {
@ -42,36 +44,50 @@ export class MenuService {
} }
} }
); );
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(
() => {
this.updateOpenedMenuSections();
}
);
} }
private buildMenu() { private buildMenu() {
this.store.pipe(select(selectAuth), take(1)).subscribe( this.store.pipe(select(selectAuth), take(1)).subscribe(
(authState: AuthState) => { (authState: AuthState) => {
if (authState.authUser) { if (authState.authUser) {
let menuSections: Array<MenuSection>;
let homeSections: Array<HomeSection>; let homeSections: Array<HomeSection>;
switch (authState.authUser.authority) { switch (authState.authUser.authority) {
case Authority.SYS_ADMIN: case Authority.SYS_ADMIN:
menuSections = this.buildSysAdminMenu(authState); this.currentMenuSections = this.buildSysAdminMenu();
homeSections = this.buildSysAdminHome(authState); homeSections = this.buildSysAdminHome();
break; break;
case Authority.TENANT_ADMIN: case Authority.TENANT_ADMIN:
menuSections = this.buildTenantAdminMenu(authState); this.currentMenuSections = this.buildTenantAdminMenu(authState);
homeSections = this.buildTenantAdminHome(authState); homeSections = this.buildTenantAdminHome(authState);
break; break;
case Authority.CUSTOMER_USER: case Authority.CUSTOMER_USER:
menuSections = this.buildCustomerUserMenu(authState); this.currentMenuSections = this.buildCustomerUserMenu(authState);
homeSections = this.buildCustomerUserHome(authState); homeSections = this.buildCustomerUserHome(authState);
break; break;
} }
this.menuSections$.next(menuSections); this.updateOpenedMenuSections();
this.menuSections$.next(this.currentMenuSections);
this.homeSections$.next(homeSections); this.homeSections$.next(homeSections);
} }
} }
); );
} }
private buildSysAdminMenu(authState: AuthState): Array<MenuSection> { private updateOpenedMenuSections() {
const url = this.router.url;
const openedMenuSections = getCurrentOpenedMenuSections(this.store);
this.currentMenuSections.filter(section => section.type === 'toggle' &&
(url.startsWith(section.path) || openedMenuSections.includes(section.path))).forEach(
section => section.opened = true
);
}
private buildSysAdminMenu(): Array<MenuSection> {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
@ -98,17 +114,33 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'widget.widget-library', name: 'admin.resources',
type: 'link', type: 'toggle',
path: '/widgets-bundles', path: '/resources',
icon: 'now_widgets' icon: 'folder',
pages: [
{
id: guid(),
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-bundles',
icon: 'now_widgets'
},
{
id: guid(),
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
}
]
}, },
{ {
id: guid(), id: guid(),
name: 'admin.system-settings', name: 'admin.settings',
type: 'toggle', type: 'link',
path: '/settings', path: '/settings',
height: '320px',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
@ -127,54 +159,58 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'admin.sms-provider', name: 'admin.notifications',
type: 'link', type: 'link',
path: '/settings/sms-provider', path: '/settings/notifications',
icon: 'sms' icon: 'mdi:message-badge',
isMdiIcon: true
}, },
{ {
id: guid(), id: guid(),
name: 'admin.security-settings', name: 'admin.queues',
type: 'link', type: 'link',
path: '/settings/security-settings', path: '/settings/queues',
icon: 'security' icon: 'swap_calls'
}, },
]
},
{
id: guid(),
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security',
pages: [
{ {
id: guid(), id: guid(),
name: 'admin.oauth2.oauth2', name: 'admin.general',
type: 'link', type: 'link',
path: '/settings/oauth2', path: '/security-settings/general',
icon: 'security' icon: 'settings_applications'
}, },
{ {
id: guid(), id: guid(),
name: 'admin.2fa.2fa', name: 'admin.2fa.2fa',
type: 'link', type: 'link',
path: '/settings/2fa', path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication', icon: 'mdi:two-factor-authentication',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: guid(),
name: 'resource.resources-library', name: 'admin.oauth2.oauth2',
type: 'link',
path: '/settings/resources-library',
icon: 'folder'
},
{
id: guid(),
name: 'admin.queues',
type: 'link', type: 'link',
path: '/settings/queues', path: '/security-settings/oauth2',
icon: 'swap_calls' icon: 'mdi:shield-account',
}, isMdiIcon: true
}
] ]
} }
); );
return sections; return sections;
} }
private buildSysAdminHome(authState: AuthState): Array<HomeSection> { private buildSysAdminHome(): Array<HomeSection> {
const homeSections: Array<HomeSection> = []; const homeSections: Array<HomeSection> = [];
homeSections.push( homeSections.push(
{ {
@ -265,38 +301,53 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'rulechain.rulechains', name: 'alarm.alarms',
type: 'link',
path: '/ruleChains',
icon: 'settings_ethernet'
},
{
id: guid(),
name: 'customer.customers',
type: 'link', type: 'link',
path: '/customers', path: '/alarms',
icon: 'supervisor_account' icon: 'notifications'
}, },
{ {
id: guid(), id: guid(),
name: 'asset.assets', name: 'dashboard.dashboards',
type: 'link', type: 'link',
path: '/assets', path: '/dashboards',
icon: 'domain' icon: 'dashboards'
}, },
{ {
id: guid(), id: guid(),
name: 'device.devices', name: 'entity.entities',
type: 'link', type: 'toggle',
path: '/devices', path: '/entities',
icon: 'devices_other' icon: 'category',
pages: [
{
id: guid(),
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
},
{
id: guid(),
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
},
{
id: guid(),
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
icon: 'view_quilt'
}
]
}, },
{ {
id: guid(), id: guid(),
name: 'profiles.profiles', name: 'profiles.profiles',
type: 'toggle', type: 'toggle',
path: '/profiles', path: '/profiles',
height: '80px',
icon: 'badge', icon: 'badge',
pages: [ pages: [
{ {
@ -319,36 +370,28 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'ota-update.ota-updates', name: 'customer.customers',
type: 'link',
path: '/otaUpdates',
icon: 'memory'
},
{
id: guid(),
name: 'entity-view.entity-views',
type: 'link', type: 'link',
path: '/entityViews', path: '/customers',
icon: 'view_quilt' icon: 'supervisor_account'
} }
); );
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
sections.push( sections.push(
{
id: guid(),
name: 'edge.edge-instances',
type: 'link',
path: '/edgeInstances',
icon: 'router'
},
{ {
id: guid(), id: guid(),
name: 'edge.management', name: 'edge.management',
type: 'toggle', type: 'toggle',
path: '/edgeManagement', path: '/edgeManagement',
height: '40px',
icon: 'settings_input_antenna', icon: 'settings_input_antenna',
pages: [ pages: [
{
id: guid(),
name: 'edge.instances',
type: 'link',
path: '/edgeManagement/instances',
icon: 'router'
},
{ {
id: guid(), id: guid(),
name: 'edge.rulechain-templates', name: 'edge.rulechain-templates',
@ -363,31 +406,104 @@ export class MenuService {
sections.push( sections.push(
{ {
id: guid(), id: guid(),
name: 'widget.widget-library', name: 'feature.advanced-features',
type: 'link', type: 'toggle',
path: '/widgets-bundles', path: '/features',
icon: 'now_widgets' icon: 'construction',
}, pages: [
{ {
id: guid(), id: guid(),
name: 'dashboard.dashboards', name: 'rulechain.rulechains',
type: 'link', type: 'link',
path: '/dashboards', path: '/features/ruleChains',
icon: 'dashboards' icon: 'settings_ethernet'
},
{
id: guid(),
name: 'ota-update.ota-updates',
type: 'link',
path: '/features/otaUpdates',
icon: 'memory'
},
{
id: guid(),
name: 'version-control.version-control',
type: 'link',
path: '/features/vc',
icon: 'history'
}
]
}, },
{ {
id: guid(), id: guid(),
name: 'version-control.version-control', name: 'admin.resources',
type: 'link', type: 'toggle',
path: '/vc', path: '/resources',
icon: 'history' icon: 'folder',
pages: [
{
id: guid(),
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-bundles',
icon: 'now_widgets'
},
{
id: guid(),
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
}
]
}, },
{ {
id: guid(), id: guid(),
name: 'audit-log.audit-logs', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/auditLogs', path: '/notification',
icon: 'track_changes' icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: guid(),
name: 'notification.inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
},
{
id: guid(),
name: 'notification.sent',
type: 'link',
path: '/notification/sent',
icon: 'outbox'
},
{
id: guid(),
name: 'notification.templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
},
{
id: guid(),
name: 'notification.recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
},
{
id: guid(),
name: 'notification.rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
}
]
}, },
{ {
id: guid(), id: guid(),
@ -398,41 +514,49 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'admin.system-settings', name: 'admin.settings',
type: 'toggle', type: 'link',
path: '/settings', path: '/settings',
height: '160px',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
id: guid(), id: guid(),
name: 'admin.home-settings', name: 'admin.home',
type: 'link', type: 'link',
path: '/settings/home', path: '/settings/home',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: guid(),
name: 'resource.resources-library', name: 'admin.repository',
type: 'link',
path: '/settings/resources-library',
icon: 'folder'
},
{
id: guid(),
name: 'admin.repository-settings',
type: 'link', type: 'link',
path: '/settings/repository', path: '/settings/repository',
icon: 'manage_history' icon: 'manage_history'
}, },
{ {
id: guid(), id: guid(),
name: 'admin.auto-commit-settings', name: 'admin.auto-commit',
type: 'link', type: 'link',
path: '/settings/auto-commit', path: '/settings/auto-commit',
icon: 'settings_backup_restore' icon: 'settings_backup_restore'
} }
] ]
},
{
id: guid(),
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security',
pages: [
{
id: guid(),
name: 'audit-log.audit-logs',
type: 'link',
path: '/security-settings/auditLogs',
icon: 'track_changes'
}
]
} }
); );
return sections; return sections;
@ -610,24 +734,47 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'asset.assets', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/assets', path: '/alarms',
icon: 'domain' icon: 'notifications'
}, },
{ {
id: guid(), id: guid(),
name: 'device.devices', name: 'dashboard.dashboards',
type: 'link', type: 'link',
path: '/devices', path: '/dashboards',
icon: 'devices_other' icon: 'dashboards'
}, },
{ {
id: guid(), id: guid(),
name: 'entity-view.entity-views', name: 'entity.entities',
type: 'link', type: 'toggle',
path: '/entityViews', path: '/entities',
icon: 'view_quilt' icon: 'category',
pages: [
{
id: guid(),
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
},
{
id: guid(),
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
},
{
id: guid(),
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
icon: 'view_quilt'
}
]
} }
); );
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
@ -636,20 +783,11 @@ export class MenuService {
id: guid(), id: guid(),
name: 'edge.edge-instances', name: 'edge.edge-instances',
type: 'link', type: 'link',
path: '/edgeInstances', path: '/edgeManagement/instances',
icon: 'router' icon: 'router'
} }
); );
} }
sections.push(
{
id: guid(),
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboard'
}
);
return sections; return sections;
} }

14
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -35,6 +35,7 @@ import {
AlarmSearchStatus, AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
AlarmsMode,
alarmStatusTranslations alarmStatusTranslations
} from '@app/shared/models/alarm.models'; } from '@app/shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
@ -70,18 +71,20 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog,
private alarmsMode: AlarmsMode = AlarmsMode.ALL,
public entityId: EntityId = null, public entityId: EntityId = null,
private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY,
private store: Store<AppState>, private store: Store<AppState>,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private overlay: Overlay, private overlay: Overlay,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private utilsService: UtilsService) { private utilsService: UtilsService,
pageMode = false) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
this.useTimePageLink = true; this.useTimePageLink = true;
this.pageMode = false; this.pageMode = pageMode;
this.defaultTimewindowInterval = historyInterval(DAY * 30); this.defaultTimewindowInterval = historyInterval(DAY * 30);
this.detailsPanelEnabled = false; this.detailsPanelEnabled = false;
this.selectionEnabled = false; this.selectionEnabled = false;
@ -156,7 +159,12 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> { fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> {
const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true, null); const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true, null);
return this.alarmService.getAlarms(query); switch (this.alarmsMode) {
case AlarmsMode.ALL:
return this.alarmService.getAllAlarms(query);
case AlarmsMode.ENTITY:
return this.alarmService.getAlarms(query);
}
} }
showAlarmDetails(entity: AlarmInfo) { showAlarmDetails(entity: AlarmInfo) {

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

@ -15,4 +15,4 @@
limitations under the License. limitations under the License.
--> -->
<tb-entities-table [entitiesTableConfig]="alarmTableConfig"></tb-entities-table> <tb-entities-table [entitiesTableConfig]="alarmTableConfig" [ngClass]="{'tb-details-mode': detailsMode}"></tb-entities-table>

2
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { :host ::ng-deep {
tb-entities-table { tb-entities-table.tb-details-mode {
.mat-drawer-container { .mat-drawer-container {
background-color: white; background-color: white;
.assignee-cell { .assignee-cell {

20
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts

@ -22,12 +22,14 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AlarmTableConfig } from './alarm-table-config'; import { AlarmTableConfig } from './alarm-table-config';
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus, AlarmsMode } from '@shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay'; import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils';
@Component({ @Component({
selector: 'tb-alarm-table', selector: 'tb-alarm-table',
@ -39,6 +41,8 @@ export class AlarmTableComponent implements OnInit {
activeValue = false; activeValue = false;
dirtyValue = false; dirtyValue = false;
entityIdValue: EntityId; entityIdValue: EntityId;
alarmsMode = AlarmsMode.ENTITY;
detailsMode = true;
@Input() @Input()
set active(active: boolean) { set active(active: boolean) {
@ -77,24 +81,34 @@ export class AlarmTableComponent implements OnInit {
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private utilsService: UtilsService) { private utilsService: UtilsService,
private route: ActivatedRoute) {
} }
ngOnInit() { ngOnInit() {
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
const pageMode = !!this.route.snapshot.data.isPage;
if (pageMode) {
this.detailsMode = false;
}
if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) {
this.alarmsMode = this.route.snapshot.data.alarmsMode;
}
this.alarmTableConfig = new AlarmTableConfig( this.alarmTableConfig = new AlarmTableConfig(
this.alarmService, this.alarmService,
this.dialogService, this.dialogService,
this.translate, this.translate,
this.datePipe, this.datePipe,
this.dialog, this.dialog,
this.alarmsMode,
this.entityIdValue, this.entityIdValue,
AlarmSearchStatus.ANY, AlarmSearchStatus.ANY,
this.store, this.store,
this.viewContainerRef, this.viewContainerRef,
this.overlay, this.overlay,
this.cd, this.cd,
this.utilsService this.utilsService,
pageMode
); );
} }

6
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -16,14 +16,14 @@
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span> <span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
<mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection"> <mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection && !attributeScopeSelectionReadonly">
<mat-label translate>attribute.attributes-scope</mat-label> <mat-label translate>attribute.attributes-scope</mat-label>
<mat-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly" <mat-select [disabled]="(isLoading$ | async)"
[ngModel]="attributeScope" [ngModel]="attributeScope"
(ngModelChange)="attributeScopeChanged($event)"> (ngModelChange)="attributeScopeChanged($event)">
<mat-option *ngFor="let scope of attributeScopes" [value]="scope"> <mat-option *ngFor="let scope of attributeScopes" [value]="scope">

7
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -24,6 +24,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fff; background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{ .mat-toolbar-tools{
min-height: auto; min-height: auto;

2
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -34,7 +34,7 @@
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxLayout="column" class="tb-entity-table-content tb-outlined-border">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">

8
ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss

@ -29,6 +29,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fff; background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{ .mat-toolbar-tools{
min-height: auto; min-height: auto;
@ -36,6 +43,7 @@
.title-container{ .title-container{
overflow: hidden; overflow: hidden;
height: 100%;
} }
.tb-entity-table-title { .tb-entity-table-title {

12
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -126,6 +126,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private widgetResize$: ResizeObserver; private widgetResize$: ResizeObserver;
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
public route: ActivatedRoute, public route: ActivatedRoute,
public translate: TranslateService, public translate: TranslateService,
@ -143,7 +145,11 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
if (this.entitiesTableConfig) { if (this.entitiesTableConfig) {
this.init(this.entitiesTableConfig); this.init(this.entitiesTableConfig);
} else { } else {
this.init(this.route.snapshot.data.entitiesTableConfig); this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
this.init(data.entitiesTableConfig);
}
));
} }
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
@ -159,6 +165,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
if (this.widgetResize$) { if (this.widgetResize$) {
this.widgetResize$.disconnect(); this.widgetResize$.disconnect();
} }
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {

8
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html

@ -17,7 +17,13 @@
--> -->
<mat-card appearance="outlined" class="settings-card"> <mat-card appearance="outlined" class="settings-card">
<mat-toolbar class="details-toolbar"> <mat-toolbar class="details-toolbar">
<div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center"> <div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<button mat-icon-button
matTooltip="{{ 'action.back' | translate }}"
matTooltipPosition="above"
(click)="goBack()">
<mat-icon>arrow_back</mat-icon>
</button>
<div class="tb-details-title-header" fxLayout="column" fxLayoutAlign="center start"> <div class="tb-details-title-header" fxLayout="column" fxLayoutAlign="center start">
<div class="tb-details-title tb-ellipsis">{{ headerTitle }}</div> <div class="tb-details-title tb-ellipsis">{{ headerTitle }}</div>
<div class="tb-details-subtitle tb-ellipsis">{{ headerSubtitle }}</div> <div class="tb-details-subtitle tb-ellipsis">{{ headerSubtitle }}</div>

6
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts

@ -144,6 +144,10 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
return this.detailsForm; return this.detailsForm;
} }
goBack(): void {
this.router.navigate(['../'], { relativeTo: this.route });
}
private onUpdateEntity() { private onUpdateEntity() {
this.broadcast.broadcast('updateBreadcrumb'); this.broadcast.broadcast('updateBreadcrumb');
this.isReadOnly = this.entitiesTableConfig.detailsReadonly(this.entity); this.isReadOnly = this.entitiesTableConfig.detailsReadonly(this.entity);
@ -164,7 +168,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
if (result) { if (result) {
this.entitiesTableConfig.deleteEntity(entity.id).subscribe( this.entitiesTableConfig.deleteEntity(entity.id).subscribe(
() => { () => {
this.router.navigate(['../'], {relativeTo: this.route}); this.goBack();
} }
); );
} }

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

@ -176,10 +176,12 @@ import { AssetProfileAutocompleteComponent } from '@home/components/profile/asse
import { MODULES_MAP } from '@shared/models/constants'; import { MODULES_MAP } from '@shared/models/constants';
import { modulesMap } from '@modules/common/modules-map'; import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
@NgModule({ @NgModule({
declarations: declarations:
[ [
RouterTabsComponent,
EntitiesTableComponent, EntitiesTableComponent,
AddEntityDialogComponent, AddEntityDialogComponent,
DetailsPanelComponent, DetailsPanelComponent,
@ -334,6 +336,7 @@ import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assign
DeviceProfileCommonModule DeviceProfileCommonModule
], ],
exports: [ exports: [
RouterTabsComponent,
EntitiesTableComponent, EntitiesTableComponent,
AddEntityDialogComponent, AddEntityDialogComponent,
DetailsPanelComponent, DetailsPanelComponent,

6
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -25,7 +25,7 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmRule } from '@shared/models/device.models'; import { DeviceProfileAlarmRule } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
@ -71,7 +71,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
@Input() @Input()
deviceProfileId: EntityId; deviceProfileId: EntityId;
private modelValue: AlarmRule; private modelValue: DeviceProfileAlarmRule;
alarmRuleFormGroup: UntypedFormGroup; alarmRuleFormGroup: UntypedFormGroup;
@ -112,7 +112,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
} }
} }
writeValue(value: AlarmRule): void { writeValue(value: DeviceProfileAlarmRule): void {
this.modelValue = value; this.modelValue = value;
const model = this.modelValue ? { const model = this.modelValue ? {
...this.modelValue, ...this.modelValue,

12
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts

@ -27,7 +27,7 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmRule, alarmRuleValidator } from '@shared/models/device.models'; import { DeviceProfileAlarmRule, alarmRuleValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
@ -101,7 +101,7 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
} }
} }
writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void { writeValue(createAlarmRules: {[severity: string]: DeviceProfileAlarmRule}): void {
if (this.valueChangeSubscription) { if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe(); this.valueChangeSubscription.unsubscribe();
} }
@ -138,7 +138,7 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
} }
public addCreateAlarmRule() { public addCreateAlarmRule() {
const createAlarmRule: AlarmRule = { const createAlarmRule: DeviceProfileAlarmRule = {
condition: { condition: {
condition: [] condition: []
} }
@ -179,15 +179,15 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
private updateUsedSeverities() { private updateUsedSeverities() {
this.usedSeverities = []; this.usedSeverities = [];
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; const value: {severity: string, alarmRule: DeviceProfileAlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
value.forEach((rule, index) => { value.forEach((rule, index) => {
this.usedSeverities[index] = AlarmSeverity[rule.severity]; this.usedSeverities[index] = AlarmSeverity[rule.severity];
}); });
} }
private updateModel() { private updateModel() {
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; const value: {severity: string, alarmRule: DeviceProfileAlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {}; const createAlarmRules: {[severity: string]: DeviceProfileAlarmRule} = {};
value.forEach(v => { value.forEach(v => {
createAlarmRules[v.severity] = v.alarmRule; createAlarmRules[v.severity] = v.alarmRule;
}); });

4
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts

@ -25,7 +25,7 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmRule, DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models'; import { DeviceProfileAlarmRule, DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
@ -128,7 +128,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
} }
public addClearAlarmRule() { public addClearAlarmRule() {
const clearAlarmRule: AlarmRule = { const clearAlarmRule: DeviceProfileAlarmRule = {
condition: { condition: {
condition: [] condition: []
} }

6
ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts

@ -161,9 +161,9 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr
this.queueFormGroup.get('additionalInfo').get('description') this.queueFormGroup.get('additionalInfo').get('description')
.patchValue(this.modelValue.additionalInfo?.description, {emitEvent: false}); .patchValue(this.modelValue.additionalInfo?.description, {emitEvent: false});
this.submitStrategyTypeChanged(); this.submitStrategyTypeChanged();
} if (!this.disabled && !this.queueFormGroup.valid) {
if (!this.disabled && !this.queueFormGroup.valid) { this.updateModel();
this.updateModel(); }
} }
} }

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

@ -16,7 +16,7 @@
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">

7
ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss

@ -24,6 +24,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fff; background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{ .mat-toolbar-tools{
min-height: auto; min-height: auto;

34
ui-ngx/src/app/modules/home/components/router-tabs.component.html

@ -0,0 +1,34 @@
<!--
Copyright © 2016-2023 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.
-->
<div fxFlex fxLayout="column" style="width: 100%;">
<nav *ngIf="!routerOutlet?.activatedRouteData?.hideTabs && !hideCurrentTabs && (tabs$ | async).length > 1" mat-tab-nav-bar mat-stretch-tabs="false" class="tb-router-tabs" [tabPanel]="tabPanel">
<a *ngFor="let tab of tabs$ | async"
routerLink="{{tab.path}}"
routerLinkActive
#rla="routerLinkActive"
mat-tab-link
[active]="rla.isActive">
<mat-icon *ngIf="!tab.isMdiIcon && tab.icon !== null" class="tb-mat-18">{{tab.icon}}</mat-icon>
<mat-icon *ngIf="tab.isMdiIcon && tab.icon !== null" [svgIcon]="tab.icon" class="tb-mat-18"></mat-icon>
<span>{{tab.name | translate}}</span>
</a>
</nav>
<mat-tab-nav-panel #tabPanel fxFlex fxLayout="column" class="tb-router-tabs-content">
<router-outlet #routerOutlet="outlet" (activate)="activeComponentChanged($event)"></router-outlet>
</mat-tab-nav-panel>
</div>

46
ui-ngx/src/app/modules/home/components/router-tabs.component.scss

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2023 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.
*/
:host {
display: flex;
width: 100%;
height: 100%;
.mat-mdc-tab-nav-bar.tb-router-tabs {
a.mat-mdc-tab-link {
display: inline-flex;
align-items: center;
overflow: hidden;
line-height: 40px;
min-width: 200px;
border-bottom: none;
mat-icon {
margin-right: 8px;
margin-left: 0;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.tb-router-tabs-content {
overflow: auto;
position: relative;
height: 100%;
}
}

106
ui-ngx/src/app/modules/home/components/router-tabs.component.ts

@ -0,0 +1,106 @@
///
/// Copyright © 2016-2023 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.
///
import { Component, Inject } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WINDOW } from '@core/services/window.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { MenuService } from '@core/services/menu.service';
import { distinctUntilChanged, filter, map, mergeMap, take } from 'rxjs/operators';
import { merge } from 'rxjs';
import { MenuSection } from '@core/services/menu.models';
import { ActiveComponentService } from '@core/services/active-component.service';
@Component({
selector: 'tb-router-tabs',
templateUrl: './router-tabs.component.html',
styleUrls: ['./router-tabs.component.scss']
})
export class RouterTabsComponent extends PageComponent {
hideCurrentTabs = false;
tabs$ = merge(this.menuService.menuSections(),
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd ),
distinctUntilChanged()
)
).pipe(
mergeMap(() => this.menuService.menuSections().pipe(take(1))),
map((sections) => this.buildTabs(this.activatedRoute, sections))
);
constructor(protected store: Store<AppState>,
private activatedRoute: ActivatedRoute,
public router: Router,
private menuService: MenuService,
private activeComponentService: ActiveComponentService,
@Inject(WINDOW) private window: Window,
public breakpointObserver: BreakpointObserver) {
super(store);
}
activeComponentChanged(activeComponent: any) {
this.activeComponentService.setCurrentActiveComponent(activeComponent);
let snapshot = this.router.routerState.snapshot.root;
this.hideCurrentTabs = false;
let found = false;
while (snapshot.children.length) {
if (snapshot.component && snapshot.component === RouterTabsComponent) {
if (this.activatedRoute.snapshot === snapshot) {
found = true;
} else if (found) {
this.hideCurrentTabs = true;
break;
}
}
snapshot = snapshot.children[0];
}
}
private buildTabs(activatedRoute: ActivatedRoute, sections: MenuSection[]): Array<MenuSection> {
const sectionPath = '/' + activatedRoute.pathFromRoot.map(r => r.snapshot.url)
.filter(f => !!f[0]).map(f => f.map(f1 => f1.path).join('/')).join('/');
const found = this.findRootSection(sections, sectionPath);
if (found) {
const rootPath = sectionPath.substring(0, sectionPath.length - found.path.length);
const isRoot = rootPath === '';
const tabs: Array<MenuSection> = found ? found.pages.filter(page => !page.disabled && (!page.rootOnly || isRoot)) : [];
return tabs.map((tab) => ({...tab, path: rootPath + tab.path}));
} else {
return [];
}
}
private findRootSection(sections: MenuSection[], sectionPath: string): MenuSection {
for (const section of sections) {
if (sectionPath.endsWith(section.path)) {
return section;
}
if (section.pages?.length) {
const found = this.findRootSection(section.pages, sectionPath);
if (found) {
return found;
}
}
}
return null;
}
}

2
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html

@ -16,7 +16,7 @@
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill" [ngClass]="{'tb-popover-mode': popoverComponent}"> <div class="mat-padding tb-entity-table tb-absolute-fill" [ngClass]="{'tb-popover-mode': popoverComponent}">
<div fxFlex fxLayout="column" class="tb-entity-table-content" [ngClass]="{'mat-elevation-z1': !popoverComponent}"> <div fxFlex fxLayout="column" class="tb-entity-table-content" [ngClass]="{'tb-outlined-border': !popoverComponent}">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">

7
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.scss

@ -31,6 +31,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fff; background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{ .mat-toolbar-tools{
min-height: auto; min-height: auto;

2
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -16,7 +16,7 @@
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span class="tb-entity-table-title" translate>widget-config.actions</span> <span class="tb-entity-table-title" translate>widget-config.actions</span>

7
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss

@ -22,6 +22,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fff; background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.tb-entity-table-title { .tb-entity-table-title {
padding-right: 20px; padding-right: 20px;

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

@ -47,6 +47,7 @@ import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TbInject } from '@shared/decorators/tb-inject'; import { TbInject } from '@shared/decorators/tb-inject';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service';
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix
@ -82,6 +83,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
this.ctx.dialogs = $injector.get(DialogService); this.ctx.dialogs = $injector.get(DialogService);
this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.customDialog = $injector.get(CustomDialogService);
this.ctx.resourceService = $injector.get(ResourceService); this.ctx.resourceService = $injector.get(ResourceService);
this.ctx.userSettingsService = $injector.get(UserSettingsService);
this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService);
this.ctx.date = $injector.get(DatePipe); this.ctx.date = $injector.get(DatePipe);
this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe); this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe);

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

@ -35,7 +35,7 @@
</mat-sidenav> </mat-sidenav>
<mat-sidenav-content> <mat-sidenav-content>
<div fxLayout="column" role="main" style="height: 100%;"> <div fxLayout="column" role="main" style="height: 100%;">
<mat-toolbar fxLayout="row" color="primary" class="mat-elevation-z1 tb-primary-toolbar"> <mat-toolbar fxLayout="row" color="primary" class="tb-primary-toolbar">
<button [fxShow]="!forceFullscreen" mat-icon-button id="main" <button [fxShow]="!forceFullscreen" mat-icon-button id="main"
[ngClass]="{'tb-invisible': displaySearchMode()}" [ngClass]="{'tb-invisible': displaySearchMode()}"
fxHide.gt-sm (click)="sidenav.toggle()"> fxHide.gt-sm (click)="sidenav.toggle()">
@ -52,7 +52,7 @@
<mat-icon class="material-icons">arrow_back</mat-icon> <mat-icon class="material-icons">arrow_back</mat-icon>
</button> </button>
<tb-breadcrumb [fxShow]="!displaySearchMode()" <tb-breadcrumb [fxShow]="!displaySearchMode()"
fxFlex [activeComponent]="activeComponent" class="mat-toolbar-tools"> fxFlex class="mat-toolbar-tools">
</tb-breadcrumb> </tb-breadcrumb>
<div [fxShow]="displaySearchMode()" fxFlex fxLayout="row" class="tb-dark"> <div [fxShow]="displaySearchMode()" fxFlex fxLayout="row" class="tb-dark">
<mat-form-field fxFlex class="tb-appearance-transparent"> <mat-form-field fxFlex class="tb-appearance-transparent">
@ -72,7 +72,7 @@
<tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu> <tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu>
</mat-toolbar> </mat-toolbar>
<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="isLoading$ | async"> *ngIf="!hideLoadingBar && (isLoading$ | async)">
</mat-progress-bar> </mat-progress-bar>
<div fxFlex fxLayout="column" tb-toast class="tb-main-content"> <div fxFlex fxLayout="column" tb-toast class="tb-main-content">
<router-outlet (activate)="activeComponentChanged($event)"></router-outlet> <router-outlet (activate)="activeComponentChanged($event)"></router-outlet>

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

@ -15,21 +15,22 @@
/// ///
import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { fromEvent, Observable } from 'rxjs'; import { fromEvent } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { User } from '@shared/models/user.model';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthState, selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { MatSidenav } from '@angular/material/sidenav'; import { MatSidenav } from '@angular/material/sidenav';
import { AuthState } from '@core/auth/auth.models'; import { AuthState } from '@core/auth/auth.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models'; import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models';
import { ActiveComponentService } from '@core/services/active-component.service';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
@Component({ @Component({
selector: 'tb-home', selector: 'tb-home',
@ -57,28 +58,21 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
fullscreenEnabled = screenfull.isEnabled; fullscreenEnabled = screenfull.isEnabled;
authUser$: Observable<any>;
userDetails$: Observable<User>;
userDetailsString: Observable<string>;
searchEnabled = false; searchEnabled = false;
showSearch = false; showSearch = false;
searchText = ''; searchText = '';
hideLoadingBar = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
private activeComponentService: ActiveComponentService,
public breakpointObserver: BreakpointObserver) { public breakpointObserver: BreakpointObserver) {
super(store); super(store);
} }
ngOnInit() { ngOnInit() {
this.authUser$ = this.store.pipe(select(selectAuthUser));
this.userDetails$ = this.store.pipe(select(selectUserDetails));
this.userDetailsString = this.userDetails$.pipe(map((user: User) => {
return JSON.stringify(user);
}));
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
this.sidenavMode = isGtSm ? 'side' : 'over'; this.sidenavMode = isGtSm ? 'side' : 'over';
this.sidenavOpened = isGtSm; this.sidenavOpened = isGtSm;
@ -130,6 +124,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
} }
activeComponentChanged(activeComponent: any) { activeComponentChanged(activeComponent: any) {
this.activeComponentService.setCurrentActiveComponent(activeComponent);
if (!this.activeComponent) { if (!this.activeComponent) {
setTimeout(() => { setTimeout(() => {
this.updateActiveComponent(activeComponent); this.updateActiveComponent(activeComponent);
@ -143,6 +138,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
this.showSearch = false; this.showSearch = false;
this.searchText = ''; this.searchText = '';
this.activeComponent = activeComponent; this.activeComponent = activeComponent;
this.hideLoadingBar = activeComponent && activeComponent instanceof RouterTabsComponent;
if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) { if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) {
this.searchEnabled = true; this.searchEnabled = true;
this.searchableComponent = this.activeComponent; this.searchableComponent = this.activeComponent;

6
ui-ngx/src/app/modules/home/menu/menu-link.component.html

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}"> <a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'subset', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}">
<mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon> <mat-icon *ngIf="!section.isMdiIcon && section.icon !== null">{{section.icon}}</mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon> <mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon"></mat-icon>
<span>{{section.name | translate}}</span> <span>{{section.name | translate}}</span>
</a> </a>

9
ui-ngx/src/app/modules/home/menu/menu-toggle.component.html

@ -15,13 +15,12 @@
limitations under the License. limitations under the License.
--> -->
<a mat-button class="tb-button-toggle" <a mat-button class="tb-button-toggle" (click)="toggleSection($event)">
routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}"> <mat-icon *ngIf="!section.isMdiIcon && section.icon !== null">{{section.icon}}</mat-icon>
<mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon> <mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon"></mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon>
<span>{{section.name | translate}}</span> <span>{{section.name | translate}}</span>
<span class=" pull-right fa fa-chevron-down tb-toggle-icon" <span class=" pull-right fa fa-chevron-down tb-toggle-icon"
[ngClass]="{'tb-toggled' : sectionActive()}"></span> [ngClass]="{'tb-toggled' : section.opened}"></span>
</a> </a>
<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" [ngStyle]="{height: sectionHeight()}"> <ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" [ngStyle]="{height: sectionHeight()}">
<li *ngFor="let page of section.pages; trackBy: trackBySectionPages"> <li *ngFor="let page of section.pages; trackBy: trackBySectionPages">

2
ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss

@ -33,7 +33,7 @@
z-index: 1; z-index: 1;
overflow: hidden; overflow: hidden;
transition: .75s cubic-bezier(.35, 0, .25, 1); transition: .45s cubic-bezier(.35, 0, .25, 1);
transition-property: height; transition-property: height;

20
ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts

@ -17,6 +17,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { MenuSection } from '@core/services/menu.models'; import { MenuSection } from '@core/services/menu.models';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ActionPreferencesUpdateOpenedMenuSection } from '@core/auth/auth.actions';
@Component({ @Component({
selector: 'tb-menu-toggle', selector: 'tb-menu-toggle',
@ -28,24 +31,27 @@ export class MenuToggleComponent implements OnInit {
@Input() section: MenuSection; @Input() section: MenuSection;
constructor(private router: Router) { constructor(private router: Router,
private store: Store<AppState>) {
} }
ngOnInit() { ngOnInit() {
} }
sectionActive(): boolean {
return this.router.isActive(this.section.path, false);
}
sectionHeight(): string { sectionHeight(): string {
if (this.router.isActive(this.section.path, false)) { if (this.section.opened) {
return this.section.height; return this.section.pages.length * 40 + 'px';
} else { } else {
return '0px'; return '0px';
} }
} }
toggleSection(event: MouseEvent) {
event.stopPropagation();
this.section.opened = !this.section.opened;
this.store.dispatch(new ActionPreferencesUpdateOpenedMenuSection({path: this.section.path, opened: this.section.opened}));
}
trackBySectionPages(index: number, section: MenuSection){ trackBySectionPages(index: number, section: MenuSection){
return section.id; return section.id;
} }

1
ui-ngx/src/app/modules/home/menu/side-menu.component.scss

@ -63,6 +63,7 @@
white-space: nowrap; white-space: nowrap;
display: inline-flex; display: inline-flex;
& > span { & > span {
letter-spacing: .03125em;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }

4
ui-ngx/src/app/modules/home/models/services.map.ts

@ -41,6 +41,7 @@ import { ResourceService } from '@core/http/resource.service';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service';
export const ServicesMap = new Map<string, Type<any>>( export const ServicesMap = new Map<string, Type<any>>(
[ [
@ -69,6 +70,7 @@ export const ServicesMap = new Map<string, Type<any>>(
['authService', AuthService], ['authService', AuthService],
['resourceService', ResourceService], ['resourceService', ResourceService],
['twoFactorAuthenticationService', TwoFactorAuthenticationService], ['twoFactorAuthenticationService', TwoFactorAuthenticationService],
['telemetryWsService', TelemetryWebsocketService] ['telemetryWsService', TelemetryWebsocketService],
['userSettingsService', UserSettingsService]
] ]
); );

2
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -90,6 +90,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models';
import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -180,6 +181,7 @@ export class WidgetContext {
dialogs: DialogService; dialogs: DialogService;
customDialog: CustomDialogService; customDialog: CustomDialogService;
resourceService: ResourceService; resourceService: ResourceService;
userSettingsService: UserSettingsService;
telemetryWsService: TelemetryWebsocketService; telemetryWsService: TelemetryWebsocketService;
telemetrySubscribers?: TelemetrySubscriber[]; telemetrySubscribers?: TelemetrySubscriber[];
date: DatePipe; date: DatePipe;

262
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -36,6 +36,9 @@ import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component';
import { widgetsBundlesRoutes } from '@home/pages/widget/widget-library-routing.module';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
@Injectable() @Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -49,12 +52,72 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
} }
const routes: Routes = [ const routes: Routes = [
{
path: 'resources',
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'admin.resources',
icon: 'folder'
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: '/resources/widgets-bundles'
}
},
...widgetsBundlesRoutes,
{
path: 'resources-library',
data: {
breadcrumb: {
label: 'resource.resources-library',
icon: 'mdi:rhombus-split'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library',
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'mdi:rhombus-split'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library'
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
}
]
}
]
},
{ {
path: 'settings', path: 'settings',
component: RouterTabsComponent,
data: { data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: { breadcrumb: {
label: 'admin.system-settings', label: 'admin.settings',
icon: 'settings' icon: 'settings'
} }
}, },
@ -97,98 +160,18 @@ const routes: Routes = [
} }
}, },
{ {
path: 'sms-provider', path: 'notifications',
component: SmsProviderComponent, component: SmsProviderComponent,
canDeactivate: [ConfirmOnExitGuard], canDeactivate: [ConfirmOnExitGuard],
data: { data: {
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.sms-provider-settings', title: 'admin.notifications-settings',
breadcrumb: {
label: 'admin.sms-provider',
icon: 'sms'
}
}
},
{
path: 'security-settings',
component: SecuritySettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.security-settings',
breadcrumb: { breadcrumb: {
label: 'admin.security-settings', label: 'admin.notifications',
icon: 'security' icon: 'mdi:message-badge'
} }
} }
}, },
{
path: 'oauth2',
component: OAuth2SettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.oauth2.oauth2',
breadcrumb: {
label: 'admin.oauth2.oauth2',
icon: 'security'
}
},
resolve: {
loginProcessingUrl: OAuth2LoginProcessingUrlResolver
}
},
{
path: 'home',
component: HomeSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.TENANT_ADMIN],
title: 'admin.home-settings',
breadcrumb: {
label: 'admin.home-settings',
icon: 'settings_applications'
}
}
},
{
path: 'resources-library',
data: {
breadcrumb: {
label: 'resource.resources-library',
icon: 'folder'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library',
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'folder'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library'
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
}
]
},
{ {
path: 'queues', path: 'queues',
data: { data: {
@ -228,16 +211,15 @@ const routes: Routes = [
] ]
}, },
{ {
path: '2fa', path: 'home',
component: TwoFactorAuthSettingsComponent, component: HomeSettingsComponent,
canDeactivate: [ConfirmOnExitGuard], canDeactivate: [ConfirmOnExitGuard],
data: { data: {
auth: [Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.2fa.2fa', title: 'admin.home-settings',
breadcrumb: { breadcrumb: {
label: 'admin.2fa.2fa', label: 'admin.home',
icon: 'mdi:two-factor-authentication', icon: 'settings_applications'
isMdiIcon: true
} }
} }
}, },
@ -249,7 +231,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.repository-settings', title: 'admin.repository-settings',
breadcrumb: { breadcrumb: {
label: 'admin.repository-settings', label: 'admin.repository',
icon: 'manage_history' icon: 'manage_history'
} }
} }
@ -262,11 +244,103 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.auto-commit-settings', title: 'admin.auto-commit-settings',
breadcrumb: { breadcrumb: {
label: 'admin.auto-commit-settings', label: 'admin.auto-commit',
icon: 'settings_backup_restore' icon: 'settings_backup_restore'
} }
} }
},
{
path: 'security-settings',
redirectTo: '/security-settings/general'
},
{
path: 'oauth2',
redirectTo: '/security-settings/oauth2'
},
{
path: 'resources-library',
pathMatch: 'full',
redirectTo: '/resources/resources-library'
},
{
path: 'resources-library/:entityId',
redirectTo: '/resources/resources-library/:entityId'
},
{
path: '2fa',
redirectTo: '/security-settings/2fa'
},
{
path: 'sms-provider',
redirectTo: '/settings/notifications'
}
]
},
{
path: 'security-settings',
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'security.security',
icon: 'security'
} }
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: {
SYS_ADMIN: '/security-settings/general',
TENANT_ADMIN: '/security-settings/auditLogs'
}
}
},
{
path: 'general',
component: SecuritySettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.general',
breadcrumb: {
label: 'admin.general',
icon: 'settings_applications'
}
}
},
{
path: '2fa',
component: TwoFactorAuthSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.2fa.2fa',
breadcrumb: {
label: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
}
}
},
{
path: 'oauth2',
component: OAuth2SettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.oauth2.oauth2',
breadcrumb: {
label: 'admin.oauth2.oauth2',
icon: 'mdi:shield-account'
}
},
resolve: {
loginProcessingUrl: OAuth2LoginProcessingUrlResolver
}
},
...auditLogsRoutes
] ]
} }
]; ];

5
ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss

@ -14,5 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
}
:host ::ng-deep {
.mat-checkbox-layout {
white-space: normal;
}
} }

2
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -123,7 +123,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const url = this.router.createUrlTree(['settings', 'resources-library', resourceInfo.id.id]); const url = this.router.createUrlTree(['resources', 'resources-library', resourceInfo.id.id]);
this.router.navigateByUrl(url); this.router.navigateByUrl(url);
} }

2
ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html

@ -19,7 +19,7 @@
<mat-card appearance="outlined" class="settings-card"> <mat-card appearance="outlined" class="settings-card">
<mat-card-header> <mat-card-header>
<mat-card-title> <mat-card-title>
<span class="mat-headline-5" translate>admin.sms-provider-settings</span> <span class="mat-headline-5" translate>admin.notifications-settings</span>
</mat-card-title> </mat-card-title>
<span fxFlex></span> <span fxFlex></span>
<div tb-help="smsProviderSettings"></div> <div tb-help="smsProviderSettings"></div>

58
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -0,0 +1,58 @@
///
/// Copyright © 2016-2023 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.
///
import { Injectable, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { Observable } from 'rxjs';
import { OAuth2Service } from '@core/http/oauth2.service';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
constructor(private oauth2Service: OAuth2Service) {
}
resolve(): Observable<string> {
return this.oauth2Service.getLoginProcessingUrl();
}
}
const routes: Routes = [
{
path: 'alarms',
component: AlarmTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'alarm.alarms',
breadcrumb: {
label: 'alarm.alarms',
icon: 'notifications'
},
isPage: true,
alarmsMode: AlarmsMode.ALL
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class AlarmRoutingModule { }

34
ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts

@ -0,0 +1,34 @@
///
/// Copyright © 2016-2023 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.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
HomeDialogsModule,
AlarmRoutingModule
]
})
export class AlarmModule { }

14
ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts

@ -25,7 +25,7 @@ import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
const routes: Routes = [ export const assetRoutes: Routes = [
{ {
path: 'assets', path: 'assets',
data: { data: {
@ -68,6 +68,18 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'assets',
pathMatch: 'full',
redirectTo: '/entities/assets'
},
{
path: 'assets/:entityId',
redirectTo: '/entities/assets/:entityId'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],

12
ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts

@ -19,7 +19,7 @@ import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component';
const routes: Routes = [ export const auditLogsRoutes: Routes = [
{ {
path: 'auditLogs', path: 'auditLogs',
component: AuditLogTableComponent, component: AuditLogTableComponent,
@ -35,8 +35,16 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'auditLogs',
redirectTo: '/security-settings/auditLogs'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
providers: []
}) })
export class AuditLogRoutingModule { } export class AuditLogRoutingModule { }

16
ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts

@ -24,8 +24,10 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { assetRoutes } from '@home/pages/asset/asset-routing.module';
import { entityViewRoutes } from '@home/pages/entity-view/entity-view-routing.module';
const routes: Routes = [ export const deviceRoutes: Routes = [
{ {
path: 'devices', path: 'devices',
data: { data: {
@ -68,6 +70,18 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'devices',
pathMatch: 'full',
redirectTo: '/entities/devices'
},
{
path: 'devices/:entityId',
redirectTo: '/entities/devices/:entityId'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],

438
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -44,49 +44,28 @@ import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages
const routes: Routes = [ const routes: Routes = [
{ {
path: 'edgeInstances', path: 'edgeManagement',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'edge.edge-instances', label: 'edge.management',
icon: 'router' icon: 'settings_input_antenna'
} }
}, },
children: [ children: [
{ {
path: '', path: '',
component: EntitiesTableComponent, children: [],
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.edge-instances', redirectTo: '/edgeManagement/instances'
edgesType: 'tenant'
},
resolve: {
entitiesTableConfig: EdgesTableConfigResolver
} }
}, },
{ {
path: ':entityId', path: 'instances',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction, label: 'edge.instances',
icon: 'router' icon: 'router'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.edge-instances',
edgesType: 'tenant'
},
resolve: {
entitiesTableConfig: EdgesTableConfigResolver
}
},
{
path: ':edgeId/assets',
data: {
breadcrumb: {
label: 'edge.assets',
icon: 'domain'
} }
}, },
children: [ children: [
@ -95,11 +74,11 @@ const routes: Routes = [
component: EntitiesTableComponent, component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets', title: 'edge.edge-instances',
assetsType: 'edge' edgesType: 'tenant'
}, },
resolve: { resolve: {
entitiesTableConfig: AssetsTableConfigResolver entitiesTableConfig: EdgesTableConfigResolver
} }
}, },
{ {
@ -109,198 +88,219 @@ const routes: Routes = [
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction, labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'domain' icon: 'router'
} as BreadCrumbConfig<EntityDetailsPageComponent>, } as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets', title: 'edge.edge-instances',
assetsType: 'edge' edgesType: 'tenant'
}, },
resolve: { resolve: {
entitiesTableConfig: AssetsTableConfigResolver entitiesTableConfig: EdgesTableConfigResolver
}
}
]
},
{
path: ':edgeId/devices',
data: {
breadcrumb: {
label: 'edge.devices',
icon: 'devices_other'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.devices',
devicesType: 'edge'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
} }
}, },
{ {
path: ':entityId', path: ':edgeId/assets',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction, label: 'edge.assets',
icon: 'devices_other' icon: 'domain'
} as BreadCrumbConfig<EntityDetailsPageComponent>, }
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.devices',
devicesType: 'edge'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
]
},
{
path: ':edgeId/entityViews',
data: {
breadcrumb: {
label: 'edge.entity-views',
icon: 'view_quilt'
},
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
}, },
resolve: { children: [
entitiesTableConfig: EntityViewsTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets',
assetsType: 'edge'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'domain'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets',
assetsType: 'edge'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
}
]
}, },
{ {
path: ':entityId', path: ':edgeId/devices',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction, label: 'edge.devices',
icon: 'devices_other' icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>, }
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
}, },
resolve: { children: [
entitiesTableConfig: EntityViewsTableConfigResolver {
} path: '',
} component: EntitiesTableComponent,
] data: {
}, auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
{ title: 'edge.devices',
path: ':edgeId/dashboards', devicesType: 'edge'
data: { },
breadcrumb: { resolve: {
label: 'edge.dashboards', entitiesTableConfig: DevicesTableConfigResolver
icon: 'dashboard' }
} },
}, {
children: [ path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.devices',
devicesType: 'edge'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
]
},
{ {
path: '', path: ':edgeId/entityViews',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], breadcrumb: {
dashboardsType: 'edge' label: 'edge.entity-views',
}, icon: 'view_quilt'
resolve: { },
entitiesTableConfig: DashboardsTableConfigResolver
}, },
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
},
resolve: {
entitiesTableConfig: EntityViewsTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
},
resolve: {
entitiesTableConfig: EntityViewsTableConfigResolver
}
}
]
}, },
{ {
path: ':dashboardId', path: ':edgeId/dashboards',
component: DashboardPageComponent,
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction, label: 'edge.dashboards',
icon: 'dashboard' icon: 'dashboard'
} as BreadCrumbConfig<DashboardPageComponent>, }
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.dashboard',
widgetEditMode: false
}, },
resolve: { children: [
dashboard: DashboardResolver {
} path: '',
} component: EntitiesTableComponent,
] data: {
}, auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
{ dashboardsType: 'edge'
path: ':edgeId/ruleChains', },
data: { resolve: {
breadcrumb: { entitiesTableConfig: DashboardsTableConfigResolver
label: 'edge.edge-rulechains', },
icon: 'settings_ethernet' },
} {
}, path: ':dashboardId',
children: [ component: DashboardPageComponent,
{ data: {
path: '', breadcrumb: {
component: EntitiesTableComponent, labelFunction: dashboardBreadcumbLabelFunction,
data: { icon: 'dashboard'
auth: [Authority.TENANT_ADMIN], } as BreadCrumbConfig<DashboardPageComponent>,
title: 'edge.rulechains', auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
ruleChainsType: 'edge' title: 'edge.dashboard',
}, widgetEditMode: false
resolve: { },
entitiesTableConfig: RuleChainsTableConfigResolver resolve: {
} dashboard: DashboardResolver
}
}
]
}, },
{ {
path: ':ruleChainId', path: ':edgeId/ruleChains',
component: RuleChainPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: { breadcrumb: {
labelFunction: ruleChainBreadcumbLabelFunction, label: 'edge.edge-rulechains',
icon: 'settings_ethernet' icon: 'settings_ethernet'
} as BreadCrumbConfig<RuleChainPageComponent>, }
auth: [Authority.TENANT_ADMIN],
title: 'rulechain.edge-rulechain',
import: false,
ruleChainType: RuleChainType.EDGE
}, },
resolve: { children: [
ruleChain: RuleChainResolver, {
ruleChainMetaData: RuleChainMetaDataResolver, path: '',
ruleNodeComponents: RuleNodeComponentsResolver, component: EntitiesTableComponent,
tooltipster: TooltipsterResolver data: {
} auth: [Authority.TENANT_ADMIN],
title: 'edge.rulechains',
ruleChainsType: 'edge'
},
resolve: {
entitiesTableConfig: RuleChainsTableConfigResolver
}
},
{
path: ':ruleChainId',
component: RuleChainPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: ruleChainBreadcumbLabelFunction,
icon: 'settings_ethernet'
} as BreadCrumbConfig<RuleChainPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'rulechain.edge-rulechain',
import: false,
ruleChainType: RuleChainType.EDGE
},
resolve: {
ruleChain: RuleChainResolver,
ruleChainMetaData: RuleChainMetaDataResolver,
ruleNodeComponents: RuleNodeComponentsResolver,
tooltipster: TooltipsterResolver
}
}
]
} }
] ]
}
]
},
{
path: 'edgeManagement',
data: {
breadcrumb: {
label: 'edge.management',
icon: 'settings_input_antenna'
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
redirectTo: '/edgeManagement/ruleChains'
}
}, },
{ {
path: 'ruleChains', path: 'ruleChains',
@ -367,7 +367,63 @@ const routes: Routes = [
] ]
} }
] ]
}]; },
{
path: 'edgeInstances',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances'
},
{
path: 'edgeInstances/:entityId',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances/:entityId'
},
{
path: 'edgeInstances/:edgeId/assets',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances/:edgeId/assets'
},
{
path: 'edgeInstances/:edgeId/assets/:entityId',
redirectTo: '/edgeManagement/instances/:edgeId/assets/:entityId'
},
{
path: 'edgeInstances/:edgeId/devices',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances/:edgeId/devices'
},
{
path: 'edgeInstances/:edgeId/devices/:entityId',
redirectTo: '/edgeManagement/instances/:edgeId/devices/:entityId'
},
{
path: 'edgeInstances/:edgeId/entityViews',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances/:edgeId/entityViews'
},
{
path: 'edgeInstances/:edgeId/entityViews/:entityId',
redirectTo: '/edgeManagement/instances/:edgeId/entityViews/:entityId'
},
{
path: 'edgeInstances/:edgeId/dashboards',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances/:edgeId/dashboards'
},
{
path: 'edgeInstances/:edgeId/dashboards/:dashboardId',
redirectTo: '/edgeManagement/instances/:edgeId/dashboards/:dashboardId'
},
{
path: 'edgeInstances/:edgeId/ruleChains',
pathMatch: 'full',
redirectTo: '/edgeManagement/instances/:edgeId/ruleChains'
},
{
path: 'edgeInstances/:edgeId/ruleChains/:ruleChainId',
redirectTo: '/edgeManagement/instances/:edgeId/ruleChains/:ruleChainId'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],

2
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -429,7 +429,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
suffix = 'ruleChains'; suffix = 'ruleChains';
break; break;
} }
this.router.navigateByUrl(`edgeInstances/${edge.id.id}/${suffix}`); this.router.navigateByUrl(`edgeManagement/instances/${edge.id.id}/${suffix}`);
} }
assignToCustomer($event: Event, edgesIds: Array<EdgeId>) { assignToCustomer($event: Event, edgesIds: Array<EdgeId>) {

53
ui-ngx/src/app/modules/home/pages/entities/entities-routing.module.ts

@ -0,0 +1,53 @@
///
/// Copyright © 2016-2023 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.
///
import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core';
import { deviceRoutes } from '@home/pages/device/device-routing.module';
import { assetRoutes } from '@home/pages/asset/asset-routing.module';
import { entityViewRoutes } from '@home/pages/entity-view/entity-view-routing.module';
const routes: Routes = [
{
path: 'entities',
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
breadcrumb: {
skip: true
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
redirectTo: '/entities/devices'
}
},
...deviceRoutes,
...assetRoutes,
...entityViewRoutes
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class EntitiesRoutingModule { }

30
ui-ngx/src/app/modules/home/pages/entities/entities.module.ts

@ -0,0 +1,30 @@
///
/// Copyright © 2016-2023 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.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { EntitiesRoutingModule } from '@home/pages/entities/entities-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SharedModule,
EntitiesRoutingModule
]
})
export class EntitiesModule { }

14
ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts

@ -25,7 +25,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [ export const entityViewRoutes: Routes = [
{ {
path: 'entityViews', path: 'entityViews',
data: { data: {
@ -68,6 +68,18 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'entityViews',
pathMatch: 'full',
redirectTo: '/entities/entityViews'
},
{
path: 'entityViews/:entityId',
redirectTo: '/entities/entityViews/:entityId'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],

54
ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts

@ -0,0 +1,54 @@
///
/// Copyright © 2016-2023 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.
///
import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core';
import { ruleChainsRoutes } from '@home/pages/rulechain/rulechain-routing.module';
import { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module';
import { vcRoutes } from '@home/pages/vc/vc-routing.module';
const routes: Routes = [
{
path: 'features',
data: {
auth: [Authority.TENANT_ADMIN],
breadcrumb: {
label: 'feature.advanced-features',
icon: 'construction'
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.TENANT_ADMIN],
redirectTo: '/features/ruleChains'
}
},
...ruleChainsRoutes,
...otaUpdatesRoutes,
...vcRoutes
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FeaturesRoutingModule { }

30
ui-ngx/src/app/modules/home/pages/features/features.module.ts

@ -0,0 +1,30 @@
///
/// Copyright © 2016-2023 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.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { FeaturesRoutingModule } from '@home/pages/features/features-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SharedModule,
FeaturesRoutingModule
]
})
export class FeaturesModule { }

2
ui-ngx/src/app/modules/home/pages/home-pages.models.ts

@ -19,6 +19,6 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction<EntityDetailsPageComponent> export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction<EntityDetailsPageComponent>
= ((route, translate, component) => { = ((route, translate, component) => {
return component.entity?.name || component.headerSubtitle; return component.entity?.name;
}); });

8
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -38,6 +38,10 @@ import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
import { VcModule } from '@home/pages/vc/vc.module'; import { VcModule } from '@home/pages/vc/vc.module';
import { AssetProfileModule } from '@home/pages/asset-profile/asset-profile.module'; import { AssetProfileModule } from '@home/pages/asset-profile/asset-profile.module';
import { ProfilesModule } from '@home/pages/profiles/profiles.module'; import { ProfilesModule } from '@home/pages/profiles/profiles.module';
import { AlarmModule } from '@home/pages/alarm/alarm.module';
import { EntitiesModule } from '@home/pages/entities/entities.module';
import { FeaturesModule } from '@home/pages/features/features.module';
import { NotificationModule } from '@home/pages/notification/notification.module';
@NgModule({ @NgModule({
exports: [ exports: [
@ -50,8 +54,12 @@ import { ProfilesModule } from '@home/pages/profiles/profiles.module';
DeviceProfileModule, DeviceProfileModule,
AssetProfileModule, AssetProfileModule,
ProfilesModule, ProfilesModule,
EntitiesModule,
FeaturesModule,
NotificationModule,
DeviceModule, DeviceModule,
AssetModule, AssetModule,
AlarmModule,
EdgeModule, EdgeModule,
EntityViewModule, EntityViewModule,
CustomerModule, CustomerModule,

135
ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts

@ -0,0 +1,135 @@
///
/// Copyright © 2016-2023 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.
///
import { ActivatedRoute, RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { Component, NgModule, OnInit } from '@angular/core';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-notification-temp-component',
template: '<div>{{text}}</div>',
styleUrls: []
})
class NotificationTempComponent implements OnInit {
text: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
if (isDefinedAndNotNull(this.route.snapshot.data.text)) {
this.text = this.route.snapshot.data.text;
}
}
}
const routes: Routes = [
{
path: 'notification',
component: RouterTabsComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
breadcrumb: {
label: 'notification.notification-center',
icon: 'mdi:message-badge'
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
redirectTo: '/notification/inbox'
}
},
{
path: 'inbox',
component: NotificationTempComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'notification.inbox',
breadcrumb: {
label: 'notification.inbox',
icon: 'inbox'
},
text: 'TODO: Implement inbox'
}
},
{
path: 'sent',
component: NotificationTempComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'notification.sent',
breadcrumb: {
label: 'notification.sent',
icon: 'outbox'
},
text: 'TODO: Implement sent'
}
},
{
path: 'templates',
component: NotificationTempComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'notification.templates',
breadcrumb: {
label: 'notification.templates',
icon: 'mdi:message-draw'
},
text: 'TODO: Implement templates'
}
},
{
path: 'recipients',
component: NotificationTempComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'notification.recipients',
breadcrumb: {
label: 'notification.recipients',
icon: 'contacts'
},
text: 'TODO: Implement recipients'
}
},
{
path: 'rules',
component: NotificationTempComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'notification.rules',
breadcrumb: {
label: 'notification.rules',
icon: 'mdi:message-cog'
},
text: 'TODO: Implement rules'
}
}
]
}
];
@NgModule({
declarations: [NotificationTempComponent],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class NotificationRoutingModule { }

30
ui-ngx/src/app/modules/home/pages/notification/notification.module.ts

@ -0,0 +1,30 @@
///
/// Copyright © 2016-2023 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.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { NotificationRoutingModule } from '@home/pages/notification/notification-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SharedModule,
NotificationRoutingModule
]
})
export class NotificationModule { }

14
ui-ngx/src/app/modules/home/pages/ota-update/ota-update-routing.module.ts

@ -24,7 +24,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [ export const otaUpdatesRoutes: Routes = [
{ {
path: 'otaUpdates', path: 'otaUpdates',
data: { data: {
@ -65,6 +65,18 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'otaUpdates',
pathMatch: 'full',
redirectTo: '/features/otaUpdates'
},
{
path: 'otaUpdates/:entityId',
redirectTo: '/features/otaUpdates/:entityId'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],

2
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -1456,7 +1456,7 @@ export class RuleChainPageComponent extends PageComponent
this.isDirtyValue = false; this.isDirtyValue = false;
this.isImport = false; this.isImport = false;
if (this.ruleChainType !== RuleChainType.EDGE) { if (this.ruleChainType !== RuleChainType.EDGE) {
this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); this.router.navigateByUrl(`features/ruleChains/${this.ruleChain.id.id}`);
} else { } else {
this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`);
} }

20
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts

@ -122,7 +122,7 @@ export const importRuleChainBreadcumbLabelFunction: BreadCrumbLabelFunction<Rule
return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`; return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`;
}); });
const routes: Routes = [ export const ruleChainsRoutes: Routes = [
{ {
path: 'ruleChains', path: 'ruleChains',
data: { data: {
@ -189,6 +189,24 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'ruleChains',
pathMatch: 'full',
redirectTo: '/features/ruleChains'
},
{
path: 'ruleChains/:ruleChainId',
pathMatch: 'full',
redirectTo: '/features/ruleChains/:ruleChainId'
},
{
path: 'ruleChains/ruleChain/import',
pathMatch: 'full',
redirectTo: '/features/ruleChains/ruleChain/import'
}
];
// @dynamic // @dynamic
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],

2
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -301,7 +301,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
if (this.config.componentsData.ruleChainScope === 'edges') { if (this.config.componentsData.ruleChainScope === 'edges') {
this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`); this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`);
} else { } else {
this.router.navigateByUrl(`ruleChains/ruleChain/import`); this.router.navigateByUrl(`features/ruleChains/ruleChain/import`);
} }
} }
}); });

5
ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html

@ -36,8 +36,3 @@
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab"> label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table> <tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab> </mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
[entityId]="entity.id"></tb-event-table>
</mat-tab>

9
ui-ngx/src/app/modules/home/pages/vc/vc-routing.module.ts

@ -20,7 +20,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
const routes: Routes = [ export const vcRoutes: Routes = [
{ {
path: 'vc', path: 'vc',
component: VersionControlComponent, component: VersionControlComponent,
@ -36,6 +36,13 @@ const routes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'vc',
redirectTo: '/features/vc'
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],

28
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -27,8 +27,8 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { detailsToWidgetInfo, toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models'; import { widgetType, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { WidgetsData } from '@home/models/dashboard-component.models'; import { WidgetsData } from '@home/models/dashboard-component.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -120,7 +120,7 @@ export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction<any> = (
export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> = export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> =
((route, translate, component) => component ? component.widget.widgetName : ''); ((route, translate, component) => component ? component.widget.widgetName : '');
export const routes: Routes = [ export const widgetsBundlesRoutes: Routes = [
{ {
path: 'widgets-bundles', path: 'widgets-bundles',
data: { data: {
@ -199,6 +199,28 @@ export const routes: Routes = [
] ]
} }
] ]
},
];
const routes: Routes = [
{
path: 'widgets-bundles',
pathMatch: 'full',
redirectTo: '/resources/widgets-bundles'
},
{
path: 'widgets-bundles/:widgetsBundleId/widgetTypes',
pathMatch: 'full',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes',
},
{
path: 'widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
pathMatch: 'full',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
},
{
path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
} }
]; ];

2
ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts

@ -160,7 +160,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.router.navigateByUrl(`widgets-bundles/${widgetsBundle.id.id}/widgetTypes`); this.router.navigateByUrl(`resources/widgets-bundles/${widgetsBundle.id.id}/widgetTypes`);
} }
exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) { exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {

33
ui-ngx/src/app/shared/components/breadcrumb.component.html

@ -17,25 +17,32 @@
--> -->
<div fxFlex class="tb-breadcrumb" fxLayout="row"> <div fxFlex class="tb-breadcrumb" fxLayout="row">
<h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb"> <h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb">
{{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }} {{ breadcrumb.ignoreTranslate
? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label))
: (breadcrumb.label | translate) }}
</h1> </h1>
<span fxHide.lt-md fxLayout="row" *ngFor="let breadcrumb of breadcrumbs$ | async; trackBy: trackByBreadcrumbs; last as isLast;" [ngSwitch]="isLast"> <span fxHide.lt-md fxLayout="row" *ngFor="let breadcrumb of breadcrumbs$ | async; trackBy: trackByBreadcrumbs; last as isLast;" [ngSwitch]="isLast">
<a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams"> <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> <ng-container
</mat-icon> *ngTemplateOutlet="breadcrumbWithIcon;context:{breadcrumb: breadcrumb}">
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> </ng-container>
{{ breadcrumb.icon }}
</mat-icon>
{{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
</a> </a>
<span *ngSwitchCase="true"> <span *ngSwitchCase="true">
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> <ng-container
</mat-icon> *ngTemplateOutlet="breadcrumbWithIcon;context:{breadcrumb: breadcrumb}">
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> </ng-container>
{{ breadcrumb.icon }}
</mat-icon>
{{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
</span> </span>
<span class="divider" [fxHide]="isLast"> > </span> <span class="divider" [fxHide]="isLast"> > </span>
</span> </span>
</div> </div>
<ng-template #breadcrumbWithIcon let-breadcrumb="breadcrumb">
<img *ngIf="breadcrumb.iconUrl" [src]="breadcrumb.iconUrl"/>
<mat-icon *ngIf="!breadcrumb.iconUrl && breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
</mat-icon>
<mat-icon *ngIf="!breadcrumb.iconUrl && !breadcrumb.isMdiIcon">
{{ breadcrumb.icon }}
</mat-icon>
{{ breadcrumb.ignoreTranslate
? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label))
: (breadcrumb.label | translate) }}
</ng-template>

19
ui-ngx/src/app/shared/components/breadcrumb.component.ts

@ -18,10 +18,12 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy
import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { guid } from '@core/utils'; import { guid } from '@core/utils';
import { BroadcastService } from '@core/services/broadcast.service'; import { BroadcastService } from '@core/services/broadcast.service';
import { ActiveComponentService } from '@core/services/active-component.service';
import { UtilsService } from '@core/services/utils.service';
@Component({ @Component({
selector: 'tb-breadcrumb', selector: 'tb-breadcrumb',
@ -34,8 +36,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
activeComponentValue: any; activeComponentValue: any;
updateBreadcrumbsSubscription: Subscription = null; updateBreadcrumbsSubscription: Subscription = null;
@Input() setActiveComponent(activeComponent: any) {
set activeComponent(activeComponent: any) {
if (this.updateBreadcrumbsSubscription) { if (this.updateBreadcrumbsSubscription) {
this.updateBreadcrumbsSubscription.unsubscribe(); this.updateBreadcrumbsSubscription.unsubscribe();
this.updateBreadcrumbsSubscription = null; this.updateBreadcrumbsSubscription = null;
@ -48,7 +49,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
} }
} }
breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>([]);
routerEventsSubscription = this.router.events.pipe( routerEventsSubscription = this.router.events.pipe(
filter((event) => event instanceof NavigationEnd ), filter((event) => event instanceof NavigationEnd ),
@ -56,6 +57,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) ) map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) )
).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) ); ).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) );
activeComponentSubscription = this.activeComponentService.onActiveComponentChanged().subscribe(comp => this.setActiveComponent(comp));
lastBreadcrumb$ = this.breadcrumbs$.pipe( lastBreadcrumb$ = this.breadcrumbs$.pipe(
map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1]) map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1])
); );
@ -63,20 +66,26 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
constructor(private router: Router, constructor(private router: Router,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private broadcast: BroadcastService, private broadcast: BroadcastService,
private activeComponentService: ActiveComponentService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private translate: TranslateService) { private translate: TranslateService,
public utils: UtilsService) {
} }
ngOnInit(): void { ngOnInit(): void {
this.broadcast.on('updateBreadcrumb', () => { this.broadcast.on('updateBreadcrumb', () => {
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.setActiveComponent(this.activeComponentService.getCurrentActiveComponent());
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.routerEventsSubscription) { if (this.routerEventsSubscription) {
this.routerEventsSubscription.unsubscribe(); this.routerEventsSubscription.unsubscribe();
} }
if (this.activeComponentSubscription) {
this.activeComponentSubscription.unsubscribe();
}
} }
private lastChild(route: ActivatedRouteSnapshot) { private lastChild(route: ActivatedRouteSnapshot) {

7
ui-ngx/src/app/shared/models/alarm.models.ts

@ -26,6 +26,11 @@ import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/tab
import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; import { AlarmCommentId } from '@shared/models/id/alarm-comment-id';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
export enum AlarmsMode {
ALL,
ENTITY
}
export enum AlarmSeverity { export enum AlarmSeverity {
CRITICAL = 'CRITICAL', CRITICAL = 'CRITICAL',
MAJOR = 'MAJOR', MAJOR = 'MAJOR',
@ -278,7 +283,7 @@ export class AlarmQuery {
} }
public toQuery(): string { public toQuery(): string {
let query = `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}`; let query = this.affectedEntityId ? `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}` : '';
query += this.pageLink.toQuery(); query += this.pageLink.toQuery();
if (this.searchStatus) { if (this.searchStatus) {
query += `&searchStatus=${this.searchStatus}`; query += `&searchStatus=${this.searchStatus}`;

1
ui-ngx/src/app/shared/models/asset.models.ts

@ -39,6 +39,7 @@ export interface AssetProfile extends BaseData<AssetProfileId>, ExportableEntity
} }
export interface AssetProfileInfo extends EntityInfoData { export interface AssetProfileInfo extends EntityInfoData {
tenantId?: TenantId;
image?: string; image?: string;
defaultDashboardId?: DashboardId; defaultDashboardId?: DashboardId;
} }

5
ui-ngx/src/app/shared/models/device.models.ts

@ -500,13 +500,15 @@ export interface CustomTimeSchedulerItem{
endsOn: number; endsOn: number;
} }
export interface AlarmRule { interface AlarmRule {
condition: AlarmCondition; condition: AlarmCondition;
alarmDetails?: string; alarmDetails?: string;
dashboardId?: DashboardId; dashboardId?: DashboardId;
schedule?: AlarmSchedule; schedule?: AlarmSchedule;
} }
export { AlarmRule as DeviceProfileAlarmRule };
export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null { export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null {
const alarmRule: AlarmRule = control.value; const alarmRule: AlarmRule = control.value;
return alarmRuleValid(alarmRule) ? null : {alarmRule: true}; return alarmRuleValid(alarmRule) ? null : {alarmRule: true};
@ -585,6 +587,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId>, ExportableEnti
} }
export interface DeviceProfileInfo extends EntityInfoData { export interface DeviceProfileInfo extends EntityInfoData {
tenantId?: TenantId;
type: DeviceProfileType; type: DeviceProfileType;
transportType: DeviceTransportType; transportType: DeviceTransportType;
image?: string; image?: string;

16
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -251,7 +251,7 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
typePlural: 'entity.type-alarms', typePlural: 'entity.type-alarms',
list: 'entity.list-of-alarms', list: 'entity.list-of-alarms',
nameStartsWith: 'entity.alarm-name-starts-with', nameStartsWith: 'entity.alarm-name-starts-with',
details: 'dashboard.dashboard-details', details: 'alarm.alarm-details',
noEntities: 'alarm.no-alarms-prompt', noEntities: 'alarm.no-alarms-prompt',
search: 'alarm.search', search: 'alarm.search',
selectedEntities: 'alarm.selected-alarms' selectedEntities: 'alarm.selected-alarms'
@ -445,15 +445,15 @@ export const baseDetailsPageByEntityType = new Map<EntityType, string>([
[EntityType.CUSTOMER, '/customers'], [EntityType.CUSTOMER, '/customers'],
[EntityType.USER, '/users'], [EntityType.USER, '/users'],
[EntityType.DASHBOARD, '/dashboards'], [EntityType.DASHBOARD, '/dashboards'],
[EntityType.ASSET, '/assets'], [EntityType.ASSET, '/entities/assets'],
[EntityType.DEVICE, '/devices'], [EntityType.DEVICE, '/entities/devices'],
[EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'], [EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'],
[EntityType.ASSET_PROFILE, '/profiles/assetProfiles'], [EntityType.ASSET_PROFILE, '/profiles/assetProfiles'],
[EntityType.RULE_CHAIN, '/ruleChains'], [EntityType.RULE_CHAIN, '/features/ruleChains'],
[EntityType.EDGE, '/edgeInstances'], [EntityType.EDGE, '/edgeManagement/instances'],
[EntityType.ENTITY_VIEW, '/entityViews'], [EntityType.ENTITY_VIEW, '/entities/entityViews'],
[EntityType.TB_RESOURCE, '/settings/resources-library'], [EntityType.TB_RESOURCE, '/resources/resources-library'],
[EntityType.OTA_PACKAGE, '/otaUpdates'], [EntityType.OTA_PACKAGE, '/features/otaUpdates'],
[EntityType.QUEUE, '/settings/queues'] [EntityType.QUEUE, '/settings/queues']
]); ]);

1
ui-ngx/src/app/shared/models/public-api.ts

@ -48,6 +48,7 @@ export * from './rule-node.models';
export * from './settings.models'; export * from './settings.models';
export * from './tenant.model'; export * from './tenant.model';
export * from './user.model'; export * from './user.model';
export * from './user-settings.models';
export * from './widget.models'; export * from './widget.models';
export * from './widgets-bundle.model'; export * from './widgets-bundle.model';
export * from './window-message.model'; export * from './window-message.model';

23
ui-ngx/src/app/shared/models/user-settings.models.ts

@ -0,0 +1,23 @@
///
/// Copyright © 2016-2023 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.
///
export interface UserSettings {
openedMenuSections?: string[];
}
export const initialUserSettings: UserSettings = {
openedMenuSections: []
};

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

@ -76,9 +76,11 @@
"none": "None" "none": "None"
}, },
"admin": { "admin": {
"settings": "Settings",
"general": "General", "general": "General",
"general-settings": "General Settings", "general-settings": "General Settings",
"home-settings": "Home Settings", "home-settings": "Home Settings",
"home": "Home",
"outgoing-mail": "Mail Server", "outgoing-mail": "Mail Server",
"outgoing-mail-settings": "Outgoing Mail Server Settings", "outgoing-mail-settings": "Outgoing Mail Server Settings",
"system-settings": "System Settings", "system-settings": "System Settings",
@ -325,6 +327,7 @@
"queue-processing-strategy": "Processing strategy", "queue-processing-strategy": "Processing strategy",
"queue-configuration": "Queue configuration", "queue-configuration": "Queue configuration",
"repository-settings": "Repository settings", "repository-settings": "Repository settings",
"repository": "Repository",
"repository-url": "Repository URL", "repository-url": "Repository URL",
"repository-url-required": "Repository URL is required.", "repository-url-required": "Repository URL is required.",
"default-branch": "Default branch name", "default-branch": "Default branch name",
@ -346,6 +349,7 @@
"delete-repository-settings-title": "Are you sure you want to delete repository settings?", "delete-repository-settings-title": "Are you sure you want to delete repository settings?",
"delete-repository-settings-text": "Be careful, after the confirmation the repository settings will be removed and version control feature will be unavailable.", "delete-repository-settings-text": "Be careful, after the confirmation the repository settings will be removed and version control feature will be unavailable.",
"auto-commit-settings": "Auto-commit settings", "auto-commit-settings": "Auto-commit settings",
"auto-commit": "Auto-commit",
"auto-commit-entities": "Auto-commit entities", "auto-commit-entities": "Auto-commit entities",
"no-auto-commit-entities-prompt": "No entities configured for auto-commit", "no-auto-commit-entities-prompt": "No entities configured for auto-commit",
"delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?", "delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?",
@ -404,11 +408,15 @@
"generate-key": "Generate key", "generate-key": "Generate key",
"info-header": "All users will be to re-logined", "info-header": "All users will be to re-logined",
"info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets." "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets."
} },
}, "resources": "Resources",
"notifications": "Notifications",
"notifications-settings": "Notifications settings"
},
"alarm": { "alarm": {
"alarm": "Alarm", "alarm": "Alarm",
"alarms": "Alarms", "alarms": "Alarms",
"all-alarms": "All alarms",
"select-alarm": "Select alarm", "select-alarm": "Select alarm",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.", "no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required", "alarm-required": "Alarm is required",
@ -1729,6 +1737,7 @@
"edge": { "edge": {
"edge": "Edge", "edge": "Edge",
"edge-instances": "Edge instances", "edge-instances": "Edge instances",
"instances": "Instances",
"edge-file": "Edge file", "edge-file": "Edge file",
"name-max-length": "Name should be less than 256", "name-max-length": "Name should be less than 256",
"label-max-length": "Label should be less than 256", "label-max-length": "Label should be less than 256",
@ -1952,7 +1961,7 @@
"dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
"type-alarm": "Alarm", "type-alarm": "Alarm",
"type-alarms": "Alarms", "type-alarms": "Alarms",
"list-of-alarms": "{ count, plural, =1 {One alarms} other {List of # alarms} }", "list-of-alarms": "{ count, plural, =1 {One alarm} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
"type-rulechain": "Rule chain", "type-rulechain": "Rule chain",
"type-rulechains": "Rule chains", "type-rulechains": "Rule chains",
@ -2318,6 +2327,9 @@
"file": "Extensions file", "file": "Extensions file",
"invalid-file-error": "Invalid extension file" "invalid-file-error": "Invalid extension file"
}, },
"feature": {
"advanced-features": "Advanced features"
},
"filter": { "filter": {
"add": "Add filter", "add": "Add filter",
"edit": "Edit filter", "edit": "Edit filter",
@ -2699,6 +2711,14 @@
"copy-code": "Click to copy", "copy-code": "Click to copy",
"copied": "Copied!" "copied": "Copied!"
}, },
"notification": {
"notification-center": "Notification center",
"inbox": "Inbox",
"sent": "Sent",
"templates": "Templates",
"recipients": "Recipients",
"rules": "Rules"
},
"ota-update": { "ota-update": {
"add": "Add package", "add": "Add package",
"assign-firmware": "Assigned firmware", "assign-firmware": "Assigned firmware",

1
ui-ngx/src/assets/mdi.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.3 MiB

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save