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);
List<AssetProfileInfo> assetProfileInfos = assetProfiles.stream().map(assetProfile -> new AssetProfileInfo(assetProfile.getId(),
assetProfile.getTenantId(),
assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList());
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);
List<DeviceProfileInfo> deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(),
deviceProfile.getTenantId(),
deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(),
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.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
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.")
private final DeviceTransportType transportType;
@ApiModelProperty(position = 7, value = "Tenant id.")
private final TenantId tenantId;
@JsonCreator
public DeviceProfileInfo(@JsonProperty("id") EntityId id,
@JsonProperty("tenantId") TenantId tenantId,
@JsonProperty("name") String name,
@JsonProperty("image") String image,
@JsonProperty("defaultDashboardId") DashboardId defaultDashboardId,
@JsonProperty("type") DeviceProfileType type,
@JsonProperty("transportType") DeviceTransportType transportType) {
super(id, name);
this.tenantId = tenantId;
this.image = image;
this.defaultDashboardId = defaultDashboardId;
this.type = type;
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);
this.tenantId = new TenantId(tenantId);
this.image = image;
this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null;
this.type = type;
@ -64,7 +71,7 @@ public class DeviceProfileInfo extends EntityInfo {
}
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());
}

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.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
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.")
private final DashboardId defaultDashboardId;
@ApiModelProperty(position = 5, value = "Tenant id.")
private final TenantId tenantId;
@JsonCreator
public AssetProfileInfo(@JsonProperty("id") EntityId id,
@JsonProperty("name") String name,
@JsonProperty("image") String image,
@JsonProperty("defaultDashboardId") DashboardId defaultDashboardId) {
@JsonProperty("tenantId") TenantId tenantId,
@JsonProperty("name") String name,
@JsonProperty("image") String image,
@JsonProperty("defaultDashboardId") DashboardId defaultDashboardId) {
super(id, name);
this.tenantId = tenantId;
this.image = image;
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);
this.tenantId = new TenantId(tenantId);
this.image = image;
this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null;
}
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) {
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());
}

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) {
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());
}

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> {
@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 a.id = :assetProfileId")
AssetProfileInfo findAssetProfileInfoById(@Param("assetProfileId") UUID assetProfileId);
@ -39,7 +39,7 @@ public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity
@Param("textSearch") String textSearch,
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 " +
"a.tenantId = :tenantId AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
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")
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 " +
"WHERE a.tenantId = :tenantId AND a.isDefault = true")
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> {
@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 d.id = :deviceProfileId")
DeviceProfileInfo findDeviceProfileInfoById(@Param("deviceProfileId") UUID deviceProfileId);
@ -40,14 +40,14 @@ public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEnti
@Param("textSearch") String textSearch,
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 " +
"d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
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 " +
"d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
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")
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 " +
"WHERE d.tenantId = :tenantId AND d.isDefault = true")
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);
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());
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);
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.getType(), deviceProfile.getTransportType())).collect(Collectors.toList());

5
ui-ngx/angular.json

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

1
ui-ngx/package.json

@ -36,6 +36,7 @@
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/pickers": "3.3.10",
"@mdi/svg": "^7.1.96",
"@messageformat/core": "^3.0.1",
"@ngrx/effects": "^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}`);
this.matIconRegistry.addSvgIconSetInNamespace('mdi',
this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg'));
this.matIconRegistry.addSvgIconResolver((name, namespace) => {
if (namespace === 'mdi') {
return this.domSanitizer.bypassSecurityTrustResourceUrl(`./assets/mdi/${name}.svg`);
} else {
return null;
}
});
this.matIconRegistry.addSvgIconLiteral(
'google-logo',

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

@ -24,7 +24,8 @@ export enum AuthActionTypes {
LOAD_USER = '[Auth] Load User',
UPDATE_USER_DETAILS = '[Auth] Update User Details',
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 {
@ -61,5 +62,12 @@ export class ActionAuthUpdateHasRepository implements Action {
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 |
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 { UserSettings } from '@shared/models/user-settings.models';
export interface SysParamsState {
userTokenAccessEnabled: boolean;
@ -22,6 +23,7 @@ export interface SysParamsState {
edgesSupportEnabled: boolean;
hasRepository: boolean;
tbelEnabled: boolean;
userSettings: UserSettings;
}
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 { AuthActions, AuthActionTypes } from './auth.actions';
import { initialUserSettings } from '@shared/models/user-settings.models';
const emptyUserAuthState: AuthPayload = {
authUser: null,
@ -25,7 +26,8 @@ const emptyUserAuthState: AuthPayload = {
allowedDashboardIds: [],
edgesSupportEnabled: false,
hasRepository: false,
tbelEnabled: false
tbelEnabled: false,
userSettings: initialUserSettings
};
export const initialState: AuthState = {
@ -35,10 +37,10 @@ export const initialState: AuthState = {
...emptyUserAuthState
};
export function authReducer(
export const authReducer = (
state: AuthState = initialState,
action: AuthActions
): AuthState {
): AuthState => {
switch (action.type) {
case AuthActionTypes.AUTHENTICATED:
return { ...state, isAuthenticated: true, ...action.payload };
@ -59,7 +61,19 @@ export function authReducer(
case AuthActionTypes.UPDATE_HAS_REPOSITORY:
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:
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 { take } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
export const selectAuthState = createFeatureSelector< AuthState>(
'auth'
@ -65,18 +66,45 @@ export const selectTbelEnabled = createSelector(
(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;
store.pipe(select(selectAuth), take(1)).subscribe(
val => state = val
);
return state;
}
};
export function getCurrentAuthUser(store: Store<AppState>): AuthUser {
export const getCurrentAuthUser = (store: Store<AppState>): AuthUser => {
let authUser: AuthUser;
store.pipe(select(selectAuthUser), take(1)).subscribe(
val => authUser = val
);
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 { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.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({
providedIn: 'root'
@ -66,6 +68,7 @@ export class AuthService {
private dashboardService: DashboardService,
private adminService: AdminService,
private translate: TranslateService,
private userSettingsService: UserSettingsService,
private dialog: MatDialog
) {
}
@ -127,7 +130,8 @@ export class AuthService {
}
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(
tap((loginResponse: LoginResponse) => {
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> {
const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
this.fetchAllowedDashboardIds(authPayload),
this.loadIsEdgesSupportEnabled(),
this.loadHasRepository(authPayload.authUser),
this.loadTbelEnabled(authPayload.authUser),
this.loadUserSettings(),
this.timeService.loadMaxDatapointsLimit()];
return forkJoin(sources)
.pipe(map((data) => {
@ -504,10 +513,9 @@ export class AuthService {
const edgesSupportEnabled: boolean = data[2] as boolean;
const hasRepository: boolean = data[3] as boolean;
const tbelEnabled: boolean = data[4] as boolean;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled};
}, catchError((err) => {
return of({});
})));
const userSettings = data[5] as UserSettings;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled, userSettings};
}, catchError((err) => of({}))));
}
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.effects';
export * from './auth.models';
export * from './auth.reducer';
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 { notificationReducer } from '@app/core/notification/notification.reducer';
import { NotificationEffects } from '@app/core/notification/notification.effects';
import { AuthEffects } from '@core/auth/auth.effects';
export const reducers: ActionReducerMap<AppState> = {
load: loadReducer,
@ -49,6 +50,7 @@ if (!env.production) {
}
export const effects: Type<any>[] = [
AuthEffects,
SettingsEffects,
NotificationEffects
];

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

@ -78,6 +78,12 @@ export class AlarmService {
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,
config?: RequestConfig): Observable<AlarmSeverity> {
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 './ui-settings.service';
export * from './user.service';
export * from './user-settings.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;
icon: string;
isMdiIcon?: boolean;
height?: string;
pages?: Array<MenuSection>;
opened?: boolean;
disabled?: boolean;
rootOnly?: boolean;
}
export interface HomeSection {

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

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

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

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

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

@ -15,4 +15,4 @@
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.
*/
:host ::ng-deep {
tb-entities-table {
tb-entities-table.tb-details-mode {
.mat-drawer-container {
background-color: white;
.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 { DialogService } from '@core/services/dialog.service';
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-alarm-table',
@ -39,6 +41,8 @@ export class AlarmTableComponent implements OnInit {
activeValue = false;
dirtyValue = false;
entityIdValue: EntityId;
alarmsMode = AlarmsMode.ENTITY;
detailsMode = true;
@Input()
set active(active: boolean) {
@ -77,24 +81,34 @@ export class AlarmTableComponent implements OnInit {
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private utilsService: UtilsService) {
private utilsService: UtilsService,
private route: ActivatedRoute) {
}
ngOnInit() {
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.alarmService,
this.dialogService,
this.translate,
this.datePipe,
this.dialog,
this.alarmsMode,
this.entityIdValue,
AlarmSearchStatus.ANY,
this.store,
this.viewContainerRef,
this.overlay,
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 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()">
<div class="mat-toolbar-tools">
<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>
<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-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly"
<mat-select [disabled]="(isLoading$ | async)"
[ngModel]="attributeScope"
(ngModelChange)="attributeScopeChanged($event)">
<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%;
height: 100%;
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{
min-height: auto;

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

@ -34,7 +34,7 @@
</mat-drawer>
<mat-drawer-content>
<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()">
<div class="mat-toolbar-tools">
<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%;
height: 100%;
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{
min-height: auto;
@ -36,6 +43,7 @@
.title-container{
overflow: hidden;
height: 100%;
}
.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 rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
public route: ActivatedRoute,
public translate: TranslateService,
@ -143,7 +145,11 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
if (this.entitiesTableConfig) {
this.init(this.entitiesTableConfig);
} else {
this.init(this.route.snapshot.data.entitiesTableConfig);
this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
this.init(data.entitiesTableConfig);
}
));
}
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
@ -159,6 +165,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
}
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-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 tb-ellipsis">{{ headerTitle }}</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;
}
goBack(): void {
this.router.navigate(['../'], { relativeTo: this.route });
}
private onUpdateEntity() {
this.broadcast.broadcast('updateBreadcrumb');
this.isReadOnly = this.entitiesTableConfig.detailsReadonly(this.entity);
@ -164,7 +168,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
if (result) {
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 { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
@NgModule({
declarations:
[
RouterTabsComponent,
EntitiesTableComponent,
AddEntityDialogComponent,
DetailsPanelComponent,
@ -334,6 +336,7 @@ import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assign
DeviceProfileCommonModule
],
exports: [
RouterTabsComponent,
EntitiesTableComponent,
AddEntityDialogComponent,
DetailsPanelComponent,

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

@ -25,7 +25,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { AlarmRule } from '@shared/models/device.models';
import { DeviceProfileAlarmRule } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isDefinedAndNotNull } from '@core/utils';
@ -71,7 +71,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
@Input()
deviceProfileId: EntityId;
private modelValue: AlarmRule;
private modelValue: DeviceProfileAlarmRule;
alarmRuleFormGroup: UntypedFormGroup;
@ -112,7 +112,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
}
}
writeValue(value: AlarmRule): void {
writeValue(value: DeviceProfileAlarmRule): void {
this.modelValue = value;
const model = 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,
Validators
} 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 { Subscription } from 'rxjs';
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) {
this.valueChangeSubscription.unsubscribe();
}
@ -138,7 +138,7 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}
public addCreateAlarmRule() {
const createAlarmRule: AlarmRule = {
const createAlarmRule: DeviceProfileAlarmRule = {
condition: {
condition: []
}
@ -179,15 +179,15 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
private updateUsedSeverities() {
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) => {
this.usedSeverities[index] = AlarmSeverity[rule.severity];
});
}
private updateModel() {
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {};
const value: {severity: string, alarmRule: DeviceProfileAlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: DeviceProfileAlarmRule} = {};
value.forEach(v => {
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,
Validators
} 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 { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
@ -128,7 +128,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
}
public addClearAlarmRule() {
const clearAlarmRule: AlarmRule = {
const clearAlarmRule: DeviceProfileAlarmRule = {
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')
.patchValue(this.modelValue.additionalInfo?.description, {emitEvent: false});
this.submitStrategyTypeChanged();
}
if (!this.disabled && !this.queueFormGroup.valid) {
this.updateModel();
if (!this.disabled && !this.queueFormGroup.valid) {
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 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()">
<div class="mat-toolbar-tools">
<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%;
height: 100%;
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{
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 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">
<div class="mat-toolbar-tools">
<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%;
height: 100%;
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{
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 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">
<div class="mat-toolbar-tools">
<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%;
height: 100%;
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 {
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 { TbInject } from '@shared/decorators/tb-inject';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service';
@Directive()
// 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.customDialog = $injector.get(CustomDialogService);
this.ctx.resourceService = $injector.get(ResourceService);
this.ctx.userSettingsService = $injector.get(UserSettingsService);
this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService);
this.ctx.date = $injector.get(DatePipe);
this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe);

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

@ -35,7 +35,7 @@
</mat-sidenav>
<mat-sidenav-content>
<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"
[ngClass]="{'tb-invisible': displaySearchMode()}"
fxHide.gt-sm (click)="sidenav.toggle()">
@ -52,7 +52,7 @@
<mat-icon class="material-icons">arrow_back</mat-icon>
</button>
<tb-breadcrumb [fxShow]="!displaySearchMode()"
fxFlex [activeComponent]="activeComponent" class="mat-toolbar-tools">
fxFlex class="mat-toolbar-tools">
</tb-breadcrumb>
<div [fxShow]="displaySearchMode()" fxFlex fxLayout="row" class="tb-dark">
<mat-form-field fxFlex class="tb-appearance-transparent">
@ -72,7 +72,7 @@
<tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu>
</mat-toolbar>
<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>
<div fxFlex fxLayout="column" tb-toast class="tb-main-content">
<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 { fromEvent, Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
import { Store } from '@ngrx/store';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { User } from '@shared/models/user.model';
import { PageComponent } from '@shared/components/page.component';
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 screenfull from 'screenfull';
import { MatSidenav } from '@angular/material/sidenav';
import { AuthState } from '@core/auth/auth.models';
import { WINDOW } from '@core/services/window.service';
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({
selector: 'tb-home',
@ -57,28 +58,21 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
fullscreenEnabled = screenfull.isEnabled;
authUser$: Observable<any>;
userDetails$: Observable<User>;
userDetailsString: Observable<string>;
searchEnabled = false;
showSearch = false;
searchText = '';
hideLoadingBar = false;
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
private activeComponentService: ActiveComponentService,
public breakpointObserver: BreakpointObserver) {
super(store);
}
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']);
this.sidenavMode = isGtSm ? 'side' : 'over';
this.sidenavOpened = isGtSm;
@ -130,6 +124,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
}
activeComponentChanged(activeComponent: any) {
this.activeComponentService.setCurrentActiveComponent(activeComponent);
if (!this.activeComponent) {
setTimeout(() => {
this.updateActiveComponent(activeComponent);
@ -143,6 +138,7 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
this.showSearch = false;
this.searchText = '';
this.activeComponent = activeComponent;
this.hideLoadingBar = activeComponent && activeComponent instanceof RouterTabsComponent;
if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) {
this.searchEnabled = true;
this.searchableComponent = this.activeComponent;

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

@ -15,8 +15,8 @@
limitations under the License.
-->
<a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', 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" [svgIcon]="section.icon"></mat-icon>
<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">{{section.icon}}</mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon"></mat-icon>
<span>{{section.name | translate}}</span>
</a>

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

@ -15,13 +15,12 @@
limitations under the License.
-->
<a mat-button class="tb-button-toggle"
routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', 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" [svgIcon]="section.icon"></mat-icon>
<a mat-button class="tb-button-toggle" (click)="toggleSection($event)">
<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>
<span>{{section.name | translate}}</span>
<span class=" pull-right fa fa-chevron-down tb-toggle-icon"
[ngClass]="{'tb-toggled' : sectionActive()}"></span>
[ngClass]="{'tb-toggled' : section.opened}"></span>
</a>
<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" [ngStyle]="{height: sectionHeight()}">
<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;
overflow: hidden;
transition: .75s cubic-bezier(.35, 0, .25, 1);
transition: .45s cubic-bezier(.35, 0, .25, 1);
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 { MenuSection } from '@core/services/menu.models';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ActionPreferencesUpdateOpenedMenuSection } from '@core/auth/auth.actions';
@Component({
selector: 'tb-menu-toggle',
@ -28,24 +31,27 @@ export class MenuToggleComponent implements OnInit {
@Input() section: MenuSection;
constructor(private router: Router) {
constructor(private router: Router,
private store: Store<AppState>) {
}
ngOnInit() {
}
sectionActive(): boolean {
return this.router.isActive(this.section.path, false);
}
sectionHeight(): string {
if (this.router.isActive(this.section.path, false)) {
return this.section.height;
if (this.section.opened) {
return this.section.pages.length * 40 + 'px';
} else {
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){
return section.id;
}

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

@ -63,6 +63,7 @@
white-space: nowrap;
display: inline-flex;
& > span {
letter-spacing: .03125em;
overflow: hidden;
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 { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
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>>(
[
@ -69,6 +70,7 @@ export const ServicesMap = new Map<string, Type<any>>(
['authService', AuthService],
['resourceService', ResourceService],
['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 { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service';
export interface IWidgetAction {
name: string;
@ -180,6 +181,7 @@ export class WidgetContext {
dialogs: DialogService;
customDialog: CustomDialogService;
resourceService: ResourceService;
userSettingsService: UserSettingsService;
telemetryWsService: TelemetryWebsocketService;
telemetrySubscribers?: TelemetrySubscriber[];
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 { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-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()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -49,12 +52,72 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
}
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',
component: RouterTabsComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'admin.system-settings',
label: 'admin.settings',
icon: 'settings'
}
},
@ -97,98 +160,18 @@ const routes: Routes = [
}
},
{
path: 'sms-provider',
path: 'notifications',
component: SmsProviderComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.sms-provider-settings',
breadcrumb: {
label: 'admin.sms-provider',
icon: 'sms'
}
}
},
{
path: 'security-settings',
component: SecuritySettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.security-settings',
title: 'admin.notifications-settings',
breadcrumb: {
label: 'admin.security-settings',
icon: 'security'
label: 'admin.notifications',
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',
data: {
@ -228,16 +211,15 @@ const routes: Routes = [
]
},
{
path: '2fa',
component: TwoFactorAuthSettingsComponent,
path: 'home',
component: HomeSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.2fa.2fa',
auth: [Authority.TENANT_ADMIN],
title: 'admin.home-settings',
breadcrumb: {
label: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
label: 'admin.home',
icon: 'settings_applications'
}
}
},
@ -249,7 +231,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'admin.repository-settings',
breadcrumb: {
label: 'admin.repository-settings',
label: 'admin.repository',
icon: 'manage_history'
}
}
@ -262,11 +244,103 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'admin.auto-commit-settings',
breadcrumb: {
label: 'admin.auto-commit-settings',
label: 'admin.auto-commit',
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.
*/
: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) {
$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);
}

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-header>
<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>
<span fxFlex></span>
<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 { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
const routes: Routes = [
export const assetRoutes: Routes = [
{
path: 'assets',
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({
imports: [RouterModule.forChild(routes)],
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 { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component';
const routes: Routes = [
export const auditLogsRoutes: Routes = [
{
path: 'auditLogs',
component: AuditLogTableComponent,
@ -35,8 +35,16 @@ const routes: Routes = [
}
];
const routes: Routes = [
{
path: 'auditLogs',
redirectTo: '/security-settings/auditLogs'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
exports: [RouterModule],
providers: []
})
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 { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
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',
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({
imports: [RouterModule.forChild(routes)],
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 = [
{
path: 'edgeInstances',
path: 'edgeManagement',
data: {
breadcrumb: {
label: 'edge.edge-instances',
icon: 'router'
label: 'edge.management',
icon: 'settings_input_antenna'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
children: [],
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.edge-instances',
edgesType: 'tenant'
},
resolve: {
entitiesTableConfig: EdgesTableConfigResolver
redirectTo: '/edgeManagement/instances'
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
path: 'instances',
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
label: 'edge.instances',
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: [
@ -95,11 +74,11 @@ const routes: Routes = [
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets',
assetsType: 'edge'
title: 'edge.edge-instances',
edgesType: 'tenant'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
entitiesTableConfig: EdgesTableConfigResolver
}
},
{
@ -109,198 +88,219 @@ const routes: Routes = [
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'domain'
icon: 'router'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets',
assetsType: 'edge'
title: 'edge.edge-instances',
edgesType: 'tenant'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
}
]
},
{
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
entitiesTableConfig: EdgesTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
path: ':edgeId/assets',
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: ':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'
label: 'edge.assets',
icon: 'domain'
}
},
resolve: {
entitiesTableConfig: EntityViewsTableConfigResolver
}
children: [
{
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',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
path: ':edgeId/devices',
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
label: 'edge.devices',
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
}
},
resolve: {
entitiesTableConfig: EntityViewsTableConfigResolver
}
}
]
},
{
path: ':edgeId/dashboards',
data: {
breadcrumb: {
label: 'edge.dashboards',
icon: 'dashboard'
}
},
children: [
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.devices',
devicesType: 'edge'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
},
{
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: '',
component: EntitiesTableComponent,
path: ':edgeId/entityViews',
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
dashboardsType: 'edge'
},
resolve: {
entitiesTableConfig: DashboardsTableConfigResolver
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: {
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',
component: DashboardPageComponent,
path: ':edgeId/dashboards',
data: {
breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction,
label: 'edge.dashboards',
icon: 'dashboard'
} as BreadCrumbConfig<DashboardPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.dashboard',
widgetEditMode: false
}
},
resolve: {
dashboard: DashboardResolver
}
}
]
},
{
path: ':edgeId/ruleChains',
data: {
breadcrumb: {
label: 'edge.edge-rulechains',
icon: 'settings_ethernet'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'edge.rulechains',
ruleChainsType: 'edge'
},
resolve: {
entitiesTableConfig: RuleChainsTableConfigResolver
}
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
dashboardsType: 'edge'
},
resolve: {
entitiesTableConfig: DashboardsTableConfigResolver
},
},
{
path: ':dashboardId',
component: DashboardPageComponent,
data: {
breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction,
icon: 'dashboard'
} as BreadCrumbConfig<DashboardPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.dashboard',
widgetEditMode: false
},
resolve: {
dashboard: DashboardResolver
}
}
]
},
{
path: ':ruleChainId',
component: RuleChainPageComponent,
canDeactivate: [ConfirmOnExitGuard],
path: ':edgeId/ruleChains',
data: {
breadcrumb: {
labelFunction: ruleChainBreadcumbLabelFunction,
label: 'edge.edge-rulechains',
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
}
children: [
{
path: '',
component: EntitiesTableComponent,
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',
@ -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({
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';
break;
}
this.router.navigateByUrl(`edgeInstances/${edge.id.id}/${suffix}`);
this.router.navigateByUrl(`edgeManagement/instances/${edge.id.id}/${suffix}`);
}
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 { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [
export const entityViewRoutes: Routes = [
{
path: 'entityViews',
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({
imports: [RouterModule.forChild(routes)],
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>
= ((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 { AssetProfileModule } from '@home/pages/asset-profile/asset-profile.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({
exports: [
@ -50,8 +54,12 @@ import { ProfilesModule } from '@home/pages/profiles/profiles.module';
DeviceProfileModule,
AssetProfileModule,
ProfilesModule,
EntitiesModule,
FeaturesModule,
NotificationModule,
DeviceModule,
AssetModule,
AlarmModule,
EdgeModule,
EntityViewModule,
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 { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [
export const otaUpdatesRoutes: Routes = [
{
path: 'otaUpdates',
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({
imports: [RouterModule.forChild(routes)],
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.isImport = false;
if (this.ruleChainType !== RuleChainType.EDGE) {
this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`);
this.router.navigateByUrl(`features/ruleChains/${this.ruleChain.id.id}`);
} else {
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}`;
});
const routes: Routes = [
export const ruleChainsRoutes: Routes = [
{
path: 'ruleChains',
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
@NgModule({
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') {
this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`);
} 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">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</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 { VersionControlComponent } from '@home/components/vc/version-control.component';
const routes: Routes = [
export const vcRoutes: Routes = [
{
path: 'vc',
component: VersionControlComponent,
@ -36,6 +36,13 @@ const routes: Routes = [
}
];
const routes: Routes = [
{
path: 'vc',
redirectTo: '/features/vc'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
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 { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators';
import { detailsToWidgetInfo, toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { WidgetsData } from '@home/models/dashboard-component.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -120,7 +120,7 @@ export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction<any> = (
export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> =
((route, translate, component) => component ? component.widget.widgetName : '');
export const routes: Routes = [
export const widgetsBundlesRoutes: Routes = [
{
path: 'widgets-bundles',
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) {
$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) {

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

@ -17,25 +17,32 @@
-->
<div fxFlex class="tb-breadcrumb" fxLayout="row">
<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>
<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">
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
</mat-icon>
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
{{ breadcrumb.icon }}
</mat-icon>
{{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
<ng-container
*ngTemplateOutlet="breadcrumbWithIcon;context:{breadcrumb: breadcrumb}">
</ng-container>
</a>
<span *ngSwitchCase="true">
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
</mat-icon>
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
{{ breadcrumb.icon }}
</mat-icon>
{{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
<ng-container
*ngTemplateOutlet="breadcrumbWithIcon;context:{breadcrumb: breadcrumb}">
</ng-container>
</span>
<span class="divider" [fxHide]="isLast"> > </span>
</span>
</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 { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
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 { guid } from '@core/utils';
import { BroadcastService } from '@core/services/broadcast.service';
import { ActiveComponentService } from '@core/services/active-component.service';
import { UtilsService } from '@core/services/utils.service';
@Component({
selector: 'tb-breadcrumb',
@ -34,8 +36,7 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
activeComponentValue: any;
updateBreadcrumbsSubscription: Subscription = null;
@Input()
set activeComponent(activeComponent: any) {
setActiveComponent(activeComponent: any) {
if (this.updateBreadcrumbsSubscription) {
this.updateBreadcrumbsSubscription.unsubscribe();
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(
filter((event) => event instanceof NavigationEnd ),
@ -56,6 +57,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
map( () => this.buildBreadCrumbs(this.activatedRoute.snapshot) )
).subscribe(breadcrumns => this.breadcrumbs$.next(breadcrumns) );
activeComponentSubscription = this.activeComponentService.onActiveComponentChanged().subscribe(comp => this.setActiveComponent(comp));
lastBreadcrumb$ = this.breadcrumbs$.pipe(
map( breadcrumbs => breadcrumbs[breadcrumbs.length - 1])
);
@ -63,20 +66,26 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private broadcast: BroadcastService,
private activeComponentService: ActiveComponentService,
private cd: ChangeDetectorRef,
private translate: TranslateService) {
private translate: TranslateService,
public utils: UtilsService) {
}
ngOnInit(): void {
this.broadcast.on('updateBreadcrumb', () => {
this.cd.markForCheck();
});
this.setActiveComponent(this.activeComponentService.getCurrentActiveComponent());
}
ngOnDestroy(): void {
if (this.routerEventsSubscription) {
this.routerEventsSubscription.unsubscribe();
}
if (this.activeComponentSubscription) {
this.activeComponentSubscription.unsubscribe();
}
}
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 { UserId } from '@shared/models/id/user-id';
export enum AlarmsMode {
ALL,
ENTITY
}
export enum AlarmSeverity {
CRITICAL = 'CRITICAL',
MAJOR = 'MAJOR',
@ -278,7 +283,7 @@ export class AlarmQuery {
}
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();
if (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 {
tenantId?: TenantId;
image?: string;
defaultDashboardId?: DashboardId;
}

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

@ -500,13 +500,15 @@ export interface CustomTimeSchedulerItem{
endsOn: number;
}
export interface AlarmRule {
interface AlarmRule {
condition: AlarmCondition;
alarmDetails?: string;
dashboardId?: DashboardId;
schedule?: AlarmSchedule;
}
export { AlarmRule as DeviceProfileAlarmRule };
export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null {
const alarmRule: AlarmRule = control.value;
return alarmRuleValid(alarmRule) ? null : {alarmRule: true};
@ -585,6 +587,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId>, ExportableEnti
}
export interface DeviceProfileInfo extends EntityInfoData {
tenantId?: TenantId;
type: DeviceProfileType;
transportType: DeviceTransportType;
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',
list: 'entity.list-of-alarms',
nameStartsWith: 'entity.alarm-name-starts-with',
details: 'dashboard.dashboard-details',
details: 'alarm.alarm-details',
noEntities: 'alarm.no-alarms-prompt',
search: 'alarm.search',
selectedEntities: 'alarm.selected-alarms'
@ -445,15 +445,15 @@ export const baseDetailsPageByEntityType = new Map<EntityType, string>([
[EntityType.CUSTOMER, '/customers'],
[EntityType.USER, '/users'],
[EntityType.DASHBOARD, '/dashboards'],
[EntityType.ASSET, '/assets'],
[EntityType.DEVICE, '/devices'],
[EntityType.ASSET, '/entities/assets'],
[EntityType.DEVICE, '/entities/devices'],
[EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'],
[EntityType.ASSET_PROFILE, '/profiles/assetProfiles'],
[EntityType.RULE_CHAIN, '/ruleChains'],
[EntityType.EDGE, '/edgeInstances'],
[EntityType.ENTITY_VIEW, '/entityViews'],
[EntityType.TB_RESOURCE, '/settings/resources-library'],
[EntityType.OTA_PACKAGE, '/otaUpdates'],
[EntityType.RULE_CHAIN, '/features/ruleChains'],
[EntityType.EDGE, '/edgeManagement/instances'],
[EntityType.ENTITY_VIEW, '/entities/entityViews'],
[EntityType.TB_RESOURCE, '/resources/resources-library'],
[EntityType.OTA_PACKAGE, '/features/otaUpdates'],
[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 './tenant.model';
export * from './user.model';
export * from './user-settings.models';
export * from './widget.models';
export * from './widgets-bundle.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"
},
"admin": {
"settings": "Settings",
"general": "General",
"general-settings": "General Settings",
"home-settings": "Home Settings",
"home": "Home",
"outgoing-mail": "Mail Server",
"outgoing-mail-settings": "Outgoing Mail Server Settings",
"system-settings": "System Settings",
@ -325,6 +327,7 @@
"queue-processing-strategy": "Processing strategy",
"queue-configuration": "Queue configuration",
"repository-settings": "Repository settings",
"repository": "Repository",
"repository-url": "Repository URL",
"repository-url-required": "Repository URL is required.",
"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-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": "Auto-commit",
"auto-commit-entities": "Auto-commit entities",
"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?",
@ -404,11 +408,15 @@
"generate-key": "Generate key",
"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."
}
},
},
"resources": "Resources",
"notifications": "Notifications",
"notifications-settings": "Notifications settings"
},
"alarm": {
"alarm": "Alarm",
"alarms": "Alarms",
"all-alarms": "All alarms",
"select-alarm": "Select alarm",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required",
@ -1729,6 +1737,7 @@
"edge": {
"edge": "Edge",
"edge-instances": "Edge instances",
"instances": "Instances",
"edge-file": "Edge file",
"name-max-length": "Name 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}}'",
"type-alarm": "Alarm",
"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}}'",
"type-rulechain": "Rule chain",
"type-rulechains": "Rule chains",
@ -2318,6 +2327,9 @@
"file": "Extensions file",
"invalid-file-error": "Invalid extension file"
},
"feature": {
"advanced-features": "Advanced features"
},
"filter": {
"add": "Add filter",
"edit": "Edit filter",
@ -2699,6 +2711,14 @@
"copy-code": "Click to copy",
"copied": "Copied!"
},
"notification": {
"notification-center": "Notification center",
"inbox": "Inbox",
"sent": "Sent",
"templates": "Templates",
"recipients": "Recipients",
"rules": "Rules"
},
"ota-update": {
"add": "Add package",
"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