Browse Source

Merge branch 'develop/3.5' into feature/x509-device-cert-impr

pull/7935/head
Andrii Landiak 4 years ago
parent
commit
3d7097536c
  1. 8
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 21
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 110
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  4. 10
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  6. 3
      application/src/main/resources/thingsboard.yml
  7. 17
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  8. 3
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  9. 2
      application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java
  10. 2
      application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java
  11. 2
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  12. 2
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
  13. 2
      application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java
  14. 2
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  15. 2
      application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java
  16. 453
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  17. 35
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java
  18. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  19. 39
      common/data/src/main/java/org/thingsboard/server/common/data/UserEmailInfo.java
  20. 61
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java
  21. 4
      dao/pom.xml
  22. 43
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java
  23. 7
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  24. 66
      dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java
  25. 53
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java
  26. 25
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java
  27. 2
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  28. 36
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java
  29. 30
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java
  30. 24
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java
  31. 38
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java
  32. 156
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java
  33. 6
      dao/src/main/resources/sql/schema-entities.sql
  34. 15
      dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java
  35. 101
      dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java
  36. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/DisableUIListeners.java
  37. 2
      msa/tb/pom.xml
  38. 4
      pom.xml
  39. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java
  40. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java
  41. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java
  42. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
  43. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java
  44. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  45. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  46. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java
  47. 27
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java
  48. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java
  49. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  50. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  51. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java
  52. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  53. 67
      ui-ngx/.eslintrc.json
  54. 1
      ui-ngx/.gitignore
  55. 36
      ui-ngx/angular.json
  56. 17
      ui-ngx/extra-webpack.config.js
  57. 138
      ui-ngx/package.json
  58. 127
      ui-ngx/src/app/core/api/widget-subscription.ts
  59. 2
      ui-ngx/src/app/core/auth/auth.selectors.ts
  60. 4
      ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts
  61. 2
      ui-ngx/src/app/core/http/entity.service.ts
  62. 16
      ui-ngx/src/app/core/http/widget.service.ts
  63. 2
      ui-ngx/src/app/core/interceptors/load.selectors.ts
  64. 14
      ui-ngx/src/app/core/notification/notification.effects.ts
  65. 4
      ui-ngx/src/app/core/services/raf.service.ts
  66. 4
      ui-ngx/src/app/core/services/resources.service.ts
  67. 2
      ui-ngx/src/app/core/services/utils.service.ts
  68. 26
      ui-ngx/src/app/core/settings/settings.effects.ts
  69. 2
      ui-ngx/src/app/core/settings/settings.selectors.ts
  70. 4
      ui-ngx/src/app/core/translate/translate-default-parser.ts
  71. 4
      ui-ngx/src/app/core/utils.ts
  72. 4
      ui-ngx/src/app/modules/common/modules-map.ts
  73. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  74. 6
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts
  75. 14
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts
  76. 12
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts
  77. 24
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  78. 8
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts
  79. 8
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts
  80. 8
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts
  81. 8
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  82. 8
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts
  83. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  84. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts
  85. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  86. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss
  87. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts
  88. 14
      ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.ts
  89. 8
      ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts
  90. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts
  91. 8
      ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts
  92. 8
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  93. 12
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts
  94. 12
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts
  95. 12
      ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts
  96. 14
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  97. 6
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts
  98. 8
      ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts
  99. 4
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  100. 4
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

8
application/src/main/data/upgrade/3.4.3/schema_update.sql → application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -25,13 +25,17 @@ CREATE TABLE IF NOT EXISTS alarm_comment (
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id);
CREATE TABLE IF NOT EXISTS user_settings (
user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY,
settings varchar(100000),
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE
);
-- DEVICE PROFILE CERTIFICATE START
ALTER TABLE device_profile
ADD COLUMN IF NOT EXISTS certificate_hash varchar,
DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key,
DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key,
ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash);
-- DEVICE PROFILE CERTIFICATE END

21
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -93,6 +93,8 @@ import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
@ -128,6 +130,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserSettingsService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@ -159,6 +162,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID;
import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -189,6 +194,9 @@ public abstract class BaseController {
@Autowired
protected UserService userService;
@Autowired
protected UserSettingsService userSettingsService;
@Autowired
protected DeviceService deviceService;
@ -983,4 +991,17 @@ public abstract class BaseController {
}, MoreExecutors.directExecutor());
return deferredResult;
}
protected EntityDataSortOrder createEntityDataSortOrder(String sortProperty, String sortOrder) {
if (isNotEmpty(sortProperty)) {
EntityDataSortOrder entityDataSortOrder = new EntityDataSortOrder();
entityDataSortOrder.setKey(new EntityKey(ENTITY_FIELD, sortProperty));
if (isNotEmpty(sortOrder)) {
entityDataSortOrder.setDirection(EntityDataSortOrder.Direction.valueOf(sortOrder));
}
return entityDataSortOrder;
} else {
return null;
}
}
}

110
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -15,16 +15,21 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -32,8 +37,11 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserEmailInfo;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
@ -41,12 +49,19 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -56,6 +71,11 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD;
@ -84,6 +104,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
public class UserController extends BaseController {
public static final String USER_ID = "userId";
public static final String PATHS = "paths";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s";
@ -97,6 +118,9 @@ public class UserController extends BaseController {
private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService;
@Autowired
private EntityQueryService entityQueryService;
@ApiOperation(value = "Get User (getUserById)",
notes = "Fetch the User object based on the provided User Id. " +
"If the user has the authority of 'SYS_ADMIN', the server does not perform additional checks. " +
@ -295,6 +319,44 @@ public class UserController extends BaseController {
}
}
@ApiOperation(value = "Find users by query (findUsersByQuery)",
notes = "Returns page of user data objects. Search is been executed by email, firstName and " +
"lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users/info", method = RequestMethod.GET)
@ResponseBody
public PageData<UserEmailInfo> findUsersByQuery(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
EntityTypeFilter entityFilter = new EntityTypeFilter();
entityFilter.setEntityType(EntityType.USER);
EntityDataPageLink pageLink = new EntityDataPageLink(pageSize, page, textSearch, createEntityDataSortOrder(sortProperty, sortOrder));
List<EntityKey> entityFields = Arrays.asList(new EntityKey(ENTITY_FIELD, "firstName"),
new EntityKey(ENTITY_FIELD, "lastName"),
new EntityKey(ENTITY_FIELD, "email"));
EntityDataQuery query = new EntityDataQuery(entityFilter, pageLink, entityFields, null, null);
return entityQueryService.findEntityDataByQuery(securityUser, query).mapData(entityData ->
{
Map<String, TsValue> fieldValues = entityData.getLatest().get(ENTITY_FIELD);
return new UserEmailInfo(UserId.fromString(entityData.getEntityId().getId().toString()),
fieldValues.get("email").getValue(),
fieldValues.get("firstName").getValue(),
fieldValues.get("lastName").getValue());
});
}
@ApiOperation(value = "Get Tenant Users (getTenantAdmins)",
notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@ -378,4 +440,52 @@ public class UserController extends BaseController {
}
}
@ApiOperation(value = "Save user settings (saveUserSettings)",
notes = "Save user settings represented in json format for authorized user. " )
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/user/settings")
public JsonNode saveUserSettings(@RequestBody JsonNode settings) throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
UserSettings userSettings = new UserSettings();
userSettings.setSettings(settings);
userSettings.setUserId(currentUser.getId());
return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings();
}
@ApiOperation(value = "Update user settings (saveUserSettings)",
notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(value = "/user/settings")
public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), settings);
}
@ApiOperation(value = "Get user settings (getUserSettings)",
notes = "Fetch the User settings based on authorized user. " )
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/settings")
public JsonNode getUserSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId());
return userSettings == null ? JacksonUtil.newObjectNode(): userSettings.getSettings();
}
@ApiOperation(value = "Delete user settings (deleteUserSettings)",
notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter" )
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/settings/{paths}", method = RequestMethod.DELETE)
public void deleteUserSettings(@ApiParam(value = PATHS)
@PathVariable(PATHS) String paths) throws ThingsboardException {
checkParameter(USER_ID, paths);
SecurityUser currentUser = getCurrentUser();
userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), Arrays.asList(paths.split(",")));
}
}

10
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -233,15 +233,17 @@ public class ThingsboardInstallService {
log.info("Upgrading ThingsBoard from version 3.4.1 to 3.4.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.1");
dataUpdateService.updateData("3.4.1");
case "3.4.2":
log.info("Upgrading ThingsBoard from version 3.4.2 to 3.4.3 ...");
case "3.4.3":
log.info("Upgrading ThingsBoard from version 3.4.3 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.3");
log.info("Upgrading ThingsBoard from version 3.4.3 to 3.4.4 ...");
case "3.4.4":
log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
break;
//TODO update CacheCleanupService on the next version upgrade
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);

4
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -677,11 +677,11 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.error("Failed updating schema!!!", e);
}
break;
case "3.4.3":
case "3.4.4":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
if (isOldSchema(conn, 3004002)) {
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.3", SCHEMA_UPDATE_SQL);
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.4", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
try {

3
application/src/main/resources/thingsboard.yml

@ -457,6 +457,9 @@ cache:
versionControlTask:
timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:5}"
maxSize: "${CACHE_SPECS_VERSION_CONTROL_TASK_MAX_SIZE:100000}"
userSettings:
timeToLiveInMinutes: "${CACHE_SPECS_USER_SETTINGS_TTL:1440}"
maxSize: "${CACHE_SPECS_USER_SETTINGS_MAX_SIZE:100000}"
#Disable this because it is not required.
spring.data.redis.repositories.enabled: false

17
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -103,6 +103,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
@ -613,6 +614,22 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);
}
protected <T> T doPut(String urlTemplate, T content, Class<T> responseClass, String... params) {
try {
return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected <T> ResultActions doPut(String urlTemplate, T content, String... params) throws Exception {
MockHttpServletRequestBuilder postRequest = put(urlTemplate, params);
setJwtToken(postRequest);
String json = json(content);
postRequest.contentType(contentType).content(json);
return mockMvc.perform(postRequest);
}
protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
}

3
application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -441,7 +442,6 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue("Created alarm doesn't match the found one!", equals);
}
@Test
public void testDeleteAlarmWithDeleteRelationsOk() throws Exception {
loginCustomerUser();
@ -449,6 +449,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsOk(customerDevice.getId(), alarmId, "/api/alarm/" + alarmId);
}
@Ignore
@Test
public void testDeleteAlarmExceptionWithRelationsTransactional() throws Exception {
loginCustomerUser();

2
application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -980,6 +981,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsOk(savedTenant.getId(), assetId, "/api/asset/" + assetId);
}
@Ignore
@Test
public void testDeleteAssetExceptionWithRelationsTransactional() throws Exception {
AssetId assetId = createAsset("Asset for Test WithRelations Transactional Exception").getId();

2
application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java

@ -23,6 +23,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -428,6 +429,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest
testEntityDaoWithRelationsOk(savedTenant.getId(), customerId, "/api/customer/" + customerId);
}
@Ignore
@Test
public void testDeleteCustomerExceptionWithRelationsTransactional() throws Exception {
CustomerId customerId = createCustomer("Customer for Test WithRelations Transactional Exception").getId();

2
application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java

@ -24,6 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -1324,6 +1325,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsOk(savedTenant.getId(), deviceId, "/api/device/" + deviceId);
}
@Ignore
@Test
public void testDeleteDeviceExceptionWithRelationsTransactional() throws Exception {
DeviceId deviceId = createDevice("Device for Test WithRelations Transactional Exception").getId();

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

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -1009,6 +1010,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
testEntityDaoWithRelationsOk(savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId);
}
@Ignore
@Test
public void testDeleteDeviceProfileExceptionWithRelationsTransactional() throws Exception {
DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId();

2
application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java

@ -24,6 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -931,6 +932,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsOk(savedTenant.getId(), edgeId, "/api/edge/" + edgeId);
}
@Ignore
@Test
public void testDeleteEdgeExceptionWithRelationsTransactional() throws Exception {
EdgeId edgeId = savedEdge("Edge for Test WithRelations Transactional Exception").getId();

2
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -30,6 +30,7 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -845,6 +846,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
testEntityDaoWithRelationsOk(tenantId, entityViewId, "/api/entityView/" + entityViewId);
}
@Ignore
@Test
public void testDeleteEntityViewExceptionWithRelationsTransactional() throws Exception {
EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId();

2
application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -253,6 +254,7 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest
testEntityDaoWithRelationsOk(savedTenant.getId(), ruleChainId, "/api/ruleChain/" + ruleChainId);
}
@Ignore
@Test
public void testDeleteRuleChainExceptionWithRelationsTransactional() throws Exception {
RuleChainId ruleChainId = createRuleChain("RuleChain for Test WithRelations Transactional Exception").getId();

453
application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserEmailInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -47,6 +49,7 @@ import org.thingsboard.server.service.mail.TestMailService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -60,6 +63,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SYSTEM_TENANT;
public abstract class BaseUserControllerTest extends AbstractControllerTest {
private IdComparator<User> idComparator = new IdComparator<>();
private IdComparator<UserEmailInfo> userDataIdComparator = new IdComparator<>();
private CustomerId customerNUULId = (CustomerId) createEntityId_NULL_UUID(new Customer());
@ -83,21 +87,15 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testSaveUser() throws Exception {
loginSysAdmin();
String email = "tenant2@thingsboard.org";
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
User user = createTenantAdminUser();
String email = user.getEmail();
Mockito.reset(tbClusterService, auditLogService);
User savedUser = doPost("/api/user", user, User.class);
Assert.assertNotNull(savedUser);
Assert.assertNotNull(savedUser.getId());
Assert.assertTrue(savedUser.getCreatedTime() > 0);
Assert.assertEquals(user.getEmail(), savedUser.getEmail());
Assert.assertEquals(email, savedUser.getEmail());
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
Assert.assertEquals(foundUser, savedUser);
@ -152,13 +150,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService);
String email = "tenant2@thingsboard.org";
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail(email);
user.setFirstName(StringUtils.randomAlphabetic(300));
user.setLastName("Downs");
User user = createTenantAdminUser(StringUtils.randomAlphabetic(300), "Brown");
String msgError = msgErrorFieldLength("first name");
doPost("/api/user", user)
.andExpect(status().isBadRequest())
@ -185,12 +177,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testUpdateUserFromDifferentTenant() throws Exception {
loginSysAdmin();
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(tenantId);
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
User tenantAdmin = createTenantAdminUser();
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
loginDifferentTenant();
@ -210,14 +197,8 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testResetPassword() throws Exception {
loginSysAdmin();
String email = "tenant2@thingsboard.org";
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
User user = createTenantAdminUser();
String email = user.getEmail();
User savedUser = createUserAndLogin(user, "testPassword1");
resetTokens();
@ -262,13 +243,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testFindUserById() throws Exception {
loginSysAdmin();
String email = "tenant2@thingsboard.org";
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
User user = createTenantAdminUser();
User savedUser = doPost("/api/user", user, User.class);
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
@ -282,15 +257,12 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService);
String email = TENANT_ADMIN_EMAIL;
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
user.setEmail(TENANT_ADMIN_EMAIL);
String msgError = "User with email '" + email + "' already present in database";
String msgError = "User with email '" + TENANT_ADMIN_EMAIL + "' already present in database";
doPost("/api/user", user)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
@ -307,12 +279,8 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService);
String email = "tenant_thingsboard.org";
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
User user = createTenantAdminUser();
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
String msgError = "Invalid email address format '" + email + "'";
doPost("/api/user", user)
@ -373,13 +341,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testDeleteUser() throws Exception {
loginSysAdmin();
String email = "tenant2@thingsboard.org";
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
User user = createTenantAdminUser();
User savedUser = doPost("/api/user", user, User.class);
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
@ -561,20 +523,10 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testFindCustomerUsers() throws Exception {
loginSysAdmin();
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(tenantId);
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
User tenantAdmin = createTenantAdminUser();
createUserAndLogin(tenantAdmin, "testPassword1");
Customer customer = new Customer();
customer.setTitle("My customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
CustomerId customerId = savedCustomer.getId();
CustomerId customerId = postCustomer();
List<User> customerUsers = new ArrayList<>();
for (int i = 0; i < 56; i++) {
@ -611,47 +563,22 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
public void testFindCustomerUsersByEmail() throws Exception {
loginSysAdmin();
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(tenantId);
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
User tenantAdmin = createTenantAdminUser();
createUserAndLogin(tenantAdmin, "testPassword1");
Customer customer = new Customer();
customer.setTitle("My customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
CustomerId customerId = savedCustomer.getId();
CustomerId customerId = postCustomer();
String email1 = "testEmail1";
List<User> customerUsersEmail1 = new ArrayList<>();
for (int i = 0; i < 74; i++) {
User user = new User();
user.setAuthority(Authority.CUSTOMER_USER);
user.setCustomerId(customerId);
String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10));
String email = email1 + suffix + "@thingsboard.org";
email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
user.setEmail(email);
customerUsersEmail1.add(doPost("/api/user", user, User.class));
}
String email2 = "testEmail2";
List<User> customerUsersEmail2 = new ArrayList<>();
for (int i = 0; i < 92; i++) {
User user = new User();
user.setAuthority(Authority.CUSTOMER_USER);
user.setCustomerId(customerId);
String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10));
String email = email2 + suffix + "@thingsboard.org";
email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
user.setEmail(email);
customerUsersEmail2.add(doPost("/api/user", user, User.class));
List<User> customerUsersEmail1 = new ArrayList<>();
List<User> customerUsersEmail2= new ArrayList<>();
for (int i = 0; i < 45; i++) {
User customerUser = createCustomerUser( customerId);
customerUser.setEmail(email1 + StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)) + "@thingsboard.org");
customerUsersEmail1.add(doPost("/api/user", customerUser, User.class));
customerUser.setEmail(email2 + StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)) + "@thingsboard.org");
customerUsersEmail2.add(doPost("/api/user", customerUser, User.class));
}
List<User> loadedCustomerUsersEmail1 = new ArrayList<>();
@ -717,16 +644,20 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
.andExpect(status().isOk());
}
@Test
public void testDeleteUserWithDeleteRelationsOk() throws Exception {
UserId userId = createUser().getId();
loginSysAdmin();
User tenantAdminUser = createTenantAdminUser();
UserId userId = doPost("/api/user", tenantAdminUser, User.class).getId();
testEntityDaoWithRelationsOk(tenantId, userId, "/api/user/" + userId);
}
@Ignore
@Test
public void testDeleteUserExceptionWithRelationsTransactional() throws Exception {
UserId userId = createUser().getId();
loginSysAdmin();
User tenantAdminUser = createTenantAdminUser("Joe", "Downs");
UserId userId = doPost("/api/user", tenantAdminUser, User.class).getId();
testEntityDaoWithRelationsTransactionalException(userDao, tenantId, userId, "/api/user/" + userId);
}
@ -741,15 +672,313 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
assertThat(getErrorMessage(result)).containsIgnoringCase("invalid sort property");
}
private User createUser() throws Exception {
@Test
public void testSaveUserSettings() throws Exception {
loginCustomerUser();
JsonNode userSettings = mapper.readTree("{\"A\":5, \"B\":10, \"E\":18}");
JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class);
Assert.assertEquals(userSettings, savedSettings);
JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class);
Assert.assertEquals(retrievedSettings, userSettings);
}
@Test
public void testShouldNotSaveJsonWithRestrictedSymbols() throws Exception {
loginCustomerUser();
JsonNode userSettings = mapper.readTree("{\"A.B\":5, \"E\":18}");
doPost("/api/user/settings", userSettings).andExpect(status().isBadRequest());
userSettings = mapper.readTree("{\"A,B\":5, \"E\":18}");
doPost("/api/user/settings", userSettings).andExpect(status().isBadRequest());
}
@Test
public void testUpdateUserSettings() throws Exception {
loginCustomerUser();
JsonNode userSettings = mapper.readTree("{\"A\":5, \"B\":{\"C\":true, \"D\":\"stringValue\"}}");
JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class);
Assert.assertEquals(userSettings, savedSettings);
JsonNode newSettings = mapper.readTree("{\"A\":10}");
doPut("/api/user/settings", newSettings);
JsonNode updatedSettings = doGet("/api/user/settings", JsonNode.class);
JsonNode expectedSettings = mapper.readTree("{\"A\":10, \"B\":{\"C\":true, \"D\":\"stringValue\"}}");
Assert.assertEquals(expectedSettings, updatedSettings);
JsonNode patchedSettings = mapper.readTree("{\"A\":11, \"B\":{\"C\":false, \"D\":\"stringValue2\"}}");
doPut("/api/user/settings", patchedSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":11, \"B\":{\"C\":false, \"D\":\"stringValue2\"}}");
Assert.assertEquals(expectedSettings, updatedSettings);
patchedSettings = mapper.readTree("{\"B.D\": \"stringValue3\"}");
doPut("/api/user/settings", patchedSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":11, \"B\":{\"C\":false, \"D\": \"stringValue3\"}}");
Assert.assertEquals(expectedSettings, updatedSettings);
patchedSettings = mapper.readTree("{\"B.D\": {\"E\": 76, \"F\": 92}}");
doPut("/api/user/settings", patchedSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":11, \"B\":{\"C\":false, \"D\": {\"E\":76, \"F\": 92}}}");
Assert.assertEquals(expectedSettings, updatedSettings);
patchedSettings = mapper.readTree("{\"B.D.E\": 100}");
doPut("/api/user/settings", patchedSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":11, \"B\":{\"C\":false, \"D\": {\"E\":100, \"F\": 92}}}");
Assert.assertEquals(expectedSettings, updatedSettings);
}
@Test
public void testShouldCreatePathIfNotExists() throws Exception {
loginCustomerUser();
JsonNode userSettings = mapper.readTree("{\"A\":5}");
JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class);
Assert.assertEquals(userSettings, savedSettings);
JsonNode newSettings = mapper.readTree("{\"B\":{\"C\": 10}}");
doPut("/api/user/settings", newSettings);
JsonNode updatedSettings = doGet("/api/user/settings", JsonNode.class);
JsonNode expectedSettings = mapper.readTree("{\"A\":5, \"B\":{\"C\": 10}}");
Assert.assertEquals(expectedSettings, updatedSettings);
newSettings = mapper.readTree("{\"B.K\":true}");
doPut("/api/user/settings", newSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":5, \"B\":{\"C\": 10, \"K\": true}}");
Assert.assertEquals(expectedSettings, updatedSettings);
newSettings = mapper.readTree("{\"B\":{}}");
doPut("/api/user/settings", newSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":5, \"B\":{}}");
Assert.assertEquals(expectedSettings, updatedSettings);
newSettings = mapper.readTree("{\"F.G\":\"string\"}");
doPut("/api/user/settings", newSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":5, \"B\":{}, \"F\":{\"G\": \"string\"}}");
Assert.assertEquals(expectedSettings, updatedSettings);
newSettings = mapper.readTree("{\"F\":{\"G\":\"string2\"}}");
doPut("/api/user/settings", newSettings);
updatedSettings = doGet("/api/user/settings", JsonNode.class);
expectedSettings = mapper.readTree("{\"A\":5, \"B\":{}, \"F\":{\"G\": \"string2\"}}");
Assert.assertEquals(expectedSettings, updatedSettings);
}
@Test
public void testDeleteUserSettings() throws Exception {
loginCustomerUser();
JsonNode userSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}}");
JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class);
Assert.assertEquals(userSettings, savedSettings);
doDelete("/api/user/settings/C.D,B");
JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class);
JsonNode expectedSettings = mapper.readTree("{\"A\":10, \"C\":{}}");
Assert.assertEquals(expectedSettings, retrievedSettings);
}
@Test
public void checkCustomerUserDoNotSeeTenantUsersOtherTenantUsersOtherCustomerUsers() throws Exception {
loginSysAdmin();
String searchText = "Joe";
loginDifferentTenant();
CustomerId customerId1 = postCustomer();
doPost("/api/user", createCustomerUser(searchText, "Ress", customerId1), User.class);
loginSysAdmin();
User tenantAdmin = createTenantAdminUser(searchText, "Brown");
createUserAndLogin(tenantAdmin, "testPassword1");
CustomerId customerId2 = postCustomer();
User user = createCustomerUser(searchText, "Downs", customerId2);
doPost("/api/user", user, User.class);
CustomerId customerId3 = postCustomer();
User user2 = createCustomerUser(customerId3);
createUserAndLogin(user2, "testPassword2");
PageLink pageLink = new PageLink(10, 0, searchText);
List<UserEmailInfo> usersInfo = getUsersInfo(pageLink);
Assert.assertEquals(usersInfo.size(), 0);
//clear users
loginDifferentTenant();
doDelete("/api/customer/" + customerId1.getId().toString())
.andExpect(status().isOk());
loginUser(tenantAdmin.getEmail(), "testPassword1");
doDelete("/api/customer/" + customerId2.getId().toString())
.andExpect(status().isOk());
doDelete("/api/customer/" + customerId3.getId().toString())
.andExpect(status().isOk());
}
@Test
public void shouldFindCustomerUsersBySearchText() throws Exception {
loginSysAdmin();
User tenantAdmin = createTenantAdminUser();
createUserAndLogin(tenantAdmin, "testPassword1");
String searchText = "Philip";
CustomerId customerId = postCustomer();
CustomerId customerId2 = postCustomer();
List<User> customerUsersContainingWord = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String suffix = StringUtils.randomAlphabetic((int) (5 + Math.random() * 10));
customerUsersContainingWord.add(doPost("/api/user", createCustomerUser(searchText + i, "Last" + i, customerId), User.class));
customerUsersContainingWord.add(doPost("/api/user", createCustomerUser(null, null, searchText + suffix + "@thingsboard.org", customerId), User.class));
doPost("/api/user", createCustomerUser(null, null, customerId), User.class);
suffix = StringUtils.randomAlphabetic((int) (5 + Math.random() * 10));
doPost("/api/user", createCustomerUser(searchText + i, "Last" + i, customerId2), User.class);
doPost("/api/user", createCustomerUser(null, null, searchText + suffix + "@thingsboard.org", customerId2), User.class);
}
createUserAndLogin(createCustomerUser(customerId), "testPassword2");
// find users by search text
PageLink pageLink = new PageLink(10, 0, searchText);
List<UserEmailInfo> usersInfo = getUsersInfo(pageLink);
List<UserEmailInfo> expectedUserInfos = customerUsersContainingWord.stream().map(customerUser -> new UserEmailInfo(customerUser.getId(),
customerUser.getEmail(), customerUser.getFirstName() == null ? "" : customerUser.getFirstName(),
customerUser.getLastName() == null ? "" : customerUser.getLastName()))
.sorted(userDataIdComparator).collect(Collectors.toList());
usersInfo.sort(userDataIdComparator);
Assert.assertEquals(expectedUserInfos, usersInfo);
// find user by full name
pageLink = new PageLink(10, 0, searchText + "5");
usersInfo = getUsersInfo(pageLink);
Assert.assertEquals(1, usersInfo.size());
//clear users
loginUser(tenantAdmin.getEmail(), "testPassword1");
doDelete("/api/customer/" + customerId.getId().toString())
.andExpect(status().isOk());
doDelete("/api/customer/" + customerId2.getId().toString())
.andExpect(status().isOk());
}
@Test
public void shouldFindTenantUsersBySearchText() throws Exception {
loginSysAdmin();
String email = "tenant2@thingsboard.org";
User tenantAdmin = createTenantAdminUser();
createUserAndLogin(tenantAdmin, "testPassword1");
CustomerId customerId = postCustomer();
CustomerId customerId2 = postCustomer();
String searchText = "Brown";
List<User> usersContainingWord = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String suffix = StringUtils.randomAlphabetic((int) (5 + Math.random() * 10));
usersContainingWord.add(doPost("/api/user", createCustomerUser("First" + i, searchText + i, customerId), User.class));
usersContainingWord.add(doPost("/api/user", createCustomerUser(null, null, searchText + suffix + "@thingsboard.org", customerId), User.class));
doPost("/api/user", createCustomerUser(null, null, customerId), User.class);
suffix = StringUtils.randomAlphabetic((int) (5 + Math.random() * 10));
usersContainingWord.add(doPost("/api/user", createCustomerUser("First" + i, searchText + i, customerId2), User.class));
usersContainingWord.add(doPost("/api/user", createCustomerUser(null, null, searchText + suffix + "@thingsboard.org", customerId2), User.class));
}
loginDifferentTenant();
CustomerId customerId3 = postCustomer();
doPost("/api/user", createCustomerUser("Jane", searchText, customerId3), User.class);
// find users by search text
loginUser(tenantAdmin.getEmail(), "testPassword1");
PageLink pageLink = new PageLink(10, 0, searchText);
List<UserEmailInfo> usersInfo = getUsersInfo(pageLink);
List<UserEmailInfo> expectedUserInfos = usersContainingWord.stream().map(customerUser -> new UserEmailInfo(customerUser.getId(),
customerUser.getEmail(), customerUser.getFirstName() == null ? "" : customerUser.getFirstName(),
customerUser.getLastName() == null ? "" : customerUser.getLastName()))
.sorted(userDataIdComparator).collect(Collectors.toList());
usersInfo.sort(userDataIdComparator);
Assert.assertEquals(expectedUserInfos, usersInfo);
// find user by full last name
pageLink = new PageLink(10, 0, searchText + "3");
usersInfo = getUsersInfo(pageLink);
Assert.assertEquals(2, usersInfo.size());
//clear users
doDelete("/api/customer/" + customerId.getId().toString())
.andExpect(status().isOk());
doDelete("/api/customer/" + customerId2.getId().toString())
.andExpect(status().isOk());
}
private CustomerId postCustomer() {
Customer customer = new Customer();
customer.setTitle(StringUtils.randomAlphabetic(9));
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
return savedCustomer.getId();
}
private static User createCustomerUser(CustomerId customerId) {
return createCustomerUser(null, null, customerId);
}
private static User createCustomerUser(String firstName, String lastName, CustomerId customerId) {
String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10));
return createCustomerUser(firstName, lastName, "testMail" + suffix + "@thingsboard.org", customerId);
}
private static User createCustomerUser(String firstName, String lastName, String email, CustomerId customerId) {
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setAuthority(Authority.CUSTOMER_USER);
user.setFirstName(firstName);
user.setLastName(lastName);
user.setCustomerId(customerId);
user.setEmail(email);
user.setFirstName("Joe");
user.setLastName("Downs");
return doPost("/api/user", user, User.class);
return user;
}
private User createTenantAdminUser() {
return createTenantAdminUser(null, null);
}
private User createTenantAdminUser(String firstName, String lastName) {
String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10));
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(tenantId);
tenantAdmin.setEmail("testEmail" + suffix + "@thingsbord.org");
tenantAdmin.setFirstName(firstName);
tenantAdmin.setLastName(lastName);
return tenantAdmin;
}
private List<UserEmailInfo> getUsersInfo(PageLink pageLink) throws Exception {
List<UserEmailInfo> loadedCustomerUsers = new ArrayList<>();
PageData<UserEmailInfo> pageData = null;
do {
pageData = doGetTypedWithPageLink("/api/users/info?", new TypeReference<>() {}, pageLink);
loadedCustomerUsers.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
} while (pageData.hasNext());
return loadedCustomerUsers;
}
}

35
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java

@ -0,0 +1,35 @@
/**
* 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.
*/
package org.thingsboard.server.dao.user;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
import java.util.List;
public interface UserSettingsService {
void updateUserSettings(TenantId tenantId, UserId userId, JsonNode settings);
UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings);
UserSettings findUserSettings(TenantId tenantId, UserId userId);
void deleteUserSettings(TenantId tenantId, UserId userId, List<String> jsonPaths);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -39,4 +39,5 @@ public class CacheConstants {
public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings";
public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes";
public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask";
public static final String USER_SETTINGS_CACHE = "userSettings";
}

39
common/data/src/main/java/org/thingsboard/server/common/data/UserEmailInfo.java

@ -0,0 +1,39 @@
/**
* 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.
*/
package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.UserId;
@ApiModel
@Data
@AllArgsConstructor
public class UserEmailInfo implements HasId<UserId> {
@ApiModelProperty(position = 1, value = "User id")
private UserId id;
@ApiModelProperty(position = 2, value = "User email", example = "john@gmail.com")
private String email;
@ApiModelProperty(position = 3, value = "User first name", example = "John")
private String firstName;
@ApiModelProperty(position = 4, value = "User last name", example = "Brown")
private String lastName;
}

61
common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java

@ -0,0 +1,61 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson;
import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson;
@ApiModel
@Data
public class UserSettings implements Serializable {
private static final long serialVersionUID = 2628320657987010348L;
@ApiModelProperty(position = 1, value = "JSON object with User id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private UserId userId;
@ApiModelProperty(position = 2, value = "JSON object with user settings.", dataType = "com.fasterxml.jackson.databind.JsonNode")
@NoXss
@Length(fieldName = "settings", max = 100000)
private transient JsonNode settings;
@JsonIgnore
private byte[] settingsBytes;
public JsonNode getSettings() {
return getJson(() -> settings, () -> settingsBytes);
}
public void setSettings(JsonNode settings) {
setJson(settings, json -> this.settings = json, bytes -> this.settingsBytes = bytes);
}
}

4
dao/pom.xml

@ -233,6 +233,10 @@
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-core</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

43
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java

@ -0,0 +1,43 @@
/**
* 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.
*/
package org.thingsboard.server.dao.entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.thingsboard.server.cache.TbTransactionalCache;
import java.io.Serializable;
public abstract class AbstractCachedService<K extends Serializable, V extends Serializable, E> {
@Autowired
protected TbTransactionalCache<K, V> cache;
@Autowired
private ApplicationEventPublisher eventPublisher;
protected void publishEvictEvent(E event) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
eventPublisher.publishEvent(event);
} else {
handleEvictEvent(event);
}
}
public abstract void handleEvictEvent(E event);
}

7
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -86,6 +86,13 @@ public class ModelConstants {
public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token";
public static final String USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_reset_token";
/**
* User settings constants.
*/
public static final String USER_SETTINGS_COLUMN_FAMILY_NAME = "user_settings";
public static final String USER_SETTINGS_USER_ID_PROPERTY = USER_ID_PROPERTY;
public static final String USER_SETTINGS_SETTINGS = "settings";
/**
* Cassandra admin_settings constants.
*/

66
dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java

@ -0,0 +1,66 @@
/**
* 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.
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.ToData;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.UUID;
@Data
@NoArgsConstructor
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Entity
@Table(name = ModelConstants.USER_SETTINGS_COLUMN_FAMILY_NAME)
public class UserSettingsEntity implements ToData<UserSettings> {
@Id
@Column(name = ModelConstants.USER_SETTINGS_USER_ID_PROPERTY)
private UUID userId;
@Type(type = "json")
@Column(name = ModelConstants.USER_SETTINGS_SETTINGS)
private JsonNode settings;
public UserSettingsEntity(UserSettings userSettings) {
this.userId = userSettings.getUserId().getId();
if (userSettings.getSettings() != null) {
this.settings= userSettings.getSettings();
}
}
@Override
public UserSettings toData() {
UserSettings userSettings = new UserSettings();
userSettings.setUserId(new UserId(userId));
if (settings != null) {
userSettings.setSettings(settings);
}
return userSettings;
}
}

53
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java

@ -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.
*/
package org.thingsboard.server.dao.sql.user;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.UserSettingsEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
import org.thingsboard.server.dao.user.UserSettingsDao;
import org.thingsboard.server.dao.util.SqlDao;
@Slf4j
@Component
@SqlDao
public class JpaUserSettingsDao extends JpaAbstractDaoListeningExecutorService implements UserSettingsDao {
@Autowired
private UserSettingsRepository userSettingsRepository;
@Override
public UserSettings save(TenantId tenantId, UserSettings userSettings) {
return DaoUtil.getData(userSettingsRepository.save(new UserSettingsEntity(userSettings)));
}
@Override
public UserSettings findById(TenantId tenantId, UserId userId) {
return DaoUtil.getData(userSettingsRepository.findById(userId.getId()));
}
@Override
public void removeById(TenantId tenantId, UserId userId) {
userSettingsRepository.deleteById(userId.getId());
}
}

25
dao/src/main/java/org/thingsboard/server/dao/sql/user/UserSettingsRepository.java

@ -0,0 +1,25 @@
/**
* 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.
*/
package org.thingsboard.server.dao.sql.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.UserSettingsEntity;
import java.util.UUID;
public interface UserSettingsRepository extends JpaRepository<UserSettingsEntity, UUID> {
}

2
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -76,6 +77,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final UserDao userDao;
private final UserCredentialsDao userCredentialsDao;
private final UserAuthSettingsDao userAuthSettingsDao;
private final UserSettingsDao userSettingsDao;
private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;

36
dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java

@ -0,0 +1,36 @@
/**
* 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.
*/
package org.thingsboard.server.dao.user;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.asset.AssetCacheKey;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("UserSettingsCache")
public class UserSettingsCaffeineCache extends CaffeineTbTransactionalCache<UserId, UserSettings> {
public UserSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USER_SETTINGS_CACHE);
}
}

30
dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java

@ -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.
*/
package org.thingsboard.server.dao.user;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
public interface UserSettingsDao {
UserSettings save(TenantId tenantId, UserSettings userSettings);
UserSettings findById(TenantId tenantId, UserId userId);
void removeById(TenantId tenantId, UserId userId);
}

24
dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java

@ -0,0 +1,24 @@
/**
* 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.
*/
package org.thingsboard.server.dao.user;
import lombok.Data;
import org.thingsboard.server.common.data.id.UserId;
@Data
public class UserSettingsEvictEvent {
private final UserId userId;
}

38
dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java

@ -0,0 +1,38 @@
/**
* 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.
*/
package org.thingsboard.server.dao.user;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbFSTRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.asset.AssetCacheKey;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("UserSettingsCache")
public class UserSettingsRedisCache extends RedisTbTransactionalCache<UserId, UserSettings> {
public UserSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USER_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

156
dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java

@ -0,0 +1,156 @@
/**
* 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.
*/
package org.thingsboard.server.dao.user;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.fge.jackson.NodeType;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.entity.AbstractCachedService;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("UserSettingsDaoService")
@Slf4j
@RequiredArgsConstructor
public class UserSettingsServiceImpl extends AbstractCachedService<UserId, UserSettings, UserSettingsEvictEvent> implements UserSettingsService {
public static final String INCORRECT_USER_ID = "Incorrect userId ";
private final UserSettingsDao userSettingsDao;
@Override
public UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings) {
log.trace("Executing saveUserSettings for user [{}], [{}]", userSettings.getUserId(), userSettings);
validateId(userSettings.getUserId(), INCORRECT_USER_ID + userSettings.getUserId());
return doSaveUserSettings(tenantId, userSettings);
}
@Override
public void updateUserSettings(TenantId tenantId, UserId userId, JsonNode settings) {
log.trace("Executing updateUserSettings for user [{}], [{}]", userId, settings);
validateId(userId, INCORRECT_USER_ID + userId);
UserSettings oldSettings = userSettingsDao.findById(tenantId, userId);
JsonNode oldSettingsJson = oldSettings != null ? oldSettings.getSettings() : JacksonUtil.newObjectNode();
UserSettings newUserSettings = new UserSettings();
newUserSettings.setUserId(userId);
newUserSettings.setSettings(update(oldSettingsJson, settings));
doSaveUserSettings(tenantId, newUserSettings);
}
@Override
public UserSettings findUserSettings(TenantId tenantId, UserId userId) {
log.trace("Executing findUserSettings for user [{}]", userId);
validateId(userId, INCORRECT_USER_ID + userId);
return cache.getAndPutInTransaction(userId,
() -> userSettingsDao.findById(tenantId, userId), true);
}
@Override
public void deleteUserSettings(TenantId tenantId, UserId userId, List<String> jsonPaths) {
log.trace("Executing deleteUserSettings for user [{}]", userId);
validateId(userId, INCORRECT_USER_ID + userId);
UserSettings userSettings = userSettingsDao.findById(tenantId, userId);
if (userSettings == null) {
return;
}
try {
DocumentContext dcSettings = JsonPath.parse(userSettings.getSettings().toString());
for (String s : jsonPaths) {
dcSettings = dcSettings.delete("$." + s);
}
userSettings.setSettings(new ObjectMapper().readValue(dcSettings.jsonString(), ObjectNode.class));
} catch (Exception t) {
handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId()));
throw new RuntimeException(t);
}
doSaveUserSettings(tenantId, userSettings);
}
private UserSettings doSaveUserSettings(TenantId tenantId, UserSettings userSettings) {
try {
validateJsonKeys(userSettings.getSettings());
UserSettings saved = userSettingsDao.save(tenantId, userSettings);
publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId()));
return saved;
} catch (Exception t) {
handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId()));
throw t;
}
}
@TransactionalEventListener(classes = UserSettingsEvictEvent.class)
@Override
public void handleEvictEvent(UserSettingsEvictEvent event) {
List<UserId> keys = new ArrayList<>();
keys.add(event.getUserId());
cache.evict(keys);
}
private void validateJsonKeys(JsonNode userSettings) {
Iterator<String> fieldNames = userSettings.fieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
if (fieldName.contains(".") || fieldName.contains(",")) {
throw new DataValidationException("Json field name should not contain \".\" or \",\" symbols");
}
}
}
public JsonNode update(JsonNode mainNode, JsonNode updateNode) {
Iterator<String> fieldNames = updateNode.fieldNames();
while (fieldNames.hasNext()) {
String fieldExpression = fieldNames.next();
String[] fieldPath = fieldExpression.trim().split("\\.");
var node = (ObjectNode) mainNode;
for (int i = 0; i < fieldPath.length; i++) {
var fieldName = fieldPath[i];
var last = i == (fieldPath.length - 1);
if (last) {
node.set(fieldName, updateNode.get(fieldExpression));
} else {
if (!node.has(fieldName)) {
node.set(fieldName, JacksonUtil.newObjectNode());
}
node = (ObjectNode) node.get(fieldName);
}
}
}
return mainNode;
}
}

6
dao/src/main/resources/sql/schema-entities.sql

@ -792,3 +792,9 @@ CREATE TABLE IF NOT EXISTS user_auth_settings (
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id),
two_fa_settings varchar
);
CREATE TABLE IF NOT EXISTS user_settings (
user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY,
settings varchar(10000),
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE
);

15
dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java

@ -19,16 +19,19 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
@ -40,6 +43,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest {
private IdComparator<User> idComparator = new IdComparator<>();
private TenantId tenantId;
private UserSettings userSettings;
@Before
public void before() {
@ -65,7 +69,9 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest {
customerUser.setTenantId(tenantId);
customerUser.setCustomerId(savedCustomer.getId());
customerUser.setEmail("customer@thingsboard.org");
userService.saveUser(customerUser);
customerUser = userService.saveUser(customerUser);
userSettings = createUserSettings(customerUser.getId());
}
@After
@ -474,4 +480,11 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest {
tenantService.deleteTenant(tenantId);
}
private UserSettings createUserSettings(UserId userId) {
UserSettings userSettings = new UserSettings();
userSettings.setUserId(userId);
userSettings.setSettings(JacksonUtil.newObjectNode().put("text", StringUtils.randomAlphanumeric(10)));
return userSettings;
}
}

101
dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java

@ -0,0 +1,101 @@
/**
* 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.
*/
package org.thingsboard.server.dao.sql.user;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.user.UserDao;
import org.thingsboard.server.dao.user.UserSettingsDao;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.dao.service.AbstractServiceTest.SYSTEM_TENANT_ID;
public class JpaUserSettingsDaoTest extends AbstractJpaDaoTest {
private UUID tenantId;
private User user;
@Autowired
private UserSettingsDao userSettingsDao;
@Autowired
private UserDao userDao;
@Before
public void setUp() {
tenantId = Uuids.timeBased();
user = saveUser(tenantId, Uuids.timeBased());
}
@After
public void tearDown() {
userDao.removeById(user.getTenantId(), user.getUuidId());
}
@Test
public void testFindSettingsByUserId() {
UserSettings userSettings = createUserSettings(user.getId());
UserSettings retrievedUserSettings = userSettingsDao.findById(SYSTEM_TENANT_ID, user.getId());
assertEquals(retrievedUserSettings.getSettings(), userSettings.getSettings());
userSettingsDao.removeById(SYSTEM_TENANT_ID, user.getId());
UserSettings retrievedUserSettings2 = userSettingsDao.findById(SYSTEM_TENANT_ID, user.getId());
assertNull(retrievedUserSettings2);
}
private UserSettings createUserSettings(UserId userId) {
UserSettings userSettings = new UserSettings();
userSettings.setSettings(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10)));
userSettings.setUserId(userId);
return userSettingsDao.save(SYSTEM_TENANT_ID, userSettings);
}
private User saveUser(UUID tenantId, UUID customerId) {
User user = new User();
UUID id = Uuids.timeBased();
user.setId(new UserId(id));
user.setTenantId(TenantId.fromUUID(tenantId));
user.setCustomerId(new CustomerId(customerId));
if (customerId == NULL_UUID) {
user.setAuthority(Authority.TENANT_ADMIN);
} else {
user.setAuthority(Authority.CUSTOMER_USER);
}
String idString = id.toString();
String email = idString.substring(0, idString.indexOf('-')) + "@thingsboard.org";
user.setEmail(email);
return userDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, user);
}
}

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/DisableUIListeners.java

@ -16,10 +16,12 @@
package org.thingsboard.server.msa;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface DisableUIListeners {

2
msa/tb/pom.xml

@ -38,7 +38,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.4.3</pkg.upgradeVersion>
<pkg.upgradeVersion>3.4.4</pkg.upgradeVersion>
</properties>
<dependencies>

4
pom.xml

@ -818,8 +818,10 @@
<exclude>docker/haproxy/**</exclude>
<exclude>docker/tb-node/**</exclude>
<exclude>ui/**</exclude>
<exclude>src/.browserslistrc</exclude>
<exclude>**/.browserslistrc</exclude>
<exclude>**/yarn.lock</exclude>
<exclude>**/.yarnrc</exclude>
<exclude>**/.angular/**</exclude>
<exclude>**/*.raw</exclude>
<exclude>**/*.patch</exclude>
<exclude>**/apache/cassandra/io/**</exclude>

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "asset type switch",
name = "asset profile switch",
customRelations = true,
relationTypes = {},
configClazz = EmptyNodeConfiguration.class,

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java

@ -42,7 +42,7 @@ import java.io.IOException;
configClazz = TbCheckAlarmStatusNodeConfig.class,
relationTypes = {"True", "False"},
nodeDescription = "Checks alarm status.",
nodeDetails = "If the alarm status matches the specified one - msg is success if does not match - msg is failure.",
nodeDetails = "Checks the alarm status to match one of the specified statuses.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeCheckAlarmStatusConfig")
public class TbCheckAlarmStatusNode implements TbNode {

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java

@ -32,12 +32,13 @@ import java.util.Map;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "check existence fields",
name = "check fields presence",
relationTypes = {"True", "False"},
configClazz = TbCheckMessageNodeConfiguration.class,
nodeDescription = "Checks the existence of the selected keys from message data and metadata.",
nodeDetails = "If selected checkbox 'Check that all selected keys are present'\" and all keys in message data and metadata are exist - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.\n" +
"Else if the checkbox is not selected, and at least one of the keys from data or metadata of the message exists - send Message via <b>True</b> chain, otherwise, <b>False</b> chain is used. ",
nodeDescription = "Checks the presence of the specified fields in the message and/or metadata.",
nodeDetails = "Checks the presence of the specified fields in the message and/or metadata. " +
"By default, the rule node checks that all specified fields need to be present. " +
"Uncheck the 'Check that all specified fields are present' if the presence of at least one field is sufficient.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeCheckMessageConfig")
public class TbCheckMessageNode implements TbNode {

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java

@ -46,10 +46,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
name = "check relation",
configClazz = TbCheckRelationNodeConfiguration.class,
relationTypes = {"True", "False"},
nodeDescription = "Checks the relation from the selected entity to the originator of the message by type and direction" +
" if 'Check for single entity' is set to true, otherwise rule node will check if exist" +
" any relation to the originator of the message by type and direction.",
nodeDetails = "If at least one relation exists - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
nodeDescription = "Checks the presence of the relation between the originator of the message and other entities.",
nodeDetails = "If 'check relation to specific entity' is selected, one must specify a related entity. " +
"Otherwise, the rule node checks the presence of a relation to any entity that matches the direction and relation type criteria.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeCheckRelationConfig")
public class TbCheckRelationNode implements TbNode {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "device type switch",
name = "device profile switch",
customRelations = true,
relationTypes = {"default"},
configClazz = EmptyNodeConfiguration.class,

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -34,9 +34,10 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
type = ComponentType.FILTER,
name = "script", relationTypes = {"True", "False"},
configClazz = TbJsFilterNodeConfiguration.class,
nodeDescription = "Filter incoming messages using JS script",
nodeDetails = "Evaluate incoming Message with configured JS condition. " +
"If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
nodeDescription = "Filter incoming messages using TBEL or JS script",
nodeDetails = "Evaluates boolean function using incoming message. " +
"The function may be written using TBEL or plain JavaScript. " +
"Script function should return boolean value and accepts three parameters: <br/>" +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
"Message type can be accessed via <code>msgType</code> property.",

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -39,8 +39,8 @@ import java.util.Set;
name = "switch", customRelations = true,
relationTypes = {},
configClazz = TbJsSwitchNodeConfiguration.class,
nodeDescription = "Route incoming Message to one or multiple output chains",
nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
nodeDescription = "Routes incoming message to one OR multiple output connections.",
nodeDetails = "Node executes configured TBEL(recommended) or JavaScript function that returns array of strings (connection names). " +
"If Array is empty - message not routed to next Node. " +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java

@ -32,8 +32,8 @@ import org.thingsboard.server.common.msg.TbMsg;
name = "entity type",
configClazz = TbOriginatorTypeFilterNodeConfiguration.class,
relationTypes = {"True", "False"},
nodeDescription = "Filter incoming messages by message Originator Type",
nodeDetails = "If the entity type of the incoming message originator is expected - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.",
nodeDescription = "Filter incoming messages by the type of message originator entity",
nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeOriginatorTypeConfig")
public class TbOriginatorTypeFilterNode implements TbNode {

27
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java

@ -32,7 +32,32 @@ import org.thingsboard.server.common.msg.TbMsg;
configClazz = TbGpsGeofencingFilterNodeConfiguration.class,
relationTypes = {"True", "False"},
nodeDescription = "Filter incoming messages by GPS based geofencing",
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns 'True' if they are inside configured perimeters, 'False' otherwise.",
nodeDetails = "Extracts latitude and longitude parameters from the incoming message and checks them according to configured perimeter. </br>" +
"Configuration:</br></br>" +
"<ul>" +
"<li>Latitude key name - name of the message field that contains location latitude;</li>" +
"<li>Longitude key name - name of the message field that contains location longitude;</li>" +
"<li>Perimeter type - Polygon or Circle;</li>" +
"<li>Fetch perimeter from message metadata - checkbox to load perimeter from message metadata; " +
" Enable if your perimeter is specific to device/asset and you store it as device/asset attribute;</li>" +
"<li>Perimeter key name - name of the metadata key that stores perimeter information;</li>" +
"<li>For Polygon perimeter type: <ul>" +
" <li>Polygon definition - string that contains array of coordinates in the following format: [[lat1, lon1],[lat2, lon2],[lat3, lon3], ... , [latN, lonN]]</li>" +
"</ul></li>" +
"<li>For Circle perimeter type: <ul>" +
" <li>Center latitude - latitude of the circle perimeter center;</li>" +
" <li>Center longitude - longitude of the circle perimeter center;</li>" +
" <li>Range - value of the circle perimeter range, double-precision floating-point value;</li>" +
" <li>Range units - one of: Meter, Kilometer, Foot, Mile, Nautical Mile;</li>" +
"</ul></li></ul></br>" +
"Rule node will use default metadata key names, if the \"Fetch perimeter from message metadata\" is enabled and \"Perimeter key name\" is not configured. " +
"Default metadata key names for polygon perimeter type is \"perimeter\". Default metadata key names for circle perimeter are: \"centerLatitude\", \"centerLongitude\", \"range\", \"rangeUnit\"." +
"</br></br>" +
"Structure of the circle perimeter definition (stored in server-side attribute, for example):" +
"</br></br>" +
"{\"latitude\": 48.198618758582384, \"longitude\": 24.65322245153503, \"radius\": 100.0, \"radiusUnit\": \"METER\" }" +
"</br></br>" +
"Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeGpsGeofencingConfig")
public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode<TbGpsGeofencingFilterNodeConfiguration> {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java

@ -41,7 +41,7 @@ import java.util.concurrent.ExecutionException;
type = ComponentType.ENRICHMENT,
name = "fetch device credentials",
configClazz = TbFetchDeviceCredentialsNodeConfiguration.class,
nodeDescription = "Fetch device credentials for message originator",
nodeDescription = "Enrich the message body or metadata with the device credentials",
nodeDetails = "Adds <b>credentialsType</b> and <b>credentials</b> properties to the message metadata if the " +
"configuration parameter <b>fetchToMetadata</b> is set to <code>true</code>, otherwise, adds properties " +
"to the message data. If originator type is not <b>DEVICE</b> or rule node failed to get device credentials " +

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java

@ -34,7 +34,7 @@ import org.thingsboard.server.common.msg.TbMsg;
@RuleNode(type = ComponentType.ENRICHMENT,
name = "originator attributes",
configClazz = TbGetAttributesNodeConfiguration.class,
nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Data or Metadata",
nodeDescription = "Enrich the message body or metadata with the originator attributes and/or timeseries data",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message data/metadata " +
"with specific prefix: <i>cs/shared/ss</i>. Latest telemetry value added into Message data/metadata without prefix. " +
"To access those attributes in other nodes this template can be used " +

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -28,10 +28,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
name="customer attributes",
configClazz = TbGetEntityAttrNodeConfiguration.class,
nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>.",
nodeDetails = "Enrich the message metadata with the corresponding customer's latest attributes or telemetry value. " +
"The customer is selected based on the originator of the message: device, asset, etc. " +
"</br>" +
"Useful when you store some parameters on the customer level and would like to use them for message processing.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java

@ -38,7 +38,7 @@ import org.thingsboard.server.common.msg.TbMsg;
@RuleNode(type = ComponentType.ENRICHMENT,
name = "customer details",
configClazz = TbGetCustomerDetailsNodeConfiguration.class,
nodeDescription = "Adds fields from Customer details to the message body or metadata",
nodeDescription = "Enrich the message body or metadata with the corresponding customer details: title, address, email, phone, etc.",
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" +
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" +
"If the originator of the message is not assigned to Customer, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.",

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

67
ui-ngx/.eslintrc.json

@ -0,0 +1,67 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json",
"e2e/tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"arrow-parens": [
"off",
"always"
],
"@angular-eslint/component-selector": [
"error",
{
"prefix": [ "tb" ]
}
],
"id-blacklist": [
"error",
"any",
"Number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"import/order": "off",
"@typescript-eslint/member-ordering": "off",
"no-underscore-dangle": "off",
"@typescript-eslint/naming-convention": "off"
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

1
ui-ngx/.gitignore

@ -32,6 +32,7 @@ speed-measure-plugin.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage

36
ui-ngx/angular.json

@ -132,7 +132,15 @@
"qrcode",
"wcwidth",
"leaflet-polylinedecorator",
"ace-diff"
"ace-diff",
"messageformat-parser",
"html2canvas",
"jszip",
"moment",
"ace",
"ace-builds",
"diff-match-patch",
"tv4"
]
},
"configurations": {
@ -222,14 +230,11 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
@ -251,22 +256,15 @@
"devServerTarget": "thingsboard:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "thingsboard",
"cli": {
"packageManager": "yarn",
"analytics": false
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

17
ui-ngx/extra-webpack.config.js

@ -29,6 +29,11 @@ dirTree("./src/assets/locale/", {extensions: /\.json$/}, (item) => {
});
module.exports = (config, options) => {
config.ignoreWarnings.push(/Usage of '~' in imports is deprecated/);
config.ignoreWarnings.push(/Did you mean "left" instead?/);
config.ignoreWarnings.push(/autoprefixer/);
config.plugins.push(
new webpack.DefinePlugin({
TB_VERSION: JSON.stringify(require("./package.json").version),
@ -59,20 +64,20 @@ module.exports = (config, options) => {
})
);
const index = config.plugins.findIndex(p => p instanceof ngWebpack.ivy.AngularWebpackPlugin || p instanceof ngWebpack.AngularWebpackPlugin);
config.module.rules[2].use[0].options.aot = false;
const index = config.plugins.findIndex(p => p instanceof ngWebpack.AngularWebpackPlugin);
let angularWebpackPlugin = config.plugins[index];
if (config.mode === 'production') {
const angularCompilerOptions = angularWebpackPlugin.pluginOptions;
angularCompilerOptions.emitClassMetadata = true;
angularCompilerOptions.emitNgModuleScope = true;
config.plugins.splice(index, 1);
angularWebpackPlugin = new ngWebpack.ivy.AngularWebpackPlugin(angularCompilerOptions);
angularWebpackPlugin = new ngWebpack.AngularWebpackPlugin(angularCompilerOptions);
config.plugins.push(angularWebpackPlugin);
const javascriptOptimizerOptions = config.optimization.minimizer[1].options;
const javascriptOptimizerOptions = config.optimization.minimizer[0].options;
delete javascriptOptimizerOptions.define.ngJitMode;
config.optimization.minimizer.splice(1, 1);
config.optimization.minimizer.push(new JavaScriptOptimizerPlugin(javascriptOptimizerOptions));
config.optimization.minimizer.splice(0, 1);
config.optimization.minimizer.unshift(new JavaScriptOptimizerPlugin(javascriptOptimizerOptions));
}
addTransformerToAngularWebpackPlugin(angularWebpackPlugin, keysTransformer);

138
ui-ngx/package.json

@ -14,50 +14,52 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^12.2.16",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.16",
"@angular/compiler": "^12.2.16",
"@angular/core": "^12.2.16",
"@angular/flex-layout": "^12.0.0-beta.35",
"@angular/forms": "^12.2.16",
"@angular/material": "^12.2.13",
"@angular/platform-browser": "^12.2.16",
"@angular/platform-browser-dynamic": "^12.2.16",
"@angular/router": "^12.2.16",
"@auth0/angular-jwt": "^5.0.2",
"@date-io/date-fns": "^2.11.0",
"@angular/animations": "^14.2.12",
"@angular/cdk": "^14.2.7",
"@angular/common": "^14.2.12",
"@angular/compiler": "^14.2.12",
"@angular/core": "^14.2.12",
"@angular/flex-layout": "^14.0.0-beta.41",
"@angular/forms": "^14.2.12",
"@angular/material": "^14.2.7",
"@angular/platform-browser": "^14.2.12",
"@angular/platform-browser-dynamic": "^14.2.12",
"@angular/router": "^14.2.12",
"@auth0/angular-jwt": "^5.1.2",
"@date-io/core": "1.3.7",
"@date-io/date-fns": "1.3.7",
"@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "~0.4.6",
"@flowjs/ngx-flow": "~0.6.0",
"@geoman-io/leaflet-geoman-free": "^2.13.0",
"@juggle/resize-observer": "^3.3.1",
"@mat-datetimepicker/core": "~7.0.1",
"@mat-datetimepicker/core": "~10.1.1",
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/pickers": "3.3.10",
"@ngrx/effects": "^12.5.1",
"@ngrx/store": "^12.5.1",
"@ngrx/store-devtools": "^12.5.1",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@ngrx/effects": "^14.3.3",
"@ngrx/store": "^14.3.3",
"@ngrx/store-devtools": "^14.3.3",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",
"angular-gridster2": "~12.1.1",
"angular2-hotkeys": "^2.4.0",
"angular-gridster2": "~14.1.4",
"angular2-hotkeys": "^13.1.0",
"canvas-gauges": "^2.1.7",
"core-js": "^3.19.2",
"date-fns": "^2.26.0",
"core-js": "^3.26.1",
"date-fns": "2.0.0-alpha.27",
"dayjs": "1.11.4",
"flot": "https://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",
"html2canvas": "^1.3.3",
"html2canvas": "^1.4.1",
"jquery": "^3.6.0",
"jquery.terminal": "^2.29.4",
"js-beautify": "^1.14.0",
"json-schema-defaults": "^0.4.0",
"jstree": "^3.3.12",
"jstree-bootstrap-theme": "^1.0.1",
"jszip": "^3.7.1",
"jszip": "^3.10.1",
"leaflet": "~1.8.0",
"leaflet-polylinedecorator": "^1.6.0",
"leaflet-providers": "^1.13.0",
@ -66,20 +68,20 @@
"libphonenumber-js": "^1.10.4",
"messageformat": "^2.3.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"ngx-clipboard": "^14.0.2",
"ngx-color-picker": "^11.0.0",
"ngx-daterangepicker-material": "^5.0.1",
"ngx-drag-drop": "^2.0.0",
"ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/1.0.0",
"ngx-hm-carousel": "^2.0.1",
"ngx-markdown": "^12.1.0",
"ngx-sharebuttons": "^9.0.0",
"ngx-translate-messageformat-compiler": "^4.11.0",
"moment-timezone": "^0.5.40",
"ngx-clipboard": "^15.1.0",
"ngx-color-picker": "^13.0.0",
"ngx-daterangepicker-material": "^6.0.4",
"ngx-drag-drop": "^14.0.0",
"ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/2.0.0",
"ngx-hm-carousel": "^3.0.0",
"ngx-markdown": "^14.0.1",
"ngx-sharebuttons": "^11.0.0",
"ngx-translate-messageformat-compiler": "^5.1.0",
"objectpath": "^2.0.0",
"prettier": "^2.5.0",
"prop-types": "^15.7.2",
"qrcode": "^1.5.0",
"prettier": "^2.8.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.1",
"raphael": "^2.3.0",
"rc-select": "13.2.1",
"react": "17.0.2",
@ -87,29 +89,35 @@
"react-dom": "17.0.2",
"react-dropzone": "^11.4.2",
"reactcss": "^1.2.3",
"rxjs": "~6.6.7",
"schema-inspector": "^2.0.1",
"screenfull": "^6.0.0",
"split.js": "^1.6.4",
"rxjs": "~7.8.0",
"schema-inspector": "^2.0.2",
"screenfull": "^6.0.2",
"split.js": "^1.6.5",
"systemjs": "6.11.0",
"tinycolor2": "^1.4.2",
"tinycolor2": "~1.4.2",
"tooltipster": "^4.2.8",
"ts-transformer-keys": "^0.4.3",
"tslib": "^2.3.1",
"ts-transformer-keys": "^0.4.4",
"tslib": "^2.4.1",
"tv4": "^1.3.0",
"typeface-roboto": "^1.1.13",
"zone.js": "~0.11.4"
"zone.js": "~0.11.8"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~12.1.3",
"@angular-devkit/build-angular": "^12.2.17",
"@angular/cli": "^12.2.17",
"@angular/compiler-cli": "^12.2.16",
"@angular/language-service": "^12.2.16",
"@ngtools/webpack": "^12.2.17",
"@angular-builders/custom-webpack": "~14.1.0",
"@angular-devkit/build-angular": "^14.2.10",
"@angular-eslint/builder": "14.4.0",
"@angular-eslint/eslint-plugin": "14.4.0",
"@angular-eslint/eslint-plugin-template": "14.4.0",
"@angular-eslint/schematics": "14.4.0",
"@angular-eslint/template-parser": "14.4.0",
"@angular/cli": "^14.2.10",
"@angular/compiler-cli": "^14.2.12",
"@angular/language-service": "^14.2.12",
"@ngtools/webpack": "^14.2.10",
"@types/ace-diff": "^2.1.1",
"@types/canvas-gauges": "^2.1.4",
"@types/flot": "^0.0.32",
"@types/flowjs": "^2.13.9",
"@types/jasmine": "~3.10.2",
"@types/jasminewd2": "^2.0.10",
"@types/jquery": "^3.5.9",
@ -130,9 +138,14 @@
"@types/systemjs": "6.1.1",
"@types/tinycolor2": "^1.4.3",
"@types/tooltipster": "^0.0.31",
"codelyzer": "^6.0.2",
"compression-webpack-plugin": "^9.0.1",
"directory-tree": "^3.0.1",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"compression-webpack-plugin": "^10.0.0",
"directory-tree": "^3.5.1",
"eslint": "^8.28.0",
"eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-prefer-arrow": "latest",
"jasmine-core": "~3.10.1",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.3.9",
@ -141,18 +154,17 @@
"karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.7.0",
"ngrx-store-freeze": "^0.2.4",
"patch-package": "^6.4.7",
"patch-package": "^6.5.1",
"postinstall-prepare": "^2.0.0",
"protractor": "~7.0.0",
"raw-loader": "^4.0.2",
"ts-node": "^10.4.0",
"tslint": "~6.1.3",
"typescript": "~4.3.5",
"webpack": "^5.64.4"
"ts-node": "^10.9.1",
"typescript": "~4.6.4",
"webpack": "^5.75.0"
},
"resolutions": {
"lodash": "~4.17.21",
"@types/react": "17.0.37",
"ace-builds": "1.4.13"
"ace-builds": "1.4.13",
"@date-io/core": "1.3.7"
}
}

127
ui-ngx/src/app/core/api/widget-subscription.ts

@ -83,6 +83,59 @@ import { RpcStatus } from '@shared/models/rpc.models';
const moment = moment_;
const calculateMin = (data: DataSet): number => {
if (data.length > 0) {
let result = Number(data[0][1]);
for (let i = 1; i < data.length; i++) {
result = Math.min(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
};
const calculateMax = (data: DataSet): number => {
if (data.length > 0) {
let result = Number(data[0][1]);
for (let i = 1; i < data.length; i++) {
result = Math.max(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
};
const calculateTotal = (data: DataSet): number => {
if (data.length > 0) {
let result = 0;
data.forEach((dataRow) => {
result += Number(dataRow[1]);
});
return result;
} else {
return null;
}
};
const calculateAvg = (data: DataSet): number => {
if (data.length > 0) {
return calculateTotal(data) / data.length;
} else {
return null;
}
};
const calculateLatest = (data: DataSet): number => {
if (data.length > 0) {
return Number(data[data.length - 1][1]);
} else {
return null;
}
};
export class WidgetSubscription implements IWidgetSubscription {
id: string;
@ -151,7 +204,7 @@ export class WidgetSubscription implements IWidgetSubscription {
targetDeviceAliasId: string;
targetDeviceId: string;
targetDeviceName: string;
executingSubjects: Array<Subject<any>>;
executingSubjects: Array<Subject<void>>;
subscribed = false;
hasLatestData = false;
@ -305,7 +358,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private initRpc(): Observable<any> {
const initRpcSubject = new ReplaySubject();
const initRpcSubject = new ReplaySubject<void>();
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
this.ctx.aliasController.resolveSingleEntityInfo(this.targetDeviceAliasId).subscribe(
@ -354,7 +407,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private initAlarmSubscription(): Observable<any> {
const initAlarmSubscriptionSubject = new ReplaySubject(1);
const initAlarmSubscriptionSubject = new ReplaySubject<void>(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.hasResolvedData = true;
@ -387,7 +440,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private initDataSubscription(): Observable<any> {
this.notifyDataLoading();
const initDataSubscriptionSubject = new ReplaySubject(1);
const initDataSubscriptionSubject = new ReplaySubject<void>(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.configuredDatasources = deepClone(this.configuredDatasources);
@ -459,9 +512,7 @@ export class WidgetSubscription implements IWidgetSubscription {
initialPageDataChanged: this.initialPageDataChanged.bind(this),
forceReInit: this.forceReInit.bind(this),
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
return this.updateRealtimeSubscription();
},
updateRealtimeSubscription: () => this.updateRealtimeSubscription(),
setRealtimeSubscription: (subscriptionTimewindow) => {
this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
}
@ -762,7 +813,7 @@ export class WidgetSubscription implements IWidgetSubscription {
if (timeout && timeout > 0) {
requestBody.timeout = timeout;
}
const rpcSubject: Subject<any> = new Subject<any>();
const rpcSubject: Subject<any | void> = oneWayElseTwoWay ? new Subject<void>() : new Subject<any>();
this.executingRpcRequest = true;
this.callbacks.rpcStateChanged(this);
if (this.ctx.utils.widgetEditMode) {
@ -770,7 +821,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.executingRpcRequest = false;
this.callbacks.rpcStateChanged(this);
if (oneWayElseTwoWay) {
rpcSubject.next();
(rpcSubject as Subject<void>).next();
rpcSubject.complete();
} else {
rpcSubject.next(requestBody);
@ -911,9 +962,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true);
},
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
return this.updateRealtimeSubscription();
},
updateRealtimeSubscription: () => this.updateRealtimeSubscription(),
setRealtimeSubscription: (subscriptionTimewindow) => {
this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
}
@ -1560,7 +1609,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private loadStDiff(): Observable<any> {
const loadSubject = new ReplaySubject(1);
const loadSubject = new ReplaySubject<void>(1);
if (this.ctx.getServerTimeDiff && this.timeWindow) {
this.ctx.getServerTimeDiff().subscribe(
(stDiff) => {
@ -1584,55 +1633,3 @@ export class WidgetSubscription implements IWidgetSubscription {
return loadSubject.asObservable();
}
}
function calculateMin(data: DataSet): number {
if (data.length > 0) {
let result = Number(data[0][1]);
for (let i = 1; i < data.length; i++) {
result = Math.min(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateMax(data: DataSet): number {
if (data.length > 0) {
let result = Number(data[0][1]);
for (let i = 1; i < data.length; i++) {
result = Math.max(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateAvg(data: DataSet): number {
if (data.length > 0) {
return calculateTotal(data) / data.length;
} else {
return null;
}
}
function calculateTotal(data: DataSet): number {
if (data.length > 0) {
let result = 0;
data.forEach((dataRow) => {
result += Number(dataRow[1]);
});
return result;
} else {
return null;
}
}
function calculateLatest(data: DataSet): number {
if (data.length > 0) {
return Number(data[data.length - 1][1]);
} else {
return null;
}
}

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

@ -21,7 +21,7 @@ import { AuthState } from './auth.models';
import { take } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
export const selectAuthState = createFeatureSelector<AppState, AuthState>(
export const selectAuthState = createFeatureSelector< AuthState>(
'auth'
);

4
ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from '@angular/router';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AuthState } from '@core/auth/auth.models';
@ -27,7 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
import { isDefined } from '../utils';
export interface HasConfirmForm {
confirmForm(): FormGroup;
confirmForm(): UntypedFormGroup;
}
export interface HasDirtyFlag {

2
ui-ngx/src/app/core/http/entity.service.ts

@ -876,7 +876,7 @@ export class EntityService {
if (result.entityParamName && result.entityParamName.length) {
targetParams = stateParams[result.entityParamName];
}
if (targetParams && targetParams.entityId) {
if (targetParams && targetParams.entityId && targetParams.entityName) {
currentEntity = {
id: targetParams.entityId.id,
entityType: targetParams.entityId.entityType as EntityType,

16
ui-ngx/src/app/core/http/widget.service.ts

@ -51,7 +51,7 @@ export class WidgetService {
private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>();
private loadWidgetsBundleCacheSubject: ReplaySubject<any>;
private loadWidgetsBundleCacheSubject: ReplaySubject<void>;
constructor(
private http: HttpClient,
@ -111,15 +111,13 @@ export class WidgetService {
public deleteWidgetsBundle(widgetsBundleId: string, config?: RequestConfig) {
return this.getWidgetsBundle(widgetsBundleId, config).pipe(
mergeMap((widgetsBundle) => {
return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`,
mergeMap((widgetsBundle) => this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.invalidateWidgetsBundleCache();
this.widgetsBundleDeleted(widgetsBundle);
})
);
}
)
));
}
@ -232,14 +230,12 @@ export class WidgetService {
public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean,
config?: RequestConfig) {
return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, config).pipe(
mergeMap((widgetTypeInstance) => {
return this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`,
mergeMap((widgetTypeInstance) => this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.widgetTypeUpdated(widgetTypeInstance);
})
);
}
)
));
}
@ -301,7 +297,7 @@ export class WidgetService {
private loadWidgetsBundleCache(config?: RequestConfig): Observable<any> {
if (!this.allWidgetsBundles) {
if (!this.loadWidgetsBundleCacheSubject) {
this.loadWidgetsBundleCacheSubject = new ReplaySubject();
this.loadWidgetsBundleCacheSubject = new ReplaySubject<void>();
this.http.get<Array<WidgetsBundle>>('/api/widgetsBundles',
defaultHttpOptionsFromConfig(config)).subscribe(
(allWidgetsBundles) => {

2
ui-ngx/src/app/core/interceptors/load.selectors.ts

@ -20,7 +20,7 @@ import { AppState } from '../core.state';
import { LoadState } from './load.models';
import { take } from 'rxjs/operators';
export const selectLoadState = createFeatureSelector<AppState, LoadState>(
export const selectLoadState = createFeatureSelector< LoadState>(
'load'
);

14
ui-ngx/src/app/core/notification/notification.effects.ts

@ -15,7 +15,7 @@
///
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map } from 'rxjs/operators';
import { NotificationActions, NotificationActionTypes } from '@app/core/notification/notification.actions';
@ -29,23 +29,23 @@ export class NotificationEffects {
) {
}
@Effect({dispatch: false})
dispatchNotification = this.actions$.pipe(
dispatchNotification = createEffect(() => this.actions$.pipe(
ofType(
NotificationActionTypes.SHOW_NOTIFICATION,
),
map(({ notification }) => {
this.notificationService.dispatchNotification(notification);
})
);
), {dispatch: false});
@Effect({dispatch: false})
hideNotification = this.actions$.pipe(
hideNotification = createEffect(() => this.actions$.pipe(
ofType(
NotificationActionTypes.HIDE_NOTIFICATION,
),
map(({ hideNotification }) => {
this.notificationService.hideNotification(hideNotification);
})
);
), {dispatch: false});
}

4
ui-ngx/src/app/core/services/raf.service.ts

@ -33,9 +33,9 @@ export class RafService {
private ngZone: NgZone
) {
const requestAnimationFrame: (frameCallback: () => void) => number = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame;
(window as any).webkitRequestAnimationFrame;
const cancelAnimationFrame = window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
(window as any).webkitCancelAnimationFrame ||
// @ts-ignore
window.webkitCancelRequestAnimationFrame;

4
ui-ngx/src/app/core/services/resources.service.ts

@ -40,7 +40,7 @@ export interface ModulesWithFactories {
})
export class ResourcesService {
private loadedResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
@ -214,7 +214,7 @@ export class ResourcesService {
}
private loadResourceByType(type: 'css' | 'js', url: string): Observable<any> {
const subject = new ReplaySubject();
const subject = new ReplaySubject<void>();
this.loadedResources[url] = subject;
let el;
let loaded = false;

2
ui-ngx/src/app/core/services/utils.service.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
// tslint:disable-next-line:no-reference
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../src/typings/rawloader.typings.d.ts" />
import { Inject, Injectable, NgZone } from '@angular/core';

26
ui-ngx/src/app/core/settings/settings.effects.ts

@ -17,7 +17,7 @@
import { ActivationEnd, Router } from '@angular/router';
import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { TranslateService } from '@ngx-translate/core';
import { merge } from 'rxjs';
import { distinctUntilChanged, filter, map, tap, withLatestFrom } from 'rxjs/operators';
@ -49,8 +49,8 @@ export class SettingsEffects {
) {
}
@Effect({dispatch: false})
persistSettings = this.actions$.pipe(
persistSettings = createEffect(() => this.actions$.pipe(
ofType(
SettingsActionTypes.CHANGE_LANGUAGE,
),
@ -58,18 +58,18 @@ export class SettingsEffects {
tap(([action, settings]) =>
this.localStorageService.setItem(SETTINGS_KEY, settings)
)
);
), {dispatch: false});
@Effect({dispatch: false})
setTranslateServiceLanguage = this.store.pipe(
setTranslateServiceLanguage = createEffect(() => this.store.pipe(
select(selectSettingsState),
map(settings => settings.userLang),
distinctUntilChanged(),
tap(userLang => updateUserLang(this.translate, userLang))
);
), {dispatch: false});
@Effect({dispatch: false})
setTitle = merge(
setTitle = createEffect(() => merge(
this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)),
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
).pipe(
@ -79,10 +79,10 @@ export class SettingsEffects {
this.translate
);
})
);
), {dispatch: false});
@Effect({dispatch: false})
setPublicId = merge(
setPublicId = createEffect(() => merge(
this.router.events.pipe(filter(event => event instanceof ActivationEnd))
).pipe(
tap((event) => {
@ -95,5 +95,5 @@ export class SettingsEffects {
{ lastPublicDashboardId: snapshot.params.dashboardId}));
}
})
);
), {dispatch: false});
}

2
ui-ngx/src/app/core/settings/settings.selectors.ts

@ -19,7 +19,7 @@ import { createFeatureSelector, createSelector } from '@ngrx/store';
import { SettingsState } from './settings.models';
import { AppState } from '@app/core/core.state';
export const selectSettingsState = createFeatureSelector<AppState, SettingsState>(
export const selectSettingsState = createFeatureSelector< SettingsState>(
'settings'
);

4
ui-ngx/src/app/core/translate/translate-default-parser.ts

@ -22,7 +22,7 @@ import { isDefinedAndNotNull } from '@core/utils';
export class TranslateDefaultParser extends TranslateParser {
templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g;
// tslint:disable-next-line:ban-types
// eslint-disable-next-line @typescript-eslint/ban-types
public interpolate(expr: string | Function, params?: any): string {
let result: string;
@ -58,7 +58,7 @@ export class TranslateDefaultParser extends TranslateParser {
return target;
}
// tslint:disable-next-line:ban-types
// eslint-disable-next-line @typescript-eslint/ban-types
private interpolateFunction(fn: Function, params?: any) {
return fn(params);
}

4
ui-ngx/src/app/core/utils.ts

@ -258,9 +258,9 @@ export function hashCode(str: string): number {
}
for (i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
// tslint:disable-next-line:no-bitwise
// eslint-disable-next-line no-bitwise
hash = ((hash << 5) - hash) + char;
// tslint:disable-next-line:no-bitwise
// eslint-disable-next-line no-bitwise
hash = hash & hash; // Convert to 32bit integer
}
return hash;

4
ui-ngx/src/app/modules/common/modules-map.ts

@ -241,7 +241,7 @@ import * as FilterPredicateValueComponent from '@home/components/filter/filter-p
import * as TenantProfileComponent from '@home/components/profile/tenant-profile.component';
import * as TenantProfileDialogComponent from '@home/components/profile/tenant-profile-dialog.component';
import * as TenantProfileDataComponent from '@home/components/profile/tenant-profile-data.component';
// tslint:disable-next-line:max-line-length
// eslint-disable-next-line max-len
import * as DefaultDeviceProfileConfigurationComponent from '@home/components/profile/device/default-device-profile-configuration.component';
import * as DeviceProfileConfigurationComponent from '@home/components/profile/device/device-profile-configuration.component';
import * as DeviceProfileComponent from '@home/components/profile/device-profile.component';
@ -266,7 +266,7 @@ import * as AlarmScheduleInfoComponent from '@home/components/profile/alarm/alar
import * as AlarmScheduleDialogComponent from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import * as EditAlarmDetailsDialogComponent from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
import * as AlarmRuleConditionDialogComponent from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
// tslint:disable-next-line:max-line-length
// eslint-disable-next-line max-len
import * as DefaultTenantProfileConfigurationComponent from '@home/components/profile/tenant/default-tenant-profile-configuration.component';
import * as TenantProfileConfigurationComponent from '@home/components/profile/tenant/tenant-profile-configuration.component';
import * as SmsProviderConfigurationComponent from '@home/components/sms/sms-provider-configuration.component';

6
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts

@ -18,7 +18,7 @@ import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Observable, ReplaySubject } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
@ -50,7 +50,7 @@ export interface AlarmDetailsDialogData {
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit {
alarmId: string;
alarmFormGroup: FormGroup;
alarmFormGroup: UntypedFormGroup;
allowAcknowledgment: boolean;
allowClear: boolean;
@ -73,7 +73,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
@Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData,
private alarmService: AlarmService,
public dialogRef: MatDialogRef<AlarmDetailsDialogComponent, boolean>,
public fb: FormBuilder) {
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.allowAcknowledgment = data.allowAcknowledgment;

6
ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts

@ -15,7 +15,7 @@
///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
@ -40,7 +40,7 @@ import { isDefinedAndNotNull } from '@core/utils';
})
export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit {
selectEntityInfoFormGroup: FormGroup;
selectEntityInfoFormGroup: UntypedFormGroup;
modelValue: EntityInfo | null;
@ -73,7 +73,7 @@ export class AliasesEntityAutocompleteComponent implements ControlValueAccessor,
constructor(private store: Store<AppState>,
public translate: TranslateService,
private entityService: EntityService,
private fb: FormBuilder) {
private fb: UntypedFormBuilder) {
this.selectEntityInfoFormGroup = this.fb.group({
entityInfo: [null]
});

14
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts

@ -20,9 +20,9 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormBuilder,
FormControl,
FormGroup,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
@ -59,7 +59,7 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
alias: EntityAlias;
entityAliasFormGroup: FormGroup;
entityAliasFormGroup: UntypedFormGroup;
submitted = false;
@ -68,7 +68,7 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
@Inject(MAT_DIALOG_DATA) public data: EntityAliasDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<EntityAliasDialogComponent, EntityAlias>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private utils: UtilsService,
public translate: TranslateService,
private entityService: EntityService) {
@ -103,7 +103,7 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
}
validateDuplicateAliasName(): ValidatorFn {
return (c: FormControl) => {
return (c: UntypedFormControl) => {
const newAlias = c.value.trim();
const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias);
if (found) {
@ -122,7 +122,7 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

12
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts

@ -17,9 +17,9 @@
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR,
NgForm
@ -56,7 +56,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
})
export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher {
selectEntityAliasFormGroup: FormGroup;
selectEntityAliasFormGroup: UntypedFormGroup;
modelValue: string | null;
@ -107,7 +107,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
private entityService: EntityService,
public translate: TranslateService,
public truncate: TruncatePipe,
private fb: FormBuilder) {
private fb: UntypedFormBuilder) {
this.selectEntityAliasFormGroup = this.fb.group({
entityAlias: [null]
});
@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
);
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.tbRequired && !this.modelValue;
return originalErrorState || customErrorState;

24
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts

@ -21,10 +21,10 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormArray,
FormBuilder,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators
@ -67,7 +67,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
aliasToWidgetsMap: {[aliasId: string]: Array<string>} = {};
entityAliasesFormGroup: FormGroup;
entityAliasesFormGroup: UntypedFormGroup;
submitted = false;
@ -76,7 +76,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
@Inject(MAT_DIALOG_DATA) public data: EntityAliasesDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<EntityAliasesDialogComponent, EntityAliases>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private translate: TranslateService,
private dialogs: DialogService,
@ -157,14 +157,14 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
}
entityAliasesFormArray(): FormArray {
return this.entityAliasesFormGroup.get('entityAliases') as FormArray;
entityAliasesFormArray(): UntypedFormArray {
return this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray;
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
@ -183,7 +183,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
this.dialogs.alert(this.translate.instant('entity.unable-delete-entity-alias-title'),
message, this.translate.instant('action.close'), true);
} else {
(this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index);
(this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray).removeAt(index);
this.entityAliasesFormGroup.markAsDirty();
}
}
@ -216,10 +216,10 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
}).afterClosed().subscribe((entityAlias) => {
if (entityAlias) {
if (isAdd) {
(this.entityAliasesFormGroup.get('entityAliases') as FormArray)
(this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray)
.push(this.createEntityAliasFormControl(entityAlias.id, entityAlias));
} else {
const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index);
const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray).at(index);
aliasFormControl.get('alias').patchValue(entityAlias.alias);
aliasFormControl.get('filter').patchValue(entityAlias.filter);
aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple);

8
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
@ -40,7 +40,7 @@ export interface AddAttributeDialogData {
export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDialogComponent, boolean>
implements OnInit, ErrorStateMatcher {
attributeFormGroup: FormGroup;
attributeFormGroup: UntypedFormGroup;
submitted = false;
@ -50,7 +50,7 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
private attributeService: AttributeService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddAttributeDialogComponent, boolean>,
public fb: FormBuilder) {
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
}
@ -61,7 +61,7 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
});
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
@ -57,7 +57,7 @@ export class AddWidgetToDashboardDialogComponent extends
DialogComponent<AddWidgetToDashboardDialogComponent, void>
implements OnInit, ErrorStateMatcher {
addWidgetFormGroup: FormGroup;
addWidgetFormGroup: UntypedFormGroup;
submitted = false;
@ -66,7 +66,7 @@ export class AddWidgetToDashboardDialogComponent extends
@Inject(MAT_DIALOG_DATA) public data: AddWidgetToDashboardDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetToDashboardDialogComponent, void>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private dashboardService: DashboardService,
@ -107,7 +107,7 @@ export class AddWidgetToDashboardDialogComponent extends
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts

@ -18,7 +18,7 @@ import { Component, Inject, InjectionToken, OnInit, SkipSelf } from '@angular/co
import { ErrorStateMatcher } from '@angular/material/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { OverlayRef } from '@angular/cdk/overlay';
@ -36,7 +36,7 @@ export interface EditAttributeValuePanelData {
})
export class EditAttributeValuePanelComponent extends PageComponent implements OnInit, ErrorStateMatcher {
attributeFormGroup: FormGroup;
attributeFormGroup: UntypedFormGroup;
result: any = null;
@ -46,7 +46,7 @@ export class EditAttributeValuePanelComponent extends PageComponent implements O
@Inject(EDIT_ATTRIBUTE_VALUE_PANEL_DATA) public data: EditAttributeValuePanelData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public overlayRef: OverlayRef,
public fb: FormBuilder) {
public fb: UntypedFormBuilder) {
super(store);
}
@ -56,7 +56,7 @@ export class EditAttributeValuePanelComponent extends PageComponent implements O
});
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, widgetTypesData } from '@shared/models/widget.models';
@ -44,7 +44,7 @@ export interface AddWidgetDialogData {
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {
widgetFormGroup: FormGroup;
widgetFormGroup: UntypedFormGroup;
dashboard: Dashboard;
aliasController: IAliasController;
@ -57,7 +57,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>,
private fb: FormBuilder) {
private fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.dashboard = this.data.dashboard;
@ -115,7 +115,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts

@ -18,7 +18,7 @@ import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DashboardId } from '@shared/models/id/dashboard-id';
@ -56,8 +56,8 @@ export class DashboardImageDialogComponent extends DialogComponent<DashboardImag
safeImageUrl?: SafeUrl;
dashboardElement: HTMLElement;
dashboardRectFormGroup: FormGroup;
dashboardImageFormGroup: FormGroup;
dashboardRectFormGroup: UntypedFormGroup;
dashboardImageFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@ -65,7 +65,7 @@ export class DashboardImageDialogComponent extends DialogComponent<DashboardImag
public dialogRef: MatDialogRef<DashboardImageDialogComponent, DashboardImageDialogResult>,
private dashboardService: DashboardService,
private sanitizer: DomSanitizer,
private fb: FormBuilder) {
private fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.dashboardId = this.data.dashboardId;

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -94,7 +94,7 @@ import {
WidgetContextMenuItem
} from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { FormBuilder } from '@angular/forms';
import { UntypedFormBuilder } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service';
import { MatDialog } from '@angular/material/dialog';
import {
@ -352,7 +352,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private itembuffer: ItemBufferService,
private importExport: ImportExportService,
private mobileService: MobileService,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
private popoverService: TbPopoverService,

10
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
@ -46,8 +46,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
settings: DashboardSettings;
gridSettings: GridSettings;
settingsFormGroup: FormGroup;
gridSettingsFormGroup: FormGroup;
settingsFormGroup: UntypedFormGroup;
gridSettingsFormGroup: UntypedFormGroup;
stateControllerIds: string[];
@ -58,7 +58,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
@Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
@ -181,7 +181,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

6
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -25,7 +25,7 @@ import { Widget } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { isDefined, isString } from '@core/utils';
import { FormBuilder, FormGroup } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@Component({
selector: 'tb-edit-widget',
@ -49,13 +49,13 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input()
widgetLayout: WidgetLayout;
widgetFormGroup: FormGroup;
widgetFormGroup: UntypedFormGroup;
widgetConfig: WidgetConfigComponentData;
constructor(protected store: Store<AppState>,
private dialog: MatDialog,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private widgetComponentService: WidgetComponentService) {
super(store);
this.widgetFormGroup = this.fb.group({

2
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use '~@angular/material' as mat;
@use '@angular/material' as mat;
@import '../theme.scss';
$tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);

12
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts

@ -21,9 +21,9 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators
@ -62,7 +62,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
@ViewChild('tooltip', {static: true}) tooltip: MatTooltip;
layoutsFormGroup: FormGroup;
layoutsFormGroup: UntypedFormGroup;
layoutWidthType = LayoutWidthType;
@ -81,7 +81,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
@Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
@ -203,7 +203,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

14
ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.ts

@ -20,9 +20,9 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormBuilder,
FormControl,
FormGroup,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
@ -51,7 +51,7 @@ export class DashboardStateDialogComponent extends
DialogComponent<DashboardStateDialogComponent, DashboardStateInfo>
implements OnInit, ErrorStateMatcher {
stateFormGroup: FormGroup;
stateFormGroup: UntypedFormGroup;
states: {[id: string]: DashboardState };
state: DashboardStateInfo;
@ -68,7 +68,7 @@ export class DashboardStateDialogComponent extends
@Inject(MAT_DIALOG_DATA) public data: DashboardStateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DashboardStateDialogComponent, DashboardStateInfo>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private dashboardUtils: DashboardUtilsService) {
super(store, router, dialogRef);
@ -108,7 +108,7 @@ export class DashboardStateDialogComponent extends
}
private validateDuplicateStateId(): ValidatorFn {
return (c: FormControl) => {
return (c: UntypedFormControl) => {
const newStateId: string = c.value;
if (newStateId) {
const existing = this.states[newStateId];
@ -125,7 +125,7 @@ export class DashboardStateDialogComponent extends
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DashboardState } from '@app/shared/models/dashboard.models';
@ -55,7 +55,7 @@ export class ManageDashboardStatesDialogComponent extends
DialogComponent<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }>
implements OnInit, ErrorStateMatcher, AfterViewInit {
statesFormGroup: FormGroup;
statesFormGroup: UntypedFormGroup;
states: {[id: string]: DashboardState };
@ -76,7 +76,7 @@ export class ManageDashboardStatesDialogComponent extends
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private dialogs: DialogService,
private dialog: MatDialog) {
@ -233,7 +233,7 @@ export class ManageDashboardStatesDialogComponent extends
this.updateData(true);
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

2
ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts

@ -31,7 +31,7 @@ import { IStateControllerComponent } from '@home/components/dashboard-page/state
import { Subject } from 'rxjs';
@Directive({
// tslint:disable-next-line:directive-selector
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'tb-states-component'
})
export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {

8
ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DashboardState } from '@app/shared/models/dashboard.models';
@ -40,7 +40,7 @@ export class SelectTargetStateDialogComponent extends
implements OnInit, ErrorStateMatcher {
states: {[id: string]: DashboardState };
stateFormGroup: FormGroup;
stateFormGroup: UntypedFormGroup;
submitted = false;
@ -49,7 +49,7 @@ export class SelectTargetStateDialogComponent extends
@Inject(MAT_DIALOG_DATA) public data: SelectTargetStateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<SelectTargetStateDialogComponent, string>,
private fb: FormBuilder,
private fb: UntypedFormBuilder,
private dashboardUtils: DashboardUtilsService) {
super(store, router, dialogRef);
@ -65,7 +65,7 @@ export class SelectTargetStateDialogComponent extends
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/details-panel.component.ts

@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, Output }
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
@Component({
@ -36,11 +36,11 @@ export class DetailsPanelComponent extends PageComponent implements OnDestroy {
@Input() isShowSearch = false;
@Input() backgroundColor = '#FFF';
private theFormValue: FormGroup;
private theFormValue: UntypedFormGroup;
private formSubscription: Subscription = null;
@Input()
set theForm(value: FormGroup) {
set theForm(value: UntypedFormGroup) {
if (this.theFormValue !== value) {
if (this.formSubscription !== null) {
this.formSubscription.unsubscribe();
@ -53,7 +53,7 @@ export class DetailsPanelComponent extends PageComponent implements OnDestroy {
}
}
get theForm(): FormGroup {
get theForm(): UntypedFormGroup {
return this.theFormValue;
}

12
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts

@ -17,8 +17,8 @@
import { Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
@ -55,17 +55,17 @@ import { Subject } from 'rxjs';
export class DeviceCredentialsLwm2mServerComponent implements OnDestroy, ControlValueAccessor, Validator {
serverFormGroup: FormGroup;
serverFormGroup: UntypedFormGroup;
securityConfigLwM2MType = Lwm2mSecurityType;
securityConfigLwM2MTypes = Object.values(Lwm2mSecurityType);
lwm2mSecurityTypeTranslationMap = Lwm2mSecurityTypeTranslationMap;
publicKeyOrIdTooltipNamesMap = Lwm2mPublicKeyOrIdTooltipTranslationsMap;
clientSecretKeyTooltipNamesMap = Lwm2mClientSecretKeyTooltipTranslationsMap;
private destroy$ = new Subject();
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: FormBuilder) {
constructor(private fb: UntypedFormBuilder) {
this.serverFormGroup = this.fb.group({
securityMode: [Lwm2mSecurityType.NO_SEC],
clientPublicKeyOrId: [''],
@ -148,5 +148,5 @@ export class DeviceCredentialsLwm2mServerComponent implements OnDestroy, Control
this.serverFormGroup.get('clientPublicKeyOrId').enable({emitEvent: false});
this.serverFormGroup.get('clientSecretKey').enable();
}
};
}

12
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts

@ -17,8 +17,8 @@
import { Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
@ -56,16 +56,16 @@ import { isDefinedAndNotNull } from '@core/utils';
export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy {
lwm2mConfigFormGroup: FormGroup;
lwm2mConfigFormGroup: UntypedFormGroup;
securityConfigLwM2MType = Lwm2mSecurityType;
securityConfigLwM2MTypes = Object.keys(Lwm2mSecurityType);
credentialTypeLwM2MNamesMap = Lwm2mSecurityTypeTranslationMap;
clientKeyTooltipNamesMap = Lwm2mClientKeyTooltipTranslationsMap;
private destroy$ = new Subject();
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: FormBuilder) {
constructor(private fb: UntypedFormBuilder) {
this.lwm2mConfigFormGroup = this.initLwm2mConfigForm();
}
@ -161,7 +161,7 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
this.lwm2mConfigFormGroup.get('client.cert').disable({emitEvent: false});
}
private initLwm2mConfigForm = (): FormGroup => {
private initLwm2mConfigForm = (): UntypedFormGroup => {
const formGroup = this.fb.group({
client: this.fb.group({
endpoint: ['', Validators.required],

12
ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts

@ -17,8 +17,8 @@
import { Component, forwardRef, Input, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
@ -52,12 +52,12 @@ export class DeviceCredentialsMqttBasicComponent implements ControlValueAccessor
@Input()
disabled: boolean;
deviceCredentialsMqttFormGroup: FormGroup;
deviceCredentialsMqttFormGroup: UntypedFormGroup;
private destroy$ = new Subject();
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(public fb: FormBuilder) {
constructor(public fb: UntypedFormBuilder) {
this.deviceCredentialsMqttFormGroup = this.fb.group({
clientId: [null],
userName: [null],
@ -119,7 +119,7 @@ export class DeviceCredentialsMqttBasicComponent implements ControlValueAccessor
}
private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
return (group: UntypedFormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}

14
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -17,9 +17,9 @@
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
@ -73,9 +73,9 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
}
}
private destroy$ = new Subject();
private destroy$ = new Subject<void>();
deviceCredentialsFormGroup: FormGroup;
deviceCredentialsFormGroup: UntypedFormGroup;
deviceCredentialsType = DeviceCredentialsType;
@ -85,7 +85,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
private propagateChange = (v: any) => {};
constructor(public fb: FormBuilder) {
constructor(public fb: UntypedFormBuilder) {
this.deviceCredentialsFormGroup = this.fb.group({
credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
credentialsId: [null],
@ -148,7 +148,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
}
}
public validate(c: FormControl) {
public validate(c: UntypedFormControl) {
return this.deviceCredentialsFormGroup.valid ? null : {
deviceCredentials: {
valid: false,

6
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts

@ -19,7 +19,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
@ -40,7 +40,7 @@ export class AddEntityDialogComponent extends
DialogComponent<AddEntityDialogComponent, BaseData<HasId>> implements OnInit, ErrorStateMatcher {
entityComponent: EntityComponent<BaseData<HasId>>;
detailsForm: FormGroup;
detailsForm: UntypedFormGroup;
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
translations: EntityTypeTranslation;
@ -98,7 +98,7 @@ export class AddEntityDialogComponent extends
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

8
ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts

@ -16,7 +16,7 @@
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ContactBased } from '@shared/models/contact-based.model';
import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core';
import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models';
@ -28,14 +28,14 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod
export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit {
protected constructor(protected store: Store<AppState>,
protected fb: FormBuilder,
protected fb: UntypedFormBuilder,
protected entityValue: T,
protected entitiesTableConfigValue: EntityTableConfig<T>,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
buildForm(entity: T): FormGroup {
buildForm(entity: T): UntypedFormGroup {
const entityForm = this.buildEntityForm(entity);
entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [Validators.maxLength(255)]));
entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [Validators.maxLength(255)]));
@ -82,7 +82,7 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten
return zipValidators;
}
abstract buildEntityForm(entity: T): FormGroup;
abstract buildEntityForm(entity: T): UntypedFormGroup;
abstract updateEntityForm(entity: T);

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

@ -29,7 +29,7 @@ import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { BaseData, HasId } from '@shared/models/base-data';
import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { AssetId } from '@shared/models/id/asset-id';
import { TranslateService } from '@ngx-translate/core';
import { deepClone } from '@core/utils';
@ -140,7 +140,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl
});
}
confirmForm(): FormGroup {
confirmForm(): UntypedFormGroup {
return this.detailsForm;
}

4
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

@ -36,7 +36,7 @@ import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { BaseData, HasId, hasIdEquals } from '@shared/models/base-data';
import { EntityType, EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { EntityComponent } from './entity.component';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { EntityAction } from '@home/models/entity/entity-component.models';
@ -68,7 +68,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
entityTabsComponentRef: ComponentRef<EntityTabsComponent<BaseData<HasId>>>;
entityTabsComponent: EntityTabsComponent<BaseData<HasId>>;
detailsForm: FormGroup;
detailsForm: UntypedFormGroup;
entitiesTableConfigValue: EntityTableConfig<BaseData<HasId>>;
isEditValue = false;

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

Loading…
Cancel
Save