Browse Source

Merge branch 'master' into feature/scada-symbol

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
e150898bf1
  1. 8
      application/src/main/data/json/tenant/dashboards/gateways.json
  2. 13
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  3. 3
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  4. 3
      application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
  5. 3
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  6. 3
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  7. 4
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  8. 4
      application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java
  9. 3
      application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java
  10. 3
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  11. 2
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  12. 4
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 5
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
  16. 11
      application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java
  17. 10
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  18. 20
      dao/src/main/java/org/thingsboard/server/dao/eventsourcing/ActionCause.java
  19. 4
      dao/src/main/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEvent.java
  20. 3
      dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java
  21. 20
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  22. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
  23. 5
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  24. 11
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  25. 88
      dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java
  26. 2
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts
  27. 1
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html
  28. 8
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html
  29. 17
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts
  30. 2
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html
  31. 11
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts
  32. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html
  33. 8
      ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts
  34. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  35. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts
  36. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  37. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  38. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.html
  39. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss
  40. 38
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts
  41. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html
  42. 19
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  43. 2
      ui-ngx/src/app/shared/models/limited-api.models.ts
  44. 2
      ui-ngx/src/assets/locale/locale.constant-ar_AE.json
  45. 2
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  46. 2
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  47. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  48. 2
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  49. 2
      ui-ngx/src/assets/locale/locale.constant-lt_LT.json
  50. 2
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  51. 2
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json
  52. 2
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  53. 2
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  54. 2
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json

8
application/src/main/data/json/tenant/dashboards/gateways.json

@ -181,12 +181,12 @@
"actions": {
"actionCellButton": [
{
"name": "Docker commands",
"name": "Launch command",
"icon": "terminal",
"useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;",
"type": "customPretty",
"customHtml": "<div class=\"container\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Docker commands</h2>\n <span fxFlex></span>\n <div [tb-help]=\"'gatewayInstall'\"></div>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <tb-gateway-command [deviceId]=\"entityId\"></tb-gateway-command>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n (click)=\"cancel()\" cdkFocusInitial>\n {{ 'action.close' | translate }}\n </button>\n </div>\n</div>\n",
"customHtml": "<div class=\"container\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>{{ 'gateway.launch-command' | translate }}</h2>\n <span fxFlex></span>\n <div [tb-help]=\"'gatewayInstall'\"></div>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <tb-gateway-command [deviceId]=\"entityId\"></tb-gateway-command>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n (click)=\"cancel()\" cdkFocusInitial>\n {{ 'action.close' | translate }}\n </button>\n </div>\n</div>\n",
"customCss": ".container {\n display: grid;\n grid-template-rows: min-content minmax(auto, 1fr) min-content;\n height: 100%;\n max-height: 100vh;\n width: 600px;\n max-width: 100%;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenCommands();\n\nfunction openCommands() {\n customDialog.customDialog(htmlTemplate, CommandsDialogController, {panelClass: \"test\"}).subscribe();\n}\n\nfunction CommandsDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n",
"customResources": [],
@ -1861,7 +1861,7 @@
"padding": "8px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"action-buttons-container\">\r\n <button mat-raised-button color=\"primary\"\r\n (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[0], ctx.datasources[0].entity.id)\"\r\n >Launch command\r\n </button>\r\n</div>",
"markdownTextPattern": "<div class=\"action-buttons-container\">\r\n <button mat-raised-button color=\"primary\"\r\n (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[0], ctx.datasources[0].entity.id)\"\r\n >{{ 'gateway.launch-command' | translate }}\r\n </button>\r\n</div>",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: center;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n}"
},
@ -1887,7 +1887,7 @@
"useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;",
"type": "customPretty",
"customHtml": "<div class=\"container\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Launch command</h2>\n <span fxFlex></span>\n <div [tb-help]=\"'gatewayInstall'\"></div>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <tb-gateway-command [deviceId]=\"entityId\"></tb-gateway-command>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n (click)=\"cancel()\" cdkFocusInitial>\n {{ 'action.close' | translate }}\n </button>\n </div>\n</div>\n",
"customHtml": "<div class=\"container\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>{{ 'gateway.launch-command' | translate }}</h2>\n <span fxFlex></span>\n <div [tb-help]=\"'gatewayInstall'\"></div>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <tb-gateway-command [deviceId]=\"entityId\"></tb-gateway-command>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n (click)=\"cancel()\" cdkFocusInitial>\n {{ 'action.close' | translate }}\n </button>\n </div>\n</div>\n",
"customCss": ".container {\n display: grid;\n grid-template-rows: min-content minmax(auto, 1fr) min-content;\n height: 100%;\n max-height: 100vh;\n width: 600px;\n max-width: 100%;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenCommands();\n\nfunction openCommands() {\n customDialog.customDialog(htmlTemplate, CommandsDialogController, {panelClass: \"test\"}).subscribe();\n}\n\nfunction CommandsDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n",
"customResources": [],

13
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -350,11 +351,11 @@ public class AlarmController extends BaseController {
@PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})))
@RequestParam(required = false) String[] statusList,
@Parameter(description = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE"}))
@Parameter(description = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE"})))
@RequestParam(required = false) String[] severityList,
@Parameter(description = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@Parameter(description = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam(required = false) String[] typeList,
@Parameter(description = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ -412,11 +413,11 @@ public class AlarmController extends BaseController {
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})))
@RequestParam(required = false) String[] statusList,
@Parameter(description = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE"}))
@Parameter(description = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE"})))
@RequestParam(required = false) String[] severityList,
@Parameter(description = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@Parameter(description = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam(required = false) String[] typeList,
@Parameter(description = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,

3
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -354,7 +355,7 @@ public class AssetController extends BaseController {
@RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET)
@ResponseBody
public List<Asset> getAssetsByIds(
@Parameter(description = "A list of assets ids, separated by comma ','")
@Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException {
checkArrayParameter("assetIds", strAssetIds);
SecurityUser user = getCurrentUser();

3
application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
@ -83,7 +84,7 @@ public class ComponentDescriptorController extends BaseController {
@RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
@ResponseBody
public List<ComponentDescriptor> getComponentDescriptorsByTypes(
@Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true)
@Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("componentTypes") String[] strComponentTypes,
@Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"}))
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {

3
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -488,7 +489,7 @@ public class DeviceController extends BaseController {
@RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
@ResponseBody
public List<Device> getDevicesByIds(
@Parameter(description = "A list of devices ids, separated by comma ','")
@Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException {
checkArrayParameter("deviceIds", strDeviceIds);
SecurityUser user = getCurrentUser();

3
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -426,7 +427,7 @@ public class EdgeController extends BaseController {
@RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET)
@ResponseBody
public List<Edge> getEdgesByIds(
@Parameter(description = "A list of edges ids, separated by comma ','", required = true)
@Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException {
checkArrayParameter("edgeIds", strEdgeIds);
SecurityUser user = getCurrentUser();

4
application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -156,7 +158,7 @@ public class NotificationTargetController extends BaseController {
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets", params = {"ids"})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", required = true)
public List<NotificationTarget> getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) {
// PE: generic permission

4
application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@ -147,7 +149,7 @@ public class NotificationTemplateController extends BaseController {
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@Parameter(description = "Comma-separated list of notification types to filter the templates")
@Parameter(description = "Comma-separated list of notification types to filter the templates", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam(required = false) NotificationType[] notificationTypes,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// PE: generic permission

3
application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
@ -89,7 +90,7 @@ public class QueueStatsController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats", params = {"queueStatsIds"})
public List<QueueStats> getQueueStatsByIds(
@Parameter(description = "A list of queue stats ids, separated by comma ','", required = true)
@Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException {
checkArrayParameter("queueStatsIds", strQueueStatsIds);
List<QueueStatsId> queueStatsIds = new ArrayList<>();

3
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -272,7 +273,7 @@ public class TbResourceController extends BaseController {
@RequestParam String sortOrder,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam String sortProperty,
@Parameter(description = "LwM2M Object ids.", required = true)
@Parameter(description = "LwM2M Object ids.", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam(required = false) String[] objectIds) throws ThingsboardException {
return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds));
}

2
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -447,7 +447,7 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope")AttributeScope scope,
@Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope")String scope,
@Parameter(description = "A long value representing TTL (Time to Live) parameter.", required = true)@PathVariable("ttl")Long ttl,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true)@RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);

4
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -251,7 +252,8 @@ public class TenantProfileController extends BaseController {
@GetMapping(value = "/tenantProfiles", params = {"ids"})
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public List<TenantProfile> getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) {
public List<TenantProfile> getTenantProfilesByIds(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("ids") UUID[] ids) {
return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids);
}

5
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -185,7 +186,7 @@ public class WidgetTypeController extends AutoCommitController {
@RequestParam(required = false) Boolean fullSearch,
@Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"}))
@RequestParam(required = false) String deprecatedFilter,
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"timeseries", "latest", "control", "alarm", "static"}))
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"timeseries", "latest", "control", "alarm", "static"})))
@RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List<String> widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
@ -331,7 +332,7 @@ public class WidgetTypeController extends AutoCommitController {
@RequestParam(required = false) Boolean fullSearch,
@Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"}))
@RequestParam(required = false) String deprecatedFilter,
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"timeseries", "latest", "control", "alarm", "static"}))
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(allowableValues = {"timeseries", "latest", "control", "alarm", "static"})))
@RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);

2
application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java

@ -33,7 +33,7 @@ public class EmailNotificationChannel implements NotificationChannel<User, Email
@Override
public void sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
mailService.send(recipient.getTenantId(), null, TbEmail.builder()
mailService.send(ctx.getTenantId(), null, TbEmail.builder()
.to(recipient.getEmail())
.subject(processedTemplate.getSubject())
.body(processedTemplate.getBody())

2
application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java

@ -38,7 +38,7 @@ public class SmsNotificationChannel implements NotificationChannel<User, SmsDeli
throw new RuntimeException("User does not have phone number");
}
smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody());
smsService.sendSms(ctx.getTenantId(), null, new String[]{phone}, processedTemplate.getBody());
}
@Override

11
application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification.rule.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -29,7 +30,6 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import jakarta.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -71,7 +71,14 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache {
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
lock.writeLock().lock(); // locking in case rules for tenant are fetched while evicting
try {
evict(event.getTenantId());
for (var triggerType : NotificationRuleTriggerType.values()) {
String key = key(event.getTenantId(), triggerType);
/*
* temporarily putting empty value because right after tenant deletion
* the rules are still in the db, we don't want them to be fetched
* */
cache.put(key, Collections.emptyList());
}
} finally {
lock.writeLock().unlock();
}

10
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -691,7 +691,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"),
List<EntityKey> entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"),
new EntityKey(EntityKeyType.ENTITY_FIELD, "serviceId"));
EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null);
@ -705,8 +705,12 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
Assert.assertTrue(data.hasNext());
Assert.assertEquals(10, data.getData().size());
data.getData().forEach(entityData -> {
assertThat(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("queueName")).asString().isNotBlank();
assertThat(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("serviceId")).asString().isNotBlank();
String queueName = entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("queueName").getValue();
String serviceId = entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("serviceId").getValue();
assertThat(queueName).isNotBlank();
assertThat(serviceId).isNotBlank();
assertThat(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).isEqualTo(queueName + "_" + serviceId);
});
EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter);

20
dao/src/main/java/org/thingsboard/server/dao/eventsourcing/ActionCause.java

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2024 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.eventsourcing;
public enum ActionCause {
TENANT_DELETION
}

4
dao/src/main/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEvent.java

@ -17,18 +17,20 @@ package org.thingsboard.server.dao.eventsourcing;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@Builder
@Data
public class DeleteEntityEvent<T> {
private final TenantId tenantId;
private final EntityId entityId;
private final T entity;
private final String body;
private final ActionCause cause;
@Builder.Default
private final long ts = System.currentTimeMillis();
}

3
dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient;
import org.thingsboard.server.dao.eventsourcing.ActionCause;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.relation.RelationService;
@ -59,7 +60,7 @@ public class CleanUpService {
if (!skippedEntities.contains(entityType)) {
cleanUpRelatedData(tenantId, entityId);
}
if (entityType == EntityType.USER) {
if (entityType == EntityType.USER && event.getCause() != ActionCause.TENANT_DELETION) {
submitTask(HousekeeperTask.unassignAlarms((User) event.getEntity()));
}
} catch (Throwable e) {

20
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -327,12 +327,22 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
if (!isBootstrapServerUpdateEnable && serverConfig.isBootstrapServerIs()) {
throw new DeviceCredentialsValidationException("Bootstrap config must not include \"Bootstrap Server\". \"Include Bootstrap Server updates\" is " + isBootstrapServerUpdateEnable + ".");
}
if (!serverConfig.isBootstrapServerIs() && (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534)) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be less than 1 and more than 65534!");
}
if (serverConfig.isBootstrapServerIs() && !(serverConfig.getShortServerId() == null || serverConfig.getShortServerId() ==0)) {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null or '0'!");
if (serverConfig.getShortServerId() != null) {
if (serverConfig.isBootstrapServerIs()){
if(serverConfig.getShortServerId() < 0 || serverConfig.getShortServerId() > 65535){
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be in range [0 - 65535]!");
}
} else {
if (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [1 - 65534]!");
}
}
} else {
String serverName = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server";
throw new DeviceCredentialsValidationException(serverName + " ShortServerId must not be null!");
}
String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server";
if (!shortServerIds.add(serverConfig.getShortServerId())) {
throw new DeviceCredentialsValidationException(server + " \"Short server Id\" value = " + serverConfig.getShortServerId() + ". This value must be a unique value for all servers!");

3
dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java

@ -89,10 +89,12 @@ public class EntityKeyMapping {
public static final String OWNER_TYPE_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " +
"then 'TENANT' " +
"else 'CUSTOMER' end";
public static final String QUEUE_STATS_NAME_QUERY = "concat(e.queue_name, '_', e.service_id)";
public static final Map<String, String> ownerPropertiesFunctions = Map.of(
OWNER_NAME, OWNER_NAME_SELECT_QUERY,
OWNER_TYPE, OWNER_TYPE_SELECT_QUERY
);
public static final Map<String, String> queueStatsPropertiesFunctions = Map.of(NAME, QUEUE_STATS_NAME_QUERY);
public static final List<String> typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO);
public static final List<String> widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME);
@ -166,6 +168,7 @@ public class EntityKeyMapping {
propertiesFunctions.put(EntityType.ENTITY_VIEW, ownerPropertiesFunctions);
propertiesFunctions.put(EntityType.USER, ownerPropertiesFunctions);
propertiesFunctions.put(EntityType.DASHBOARD, ownerPropertiesFunctions);
propertiesFunctions.put(EntityType.QUEUE_STATS, queueStatsPropertiesFunctions);
Map<String, String> userEntityAliases = new HashMap<>();
userEntityAliases.put(TITLE, EMAIL);

5
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -39,8 +39,6 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -168,8 +166,10 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
adminSettingsService.deleteAdminSettingsByTenantId(tenantId);
mobileAppSettingsService.deleteByTenantId(tenantId);
notificationSettingsService.deleteNotificationSettings(tenantId);
tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(tenantId).entity(tenant).build());
cleanUpService.removeTenantEntities(tenantId, // don't forget to implement deleteEntity from EntityDaoService when adding entity type to this list
EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE, EntityType.WIDGET_TYPE,
@ -179,7 +179,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_TARGET, EntityType.QUEUE_STATS, EntityType.CUSTOMER
);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(tenantId).entity(tenant).build());
}
@Override

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

@ -53,6 +53,7 @@ import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.eventsourcing.ActionCause;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
@ -293,6 +294,10 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override
@Transactional
public void deleteUser(TenantId tenantId, User user) {
deleteUser(tenantId, user, null);
}
private void deleteUser(TenantId tenantId, User user, ActionCause cause) {
Objects.requireNonNull(user, "User is null");
UserId userId = user.getId();
log.trace("[{}] Executing deleteUser [{}]", tenantId, userId);
@ -307,7 +312,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
eventPublisher.publishEvent(DeleteEntityEvent.builder()
.tenantId(tenantId)
.entityId(userId)
.entity(user).build());
.entity(user)
.cause(cause)
.build());
}
@Override
@ -564,7 +571,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override
protected void removeEntity(TenantId tenantId, User user) {
deleteUser(tenantId, user);
deleteUser(tenantId, user, ActionCause.TENANT_DELETION);
}
};

88
dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java

@ -20,11 +20,18 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceDao;
@ -34,6 +41,8 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.mockito.BDDMockito.willReturn;
@ -42,6 +51,34 @@ import static org.mockito.Mockito.verify;
@SpringBootTest(classes = DeviceProfileDataValidator.class)
class DeviceProfileDataValidatorTest {
private static final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS =
" {\n" +
" \"keyName\": {},\n" +
" \"observe\": [],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
public static final String CLIENT_LWM2M_SETTINGS =
" {\n" +
" \"edrxCycle\": null,\n" +
" \"powerMode\": \"DRX\",\n" +
" \"fwUpdateResource\": null,\n" +
" \"fwUpdateStrategy\": 1,\n" +
" \"psmActivityTimer\": null,\n" +
" \"swUpdateResource\": null,\n" +
" \"swUpdateStrategy\": 1,\n" +
" \"pagingTransmissionWindow\": null,\n" +
" \"clientOnlyObserveAfterConnect\": 1\n" +
" }";
private static final String host = "localhost";
private static final String hostBs = "localhost";
private static final int port = 5685;
private static final int portBs = 5687;
@MockBean
DeviceProfileDao deviceProfileDao;
@MockBean
@ -79,5 +116,56 @@ class DeviceProfileDataValidatorTest {
validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName());
}
@Test
void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() {
Integer shortServerId = 123;
Integer shortServerIdBs = 0;
Lwm2mDeviceProfileTransportConfiguration transportConfiguration =
getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs));
DeviceProfile deviceProfile = getDeviceProfile(transportConfiguration);
validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName());
}
private DeviceProfile getDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName("default");
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
DeviceProfileData data = new DeviceProfileData();
data.setTransportConfiguration(transportConfiguration);
deviceProfile.setProfileData(data);
deviceProfile.setTenantId(tenantId);
return deviceProfile;
}
private Lwm2mDeviceProfileTransportConfiguration getTransportConfiguration(String observeAttr, List<LwM2MBootstrapServerCredential> bootstrapServerCredentials) {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
TelemetryMappingConfiguration observeAttrConfiguration = JacksonUtil.fromString(observeAttr, TelemetryMappingConfiguration.class);
OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(CLIENT_LWM2M_SETTINGS, OtherConfiguration.class);
transportConfiguration.setBootstrapServerUpdateEnable(true);
transportConfiguration.setObserveAttr(observeAttrConfiguration);
transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings);
transportConfiguration.setBootstrap(bootstrapServerCredentials);
return transportConfiguration;
}
private List<LwM2MBootstrapServerCredential> getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs){
List<LwM2MBootstrapServerCredential> bootstrap = new ArrayList<>();
bootstrap.add(getBootstrapServerCredentialNoSec(false, shortServerId, shortServerIdBs));
bootstrap.add(getBootstrapServerCredentialNoSec(true, shortServerId, shortServerIdBs));
return bootstrap;
}
private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap, Integer shortServerId, Integer shortServerIdBs) {
AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential();
bootstrapServerCredential.setServerPublicKey("");
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? portBs : port);
return bootstrapServerCredential;
}
}

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

@ -145,7 +145,7 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
if (!isEmpty(this.alias.filter?.filters)) {
this.alias.filter.filters = this.alias.filter.filters.filter((value, index, self) =>
self.findIndex(v => v.relationType === value.relationType && isEqual(v.entityTypes, value.entityTypes)) === index &&
(value.relationType || value.entityTypes.length)
(value.relationType || value.entityTypes?.length)
);
}
if (this.alias.filter.type) {

1
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.html

@ -20,6 +20,7 @@
<div *ngFor="let serverConfig of serverConfigsFromArray.controls; trackBy: trackByParams; let $index = index;">
<tb-profile-lwm2m-device-config-server
[formControl]="serverConfig"
[isBootstrap]="serverConfig.value.bootstrapServerIs"
(removeServer)="removeServerConfig($event, $index)"
(isTransportWasRunWithBootstrapChange)="updateIsTransportWasRunWithBootstrap($event)">
</tb-profile-lwm2m-device-config-server>

8
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html

@ -32,7 +32,7 @@
</div>
</div>
</mat-panel-title>
<span fxFlex></span>
<span fxFlex="100"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeServer.emit($event)"
@ -57,8 +57,8 @@
<mat-form-field fxFlex>
<mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
<mat-icon *ngIf="!disabled" class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'device-profile.lwm2m.short-id-tooltip' | translate }}">help</mat-icon>
<input matInput type="number" min="1" max="65534" formControlName="shortServerId" required>
matTooltip="{{ (isBootstrap ? 'device-profile.lwm2m.short-id-tooltip-bootstrap': 'device-profile.lwm2m.short-id-tooltip') | translate }}">help</mat-icon>
<input matInput type="number" [min]="shortServerIdMin" [max]="shortServerIdMax" formControlName="shortServerId" required>
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('required')">
{{ 'device-profile.lwm2m.short-id-required' | translate }}
</mat-error>
@ -67,7 +67,7 @@
</mat-error>
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('min') ||
serverFormGroup.get('shortServerId').hasError('max')">
{{ 'device-profile.lwm2m.short-id-range' | translate }}
{{ 'device-profile.lwm2m.short-id-range' | translate : {min: shortServerIdMin, max: shortServerIdMax} }}
</mat-error>
</mat-form-field>
</div>

17
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts

@ -38,6 +38,7 @@ import {
Lwm2mSecurityType,
Lwm2mSecurityTypeTranslationMap
} from '@shared/models/lwm2m-security-config.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-profile-lwm2m-device-config-server',
@ -73,6 +74,13 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
currentSecurityMode = null;
bootstrapDisabled = false;
shortServerIdMin = 1;
shortServerIdMax = 65534;
@Input()
@coerceBoolean()
isBootstrap = false;
@Output()
removeServer = new EventEmitter();
@ -86,13 +94,18 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
}
ngOnInit(): void {
if (this.isBootstrap) {
this.shortServerIdMin = 0;
this.shortServerIdMax = 65535;
}
this.serverFormGroup = this.fb.group({
host: ['', Validators.required],
port: ['', [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern('[0-9]*')]],
securityMode: [Lwm2mSecurityType.NO_SEC],
serverPublicKey: [''],
clientHoldOffTime: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
shortServerId: ['', [Validators.required, Validators.min(1), Validators.max(65534), Validators.pattern('[0-9]*')]],
shortServerId: ['',
[Validators.required, Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax), Validators.pattern('[0-9]*')]],
bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
binding: [''],
lifetime: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
@ -199,7 +212,7 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
if (value !== undefined) {
this.propagateChange(value);
}
}
};
private getLwm2mBootstrapSecurityInfo(securityMode = Lwm2mSecurityType.NO_SEC): Observable<ServerSecurityConfig> {
return this.deviceProfileService.getLwm2mBootstrapSecurityInfoBySecurityType(

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

@ -27,7 +27,7 @@
<div class="tb-form-table-row align-start"
*ngFor="let relationFilterControl of relationFiltersFormArray.controls; let $index = index">
<mat-chip-listbox *ngIf="enableNotOption" class="flex-18 center-stretch" [formControl]="relationFilterControl.get('negate')">
<mat-chip-option color="primary" [value]="true">{{ 'relation.not' | translate}}</mat-chip-option>
<mat-chip-option color="primary" [value]="true">{{ 'relation.not' | translate }}</mat-chip-option>
</mat-chip-listbox>
<tb-relation-type-autocomplete subscriptSizing="dynamic"
class="flex-50" showLabel="false"

11
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts

@ -128,7 +128,16 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
entityTypes: [filter ? filter.entityTypes : []]
});
if (this.enableNotOption) {
formGroup.addControl('negate', this.fb.control(filter ? filter.negate : false));
formGroup.addControl('negate', this.fb.control({value: filter ? filter.negate : false, disabled: true}));
formGroup.get('relationType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
if (value) {
formGroup.get('negate').enable({emitEvent: false});
} else {
formGroup.get('negate').disable({emitEvent: false});
}
});
}
return formGroup;
}

6
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html

@ -102,6 +102,8 @@
<tb-color-settings formControlName="valueColor"
rangeAdvancedMode
gradientAdvancedMode
minValue="0"
maxValue="100"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
@ -114,6 +116,8 @@
<tb-color-settings formControlName="batteryLevelColor"
rangeAdvancedMode
gradientAdvancedMode
minValue="0"
maxValue="100"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
@ -125,6 +129,8 @@
<tb-color-settings formControlName="batteryShapeColor"
rangeAdvancedMode
gradientAdvancedMode
minValue="0"
maxValue="100"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"

8
ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts

@ -238,10 +238,10 @@ export class CanvasDigitalGauge extends BaseGauge {
}
if (options.neonGlowBrightness) {
options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toHexString();
options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toHexString();
options.neonColorValue = tinycolor(options.colorValue).brighten(options.neonGlowBrightness).toHexString();
options.neonColorMinMax = tinycolor(options.colorMinMax).brighten(options.neonGlowBrightness).toHexString();
options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toRgbString();
options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toRgbString();
options.neonColorValue = tinycolor(options.colorValue).brighten(options.neonGlowBrightness).toRgbString();
options.neonColorMinMax = tinycolor(options.colorMinMax).brighten(options.neonGlowBrightness).toRgbString();
}
return options;

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -387,11 +387,11 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
value = deepClone(value);
switch (this.connectorType) {
case ConnectorType.SNMP:
this.clearFromArrayByName("oids");
value.oids.forEach(value => {
this.clearFromArrayByName("oid");
value.oid.forEach(value => {
this.addSNMPoid(value)
})
delete value.oids;
delete value.oid;
break;
case ConnectorType.REQUEST:
this.clearFromArrayByName("httpHeaders");

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts

@ -111,7 +111,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
).subscribe(() => this.updateModel());
this.colorRangeListFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => Promise.resolve().then(() => this.popover?.updatePosition()));
).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0));
}
ngOnDestroy() {
@ -175,7 +175,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
public removeAdvancedRange(index: number) {
(this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index);
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
get advancedRangeFormArray(): UntypedFormArray {
@ -189,7 +189,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
removeRange(index: number) {
this.rangeListFormArray.removeAt(index);
this.colorRangeListFormGroup.markAsDirty();
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
rangeDrop(event: CdkDragDrop<string[]>, range: string) {
@ -212,7 +212,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
const advancedRangeColorsArray = this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray;
const advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]);
advancedRangeColorsArray.push(advancedRangeColorControl);
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
addRange() {
@ -224,7 +224,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
};
this.rangeListFormArray.push(this.colorRangeControl(newRange));
this.colorRangeListFormGroup.markAsDirty();
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -31,7 +31,7 @@ import { deepClone } from '@core/utils';
import { WidgetService } from '@core/http/widget.service';
import { ColorSettingsComponent } from '@home/components/widget/lib/settings/common/color-settings.component';
import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@ -74,11 +74,9 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
gradientAdvancedMode = false;
@Input()
@coerceNumber()
minValue: number;
@Input()
@coerceNumber()
maxValue: number;
colorType = ColorType;
@ -108,7 +106,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
}
);
this.colorSettingsFormGroup.get('type').valueChanges.subscribe(() => {
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
}

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts

@ -33,7 +33,7 @@ import {
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
import { IAliasController } from '@core/api/widget-api.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@ -102,12 +102,10 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD
gradientAdvancedMode = false;
@Input()
@coerceNumber()
minValue = 0;
minValue: number;
@Input()
@coerceNumber()
maxValue = 100;
maxValue: number;
colorType = ColorType;

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.html

@ -36,19 +36,21 @@
<div class="gradient-settings" [ngSwitch]="gradientFormGroup.get('advancedMode').value">
<ng-template [ngSwitchCase]="false">
<div class="gradient" formGroupName="gradient">
<div class="gradient">
<div class="gradient-container gradient-container-start">
<div translate>widgets.color.gradient-start</div>
<div class="start-gradient-container">
<div class="tb-form-row no-border no-padding" fxHide.xs [fxHide]="!minValue?.toString()?.length">
<div class="gradient-text" translate>widgets.color.start-value</div>
<div class="tb-form-row no-border no-padding">
<div class="gradient-text" fxHide.xs translate>widgets.color.start-value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" disabled [(ngModel)]="minValue" [ngModelOptions]="{standalone: true}" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput type="number" formControlName="minValue" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<tb-color-input asBoxInput
formControlName="start">
</tb-color-input>
<ng-container formGroupName="gradient">
<tb-color-input asBoxInput
formControlName="start">
</tb-color-input>
</ng-container>
</div>
</div>
</div>
@ -94,19 +96,21 @@
<mat-icon>add</mat-icon>
</button>
<div class="gradient" formGroupName="gradient">
<div class="gradient">
<div class="gradient-container gradient-container-end">
<div translate>widgets.color.gradient-end</div>
<div class="end-gradient-container">
<div class="tb-form-row no-border no-padding" fxHide.xs [fxHide]="!maxValue?.toString()?.length">
<div class="gradient-text" translate>widgets.color.end-value</div>
<div class="tb-form-row no-border no-padding">
<div class="gradient-text" fxHide.xs translate>widgets.color.end-value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" disabled [(ngModel)]="maxValue" [ngModelOptions]="{standalone: true}" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput type="number" formControlName="maxValue" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<tb-color-input asBoxInput
formControlName="end">
</tb-color-input>
<ng-container formGroupName="gradient">
<tb-color-input asBoxInput
formControlName="end">
</tb-color-input>
</ng-container>
</div>
</div>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss

@ -23,7 +23,7 @@
.gradient-preview {
width: 100%;
padding: 40px 12px 0;
padding: 40px 15px 0;
.gradient-background {
position: relative;
height: 56px;
@ -120,11 +120,13 @@
display: flex;
align-items: center;
justify-content: center;
width: 33px;
width: 38px;
height: 24px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.06);
&-text {
text-overflow: ellipsis;
overflow: hidden;
font-size: 14px;
font-weight: 500;
}

38
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts

@ -31,7 +31,7 @@ import { takeUntil } from 'rxjs/operators';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { IAliasController } from '@core/api/widget-api.models';
import { DomSanitizer } from '@angular/platform-browser';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefinedAndNotNull } from '@core/utils';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@ -71,11 +71,9 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
datasource: Datasource;
@Input()
@coerceNumber()
minValue: number;
@Input()
@coerceNumber()
maxValue: number;
@Input()
@ -111,7 +109,9 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
source: [{type: ValueSourceType.constant}],
color: ['rgba(255, 0, 0, 1)']
})
})
}),
minValue: {value: 0, disabled: isFinite(this.minValue)},
maxValue: {value: 100, disabled: isFinite(this.maxValue)}
});
this.gradientFormGroup.valueChanges.pipe(
@ -119,7 +119,7 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
).subscribe(() => this.updateModel());
this.gradientFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => Promise.resolve().then(() => this.popover?.updatePosition()));
).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0));
}
ngOnDestroy() {
@ -141,6 +141,8 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
writeValue(value: ColorGradientSettings): void {
if (isDefinedAndNotNull(value)) {
this.gradientFormGroup.get('advancedMode').patchValue(value.advancedMode, {emitEvent: false});
this.gradientFormGroup.get('minValue').patchValue(isFinite(value.minValue) ? value.minValue : this.minValue, {emitEvent: false});
this.gradientFormGroup.get('maxValue').patchValue(isFinite(value.maxValue) ? value.maxValue : this.maxValue, {emitEvent: false});
if (value?.gradient?.length) {
this.gradientFormGroup.get('gradient').get('start').patchValue(value.gradient[0], {emitEvent: false});
this.gradientFormGroup.get('gradient').get('end').patchValue(value.gradient[value.gradient.length - 1], {emitEvent: false});
@ -165,10 +167,10 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.advancedGradientListFormArray.value.map((v, i) => this.pointer(shift * (i + 1), i+1, null, true)).join('') +
`<div class="pointer end"></div>`;
} else {
const point = (+this.maxValue - +this.minValue) / (this.gradientListFormArray.value.length + 1);
const min = this.gradientFormGroup.get('minValue').value || 0;
const max = this.gradientFormGroup.get('maxValue').value || 100;
const point = (+max - +min) / (this.gradientListFormArray.value.length + 1);
const shift = 100 / (this.gradientListFormArray.value.length + 1);
const min = isDefinedAndNotNull(this.minValue) ? this.minValue : 0;
const max = isDefinedAndNotNull(this.maxValue) ? this.maxValue : 100;
return `<div class="pointer start"><div class="pointer-value"><span class="pointer-value-text">${min}</span></div></div>` +
this.gradientListFormArray.value.map((v, i) => this.pointer(shift * (i + 1), i+1, point)).join('') +
`<div class="pointer end"><div class="pointer-value"><span class="pointer-value-text">${max}</span></div></div>`;
@ -180,7 +182,8 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
return `<div class="pointer" style="left: ${shift}%"></div>`;
} else {
return `<div class="pointer" style="left: ${shift}%">` +
`<div class="pointer-value"><span class="pointer-value-text">${Math.floor(+this.minValue + (value * index))}</span></div></div>`;
`<div class="pointer-value"><span class="pointer-value-text">` +
`${Math.floor(+this.gradientFormGroup.get('minValue').value + (value * index))}</span></div></div>`;
}
}
@ -233,7 +236,7 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.removeAt(index);
}
this.gradientFormGroup.markAsDirty();
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
gradientDrop(event: CdkDragDrop<string[]>, advanced = false) {
@ -252,19 +255,18 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)'));
}
this.gradientFormGroup.markAsDirty();
Promise.resolve().then(() => this.popover?.updatePosition());
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
updateModel() {
const gradient = this.gradientFormGroup.getRawValue();
this.propagateChange(
{
advancedMode: this.gradientFormGroup.value.advancedMode,
gradient: [this.gradientFormGroup.value.gradient.start,
...this.gradientFormGroup.value.gradient.gradientList.map(item => item.color),
this.gradientFormGroup.value.gradient.end],
gradientAdvanced: [this.gradientFormGroup.value.gradientAdvanced.start,
...this.gradientFormGroup.value.gradientAdvanced.gradientList,
this.gradientFormGroup.value.gradientAdvanced.end]
advancedMode: gradient.advancedMode,
gradient: [gradient.gradient.start, ...gradient.gradient.gradientList.map(item => item.color), gradient.gradient.end],
gradientAdvanced: [gradient.gradientAdvanced.start, ...gradient.gradientAdvanced.gradientList, gradient.gradientAdvanced.end],
minValue: gradient.minValue,
maxValue: gradient.maxValue
}
);
}

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html

@ -51,6 +51,8 @@
<tb-color-settings formControlName="valueColor"
rangeAdvancedMode
gradientAdvancedMode
minValue="0"
maxValue="100"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
@ -63,6 +65,8 @@
<tb-color-settings formControlName="batteryLevelColor"
rangeAdvancedMode
gradientAdvancedMode
minValue="0"
maxValue="100"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
@ -74,6 +78,8 @@
<tb-color-settings formControlName="batteryShapeColor"
rangeAdvancedMode
gradientAdvancedMode
minValue="0"
maxValue="100"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"

19
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -95,9 +95,10 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
branding: false
};
private propagateChange = (v: any) => { };
private propagateChange = null;
private readonly destroy$ = new Subject<void>();
private expendedBlocks: NotificationDeliveryMethod[];
private propagateChangePending = false;
constructor(private fb: FormBuilder,
private translate: TranslateService) {
@ -105,7 +106,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
this.templateConfigurationForm.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.propagateChange(value);
this.updateModel(value);
});
}
@ -129,6 +130,12 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
Promise.resolve().then(() => {
this.templateConfigurationForm.updateValueAndValidity();
});
}
}
registerOnTouched(fn: any): void {
@ -164,6 +171,14 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
return this.expendedBlocks.includes(name);
}
private updateModel(value: Partial<DeliveryMethodsTemplates>) {
if (this.propagateChange) {
this.propagateChange(value);
} else {
this.propagateChangePending = true;
}
}
private updateExpandedForm() {
this.expendedBlocks = [];
Object.keys(this.templateConfigurationForm.controls).forEach((name: NotificationDeliveryMethod) => {

2
ui-ngx/src/app/shared/models/limited-api.models.ts

@ -46,7 +46,7 @@ export const LimitedApiTranslationMap = new Map<LimitedApi, string>(
[LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'],
[LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device'],
[LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY, 'api-limit.transport-messages-per-gateway'],
[LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE, 'api-limit.transport-messages-per-gateway_device'],
[LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE, 'api-limit.transport-messages-per-gateway-device'],
[LimitedApi.EDGE_EVENTS, 'api-limit.edge-events'],
[LimitedApi.EDGE_EVENTS_PER_EDGE, 'api-limit.edge-events-per-edge'],
[LimitedApi.EDGE_UPLINK_MESSAGES, 'api-limit.edge-uplink-messages'],

2
ui-ngx/src/assets/locale/locale.constant-ar_AE.json

@ -2000,7 +2000,7 @@
"short-id": "معرف الخادم القصير",
"short-id-tooltip": "معرف الخادم القصير. يُستخدم كرابط لربط نموذج كائن الخادم.\nهذا المعرف يُميز بشكل فريد كل خادم LwM2M المُعد للعميل LwM2M.\nيجب ضبط المورد عندما يكون مورد الخادم التمهيدي بقيمة 'false'.\nلا يجب استخدام قيم المعرف ID:0 و ID:65535 لتحديد خادم LwM2M.",
"short-id-required": "معرف الخادم القصير مطلوب.",
"short-id-range": "يجب أن يكون معرف الخادم القصير ضمن نطاق من 1 إلى 65534.",
"short-id-range": "يجب أن يكون معرف الخادم القصير ضمن نطاق من {{ min }} إلى {{ max }}.",
"short-id-pattern": "يجب أن يكون معرف الخادم القصير عددًا صحيحًا موجبًا.",
"lifetime": "مدة تسجيل العميل",
"lifetime-required": "مدة تسجيل العميل مطلوبة.",

2
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -1632,7 +1632,7 @@
"short-id": "Identificador curt del servidor",
"short-id-tooltip": "Identificador curt del servidor. S'utilitza com a enllaç a la instància d'objecte del servidor associada.\nAquest identificador identifica de manera única cada servidor LwM2M configurat per al client LwM2M.\nEl recurs S'HA d'establir quan el recurs del servidor Bootstrap té un valor 'fals'.\nEls valors ID:0 i ID :65535 NO S'HAN d'utilitzar per identificar el servidor LwM2M.",
"short-id-required": "Cal identificador curt del servidor.",
"short-id-range": "L'identificador de servidor curt hauria d'estar entre 1 i 65534.",
"short-id-range": "L'identificador de servidor curt hauria d'estar entre {{ min }} i {{ max }}.",
"short-id-pattern": "L'identificador de servidor curt ha de ser un nombre enter positiu.",
"lifetime": "Vida útil del registre del client",
"lifetime-required": "Cal vida útil del registre del client.",

2
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -1247,7 +1247,7 @@
"mode": "Režim konfigurace bezpečnosti",
"short-id": "Krátké ID",
"short-id-required": "Krátké ID je povinné.",
"short-id-range": "Krátké ID by mělo být v rozsahu od 1 do 65534.",
"short-id-range": "Krátké ID by mělo být v rozsahu od {{ min }} do {{ max }}.",
"short-id-pattern": "Krátké ID musí být kladné celé číslo.",
"lifetime": "Doba platnosti registrace klienta",
"lifetime-required": "Doba platnosti registrace klienta je povinná.",

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

@ -1867,8 +1867,9 @@
"server": "Server",
"short-id": "Short server ID",
"short-id-tooltip": "Server short Id. Used as link to associate server Object Instance.\nThis identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.\nResource MUST be set when the Bootstrap-Server Resource has a value of 'false'.\nThe values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.",
"short-id-tooltip-bootstrap": "Server short Id. Used as link to associate server Object Instance.\nThis identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.\nResource MUST be set when the Bootstrap-Server Resource has a value of 'false'.",
"short-id-required": "Short server ID is required.",
"short-id-range": "Short server ID should be in a range from 1 to 65534.",
"short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.",
"short-id-pattern": "Short server ID must be a positive integer.",
"lifetime": "Client registration lifetime",
"lifetime-required": "Client registration lifetime is required.",
@ -2842,6 +2843,7 @@
"enable-remote-logging": "Enable remote logging",
"ellipsis-chips-text": "+ {{count}} more",
"launch-gateway": "Launch gateway",
"launch-command": "Launch command",
"launch-docker-compose": "Start the gateway using the following command in the terminal from folder with docker-compose.yml file",
"logs-configuration": "Logs configuration",
"create-new-gateway": "Create a new gateway",

2
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -1757,7 +1757,7 @@
"short-id": "Short server ID",
"short-id-tooltip": "Id corto del servidor. Usado como enlace para asociar las instancias de objetos del servidor.\nEste identificador sirve para identificar únicamente cada servidor LwM2M configurado para el cliente LwM2M.\nLos recursos DEBEN ser ajustados cuando el servidor Bootstrap tenga un valor de 'false'.\nLos valores ID:0 and ID:65535 NO DEBEN ser usados para identificar al servidor LwM2M.",
"short-id-required": "Se requiere Short server ID.",
"short-id-range": "Short server ID debe estar en un rango de 1 a 65534.",
"short-id-range": "Short server ID debe estar en un rango de {{ min }} a {{ max }}.",
"short-id-pattern": "Short server ID debe ser un número entero positivo.",
"lifetime": "Ciclo de vida registro de cliente (Registration Lifetime)",
"lifetime-required": "Se requiere ciclo de vida.",

2
ui-ngx/src/assets/locale/locale.constant-lt_LT.json

@ -1970,7 +1970,7 @@
"short-id": "Short server ID",
"short-id-tooltip": "Server short Id. Used as link to associate server Object Instance.\nThis identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.\nResource MUST be set when the Bootstrap-Server Resource has a value of 'false'.\nThe values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.",
"short-id-required": "Short server ID is required.",
"short-id-range": "Short server ID should be in a range from 1 to 65534.",
"short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.",
"short-id-pattern": "Short server ID must be a positive integer.",
"lifetime": "Client registration lifetime",
"lifetime-required": "Client registration lifetime is required.",

2
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -1857,7 +1857,7 @@
"short-id": "Korte server-ID",
"short-id-tooltip": "Server-id kort Id. Wordt gebruikt als koppeling naar het Object-exemplaar van de koppeling.\nDeze id identificeert elke LwM2M-server die is geconfigureerd voor de LwM2M-client op unieke wijze.\nResource MOET worden ingesteld wanneer de Bootstrap-Server Resource een waarde van 'false' heeft.\nDe waarden ID:0 en ID:65535 mogen NIET worden gebruikt voor het identificeren van de LwM2M-server.",
"short-id-required": "Een korte server-ID is vereist.",
"short-id-range": "Korte server-ID moet tussen 1 en 65534 liggen.",
"short-id-range": "Korte server-ID moet tussen {{ min }} en {{ max }} liggen.",
"short-id-pattern": "Korte server-ID moet een positief geheel getal zijn.",
"lifetime": "Levensduur klantregistratie",
"lifetime-required": "Levenslange registratie van klanten is vereist.",

2
ui-ngx/src/assets/locale/locale.constant-pl_PL.json

@ -1976,7 +1976,7 @@
"short-id": "Krótki identyfikator serwera",
"short-id-tooltip": "Krótki identyfikator serwera. ",
"short-id-required": "Krótki identyfikator serwera jest wymagany.",
"short-id-range": "Krótki identyfikator serwera powinien mieścić się w zakresie od 1 do 65534.",
"short-id-range": "Krótki identyfikator serwera powinien mieścić się w zakresie od {{ min }} do {{ max }}.",
"short-id-pattern": "Krótki identyfikator serwera musi być dodatnią liczbą całkowitą.",
"lifetime": "Okres rejestracji klienta",
"lifetime-required": "Okres rejestracji klienta jest wymagany.",

2
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -1260,7 +1260,7 @@
"mode": "Güvenlik yapılandırma modu",
"short-id": "Kısa ID",
"short-id-required": "Kısa ID gerekli.",
"short-id-range": "Kısa ID 1 ile 65534 aralığında olmalıdır.",
"short-id-range": "Kısa ID {{ min }} ile {{ max }} aralığında olmalıdır.",
"short-id-pattern": "Kısa ID pozitif bir tam sayı olmalıdır.",
"lifetime": "İstemci kayıt ömrü",
"lifetime-required": "İstemci kayıt ömrü gerekli.",

2
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -1794,7 +1794,7 @@
"short-id": "服务器ID",
"short-id-tooltip": "服务器ID用作关联服务器对象实例的链接。",
"short-id-required": "服务器ID必填。",
"short-id-range": "服务器ID应在1到65534范围内。",
"short-id-range": "服务器ID应在{{ min }}到{{ max }}范围内。",
"short-id-pattern": "服务器ID必须是一个正整数。",
"lifetime": "客户端注册生命周期",
"lifetime-required": "客户端注册生命周期必填。",

2
ui-ngx/src/assets/locale/locale.constant-zh_TW.json

@ -1432,7 +1432,7 @@
"short-id": "短伺服器ID",
"short-id-tooltip": "短伺服器ID。用作關聯伺服器物件實例的鏈接。 \n此標識符唯一標識為LwM2M伺服器客戶端配置的每個LwM2M 伺服器。\n當引導伺服器資源的值為'false'時,必須設置資源。\nID:0值和ID:65535值不得用於識別LwM2M伺服器。.",
"short-id-required": "短伺服器ID必填",
"short-id-range": "短伺服器ID長度需介於1到65534。",
"short-id-range": "短伺服器ID長度需介於{{ min }}到{{ max }}。",
"short-id-pattern": "短伺服器ID需為正整數",
"lifetime": "客戶登記使用期限",
"lifetime-required": "客戶登記使用期限必填",

Loading…
Cancel
Save