Browse Source

UI: Merge and resolve

pull/8931/head
Artem Dzhereleiko 3 years ago
parent
commit
dc2557b508
  1. 42
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 2
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  3. 2
      application/src/main/data/upgrade/3.5.1/schema_update.sql
  4. 5
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  5. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  6. 107
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  7. 6
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  8. 86
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  9. 38
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  10. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  11. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java
  13. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  14. 8
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  15. 33
      application/src/main/resources/thingsboard.yml
  16. 96
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  17. 340
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  18. 32
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  19. 160
      application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
  20. 29
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java
  21. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  22. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  23. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  24. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  25. 4
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  26. 4
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  27. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  28. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  29. 6
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  30. 42
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java
  31. 189
      common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java
  32. 7
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  33. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java
  34. 73
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java
  35. 34
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java
  36. 26
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java
  37. 268
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  38. 9
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java
  39. 31
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  40. 4
      dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
  41. 1
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  42. 4
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  43. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  44. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  45. 19
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
  46. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
  47. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  48. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  49. 16
      dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
  50. 123
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  51. 8
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  52. 10
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  53. 2
      dao/src/main/resources/sql/schema-entities.sql
  54. 40
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  55. 25
      dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
  56. 1
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  57. 2
      pom.xml
  58. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  59. 1
      transport/coap/src/main/resources/tb-coap-transport.yml
  60. 1
      transport/http/src/main/resources/tb-http-transport.yml
  61. 1
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  62. 1
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  63. 1
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  64. 51
      ui-ngx/src/app/app.component.ts
  65. 19
      ui-ngx/src/app/core/auth/auth.actions.ts
  66. 14
      ui-ngx/src/app/core/auth/auth.effects.ts
  67. 15
      ui-ngx/src/app/core/auth/auth.reducer.ts
  68. 6
      ui-ngx/src/app/core/auth/auth.selectors.ts
  69. 2
      ui-ngx/src/app/core/auth/auth.service.ts
  70. 10
      ui-ngx/src/app/core/http/device.service.ts
  71. 2
      ui-ngx/src/app/core/public-api.ts
  72. 6
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  73. 2
      ui-ngx/src/app/core/services/menu.models.ts
  74. 52
      ui-ngx/src/app/core/services/menu.service.ts
  75. 50
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  76. 24
      ui-ngx/src/app/core/utils.ts
  77. 19
      ui-ngx/src/app/core/ws/public-api.ts
  78. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  79. 22
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html
  80. 20
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss
  81. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  82. 81
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  83. 38
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss
  84. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  85. 25
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  86. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  87. 37
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss
  88. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  89. 48
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  90. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  91. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  92. 13
      ui-ngx/src/app/modules/home/components/details-panel.component.scss
  93. 7
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  94. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  95. 30
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  96. 37
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  97. 2
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html
  98. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  99. 4
      ui-ngx/src/app/modules/home/components/router-tabs.component.html
  100. 2
      ui-ngx/src/app/modules/home/components/router-tabs.component.scss

42
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -498,7 +498,7 @@
"settingsSchema": "",
"dataKeySettingsSchema": "{}",
"settingsDirective": "tb-update-json-attribute-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetMode\":\"ATTRIBUTE\",\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
}
]

2
application/src/main/data/upgrade/3.5.1/schema_update.sql

@ -122,3 +122,5 @@ ALTER TABLE resource
UPDATE resource
SET etag = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null;
ALTER TABLE notification_request ALTER COLUMN info SET DATA TYPE varchar(1000000);

5
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -81,6 +81,7 @@ public class ThingsboardSecurityConfiguration {
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download";
@Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@ -150,7 +151,8 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT));
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT,
DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@ -228,6 +230,7 @@ public class ThingsboardSecurityConfiguration {
.antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
.antMatchers(DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
.antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and()
.authorizeRequests()

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

@ -24,6 +24,7 @@ public class ControllerConstants {
protected static final String CUSTOMER_ID = "customerId";
protected static final String TENANT_ID = "tenantId";
protected static final String DEVICE_ID = "deviceId";
protected static final String PROTOCOL = "protocol";
protected static final String EDGE_ID = "edgeId";
protected static final String RPC_ID = "rpcId";
protected static final String ENTITY_ID = "entityId";
@ -34,6 +35,7 @@ public class ControllerConstants {
protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String PROTOCOL_PARAM_DESCRIPTION = "A string value representing the device connectivity protocol. Possible values: 'mqtt', 'mqtts', 'http', 'https', 'coap', 'coaps'";
protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";

107
application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java

@ -0,0 +1,107 @@
/**
* 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.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.dao.device.DeviceConnectivityService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URISyntaxException;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL;
import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class DeviceConnectivityController extends BaseController {
private final DeviceConnectivityService deviceConnectivityService;
private final SystemSecurityService systemSecurityService;
@ApiOperation(value = "Get commands to publish device telemetry (getDevicePublishTelemetryCommands)",
notes = "Fetch the list of commands to publish device telemetry based on device profile " +
"If the user has the authority of 'Tenant Administrator', the server checks that the device is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the device is assigned to the same customer. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK",
examples = @io.swagger.annotations.Example(
value = {
@io.swagger.annotations.ExampleProperty(
mediaType = "application/json",
value = "{\"http\":\"curl -v -X POST http://localhost:8080/api/v1/0ySs4FTOn5WU15XLmal8/telemetry --header Content-Type:application/json --data {temperature:25}\"," +
"\"mqtt\":\"mosquitto_pub -d -q 1 -h localhost -t v1/devices/me/telemetry -i myClient1 -u myUsername1 -P myPassword -m {temperature:25}\"," +
"\"coap\":\"coap-client -m POST coap://localhost:5683/api/v1/0ySs4FTOn5WU15XLmal8/telemetry -t json -e {temperature:25}\"}")}))})
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device-connectivity/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public JsonNode getDevicePublishTelemetryCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException {
checkParameter(DEVICE_ID, strDeviceId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS);
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device);
}
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.")
@RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadServerCertificate(@ApiParam(value = PROTOCOL_PARAM_DESCRIPTION)
@PathVariable(PROTOCOL) String protocol) throws ThingsboardException, IOException {
checkParameter(PROTOCOL, protocol);
var pemCert =
checkNotNull(deviceConnectivityService.getPemCertFile(protocol), protocol + " pem cert file is not found!");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + PEM_CERT_FILE_NAME)
.header("x-filename", PEM_CERT_FILE_NAME)
.contentLength(pemCert.contentLength())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(pemCert);
}
}

6
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -117,8 +117,8 @@ public class EntityRelationController extends BaseController {
tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete Relations (deleteRelations)",
notes = "Deletes all the relation (both 'from' and 'to' direction) for the specified entity. " +
@ApiOperation(value = "Delete common relations (deleteCommonRelations)",
notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"})
@ -129,7 +129,7 @@ public class EntityRelationController extends BaseController {
checkParameter("entityType", strType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId);
checkEntityId(entityId, Operation.WRITE);
tbEntityRelationService.deleteRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser());
tbEntityRelationService.deleteCommonRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser());
}
@ApiOperation(value = "Get Relation (getRelation)",

86
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -221,12 +221,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
.build();
}
private class SessionMetaData implements SendHandler {
class SessionMetaData implements SendHandler {
private final WebSocketSession session;
private final RemoteEndpoint.Async asyncRemote;
private final WebSocketSessionRef sessionRef;
private final AtomicBoolean isSending = new AtomicBoolean(false);
final AtomicBoolean isSending = new AtomicBoolean(false);
private final Queue<TbWebSocketMsg<?>> msgQueue;
private volatile long lastActivityTime;
@ -241,7 +241,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
this.lastActivityTime = System.currentTimeMillis();
}
synchronized void sendPing(long currentTime) {
void sendPing(long currentTime) {
try {
long timeSinceLastActivity = currentTime - lastActivityTime;
if (timeSinceLastActivity >= pingTimeout) {
@ -256,37 +256,38 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
private void closeSession(CloseStatus reason) {
void closeSession(CloseStatus reason) {
try {
close(this.sessionRef, reason);
} catch (IOException ioe) {
log.trace("[{}] Session transport error", session.getId(), ioe);
} finally {
msgQueue.clear();
}
}
synchronized void processPongMessage(long currentTime) {
void processPongMessage(long currentTime) {
lastActivityTime = currentTime;
}
synchronized void sendMsg(String msg) {
void sendMsg(String msg) {
sendMsg(new TbWebSocketTextMsg(msg));
}
synchronized void sendMsg(TbWebSocketMsg<?> msg) {
if (isSending.compareAndSet(false, true)) {
sendMsgInternal(msg);
} else {
try {
msgQueue.add(msg);
} catch (RuntimeException e) {
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e);
} else {
log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId());
}
closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!"));
void sendMsg(TbWebSocketMsg<?> msg) {
try {
msgQueue.add(msg);
} catch (RuntimeException e) {
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e);
} else {
log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId());
}
closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!"));
return;
}
processNextMsg();
}
private void sendMsgInternal(TbWebSocketMsg<?> msg) {
@ -294,9 +295,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
if (TbWebSocketMsgType.TEXT.equals(msg.getType())) {
TbWebSocketTextMsg textMsg = (TbWebSocketTextMsg) msg;
this.asyncRemote.sendText(textMsg.getMsg(), this);
// isSending status will be reset in the onResult method by call back
} else {
TbWebSocketPingMsg pingMsg = (TbWebSocketPingMsg) msg;
this.asyncRemote.sendPing(pingMsg.getMsg());
this.asyncRemote.sendPing(pingMsg.getMsg()); // blocking call
isSending.set(false);
processNextMsg();
}
} catch (Exception e) {
@ -310,12 +313,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
if (!result.isOK()) {
log.trace("[{}] Failed to send msg", session.getId(), result.getException());
closeSession(CloseStatus.SESSION_NOT_RELIABLE);
} else {
processNextMsg();
return;
}
isSending.set(false);
processNextMsg();
}
private void processNextMsg() {
if (msgQueue.isEmpty() || !isSending.compareAndSet(false, true)) {
return;
}
TbWebSocketMsg<?> msg = msgQueue.poll();
if (msg != null) {
sendMsgInternal(msg);
@ -392,19 +400,21 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
if (tenantProfileConfiguration == null) {
return true;
}
boolean limitAllowed;
String sessionId = session.getId();
if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) {
Set<String> tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (tenantSessions) {
if (tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant()) {
limitAllowed = tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant();
if (limitAllowed) {
tenantSessions.add(sessionId);
} else {
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!"));
return false;
}
}
}
@ -412,42 +422,48 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
if (tenantProfileConfiguration.getMaxWsSessionsPerCustomer() > 0) {
Set<String> customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
synchronized (customerSessions) {
if (customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer()) {
limitAllowed = customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer();
if (limitAllowed) {
customerSessions.add(sessionId);
} else {
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached"));
return false;
}
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0
&& UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
synchronized (regularUserSessions) {
if (regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser()) {
limitAllowed = regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser();
if (limitAllowed) {
regularUserSessions.add(sessionId);
} else {
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached"));
return false;
}
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
&& UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser()) {
limitAllowed = publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser();
if (limitAllowed) {
publicUserSessions.add(sessionId);
} else {
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
return false;
}
}
}
}

38
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -36,9 +37,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Service
@AllArgsConstructor
@ -210,6 +215,39 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
return alarmInfo;
}
@Override
public void unassignUserAlarms(TenantId tenantId, User user, long unassignTs) {
AlarmQueryV2 alarmQuery = AlarmQueryV2.builder().assigneeId(user.getId()).pageLink(new TimePageLink(Integer.MAX_VALUE)).build();
try {
List<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, alarmQuery).get(30, TimeUnit.SECONDS).getData();
for (AlarmInfo alarm : alarms) {
AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarm.getId(), getOrDefault(unassignTs));
if (!result.isSuccessful()) {
continue;
}
if (result.isModified()) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned because user %s - was deleted",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ASSIGN"))
.build();
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user);
}
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
}
@Override
public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId();

3
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
public interface TbAlarmService {
@ -37,5 +38,7 @@ public interface TbAlarmService {
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
void unassignUserAlarms(TenantId tenantId, User user, long unassignTs);
Boolean delete(Alarm alarm, User user);
}

4
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java

@ -72,9 +72,9 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
}
@Override
public void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException {
public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException {
try {
relationService.deleteEntityRelations(tenantId, entityId);
relationService.deleteEntityCommonRelations(tenantId, entityId);
notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user);
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, entityId, null, customerId,

2
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java

@ -28,6 +28,6 @@ public interface TbEntityRelationService {
void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException;
void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException;
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
@ -43,6 +44,7 @@ import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATT
public class DefaultUserService extends AbstractTbEntityService implements TbUserService {
private final UserService userService;
private final TbAlarmService tbAlarmService;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
@ -80,6 +82,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
UserId userId = tbUser.getId();
try {
tbAlarmService.unassignUserAlarms(tbUser.getTenantId(), tbUser, System.currentTimeMillis());
userService.deleteUser(tenantId, userId);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser,
user, ActionType.DELETED, true, null, customerId.toString());

8
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -27,14 +27,18 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -55,9 +59,11 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource
public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService {
private final ResourceService resourceService;
private final WidgetTypeService widgetTypeService;
public DefaultTbResourceService(ResourceService resourceService) {
public DefaultTbResourceService(ResourceService resourceService, WidgetTypeService widgetTypeService) {
this.resourceService = resourceService;
this.widgetTypeService = widgetTypeService;
}
@Override

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

@ -96,6 +96,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
cluster:
stats:
@ -249,7 +250,8 @@ cassandra:
concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
dispatcher_threads: "${CASSANDRA_QUERY_DISPATCHER_THREADS:2}"
callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}"
callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}" # Buffered rate executor (read, write), for managing I/O rate. See "nosql-*-callback" threads in JMX
result_processing_threads: "${CASSANDRA_QUERY_RESULT_PROCESSING_THREADS:50}" # Result set transformer and processing. See "cassandra-callback" threads in JMX
poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}"
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data types values except target to null for the same ts on save
@ -985,6 +987,35 @@ transport:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# Device connectivity properties to publish telemetry
device:
connectivity:
http:
enabled: "${DEVICE_CONNECTIVITY_HTTP_ENABLED:true}"
host: "${DEVICE_CONNECTIVITY_HTTP_HOST:}"
port: "${DEVICE_CONNECTIVITY_HTTP_PORT:8080}"
https:
enabled: "${DEVICE_CONNECTIVITY_HTTPS_ENABLED:false}"
host: "${DEVICE_CONNECTIVITY_HTTPS_HOST:}"
port: "${DEVICE_CONNECTIVITY_HTTPS_PORT:443}"
mqtt:
enabled: "${DEVICE_CONNECTIVITY_MQTT_ENABLED:true}"
host: "${DEVICE_CONNECTIVITY_MQTT_HOST:}"
port: "${DEVICE_CONNECTIVITY_MQTT_PORT:1883}"
mqtts:
enabled: "${DEVICE_CONNECTIVITY_MQTTS_ENABLED:false}"
host: "${DEVICE_CONNECTIVITY_MQTTS_HOST:}"
port: "${DEVICE_CONNECTIVITY_MQTTS_PORT:8883}"
pem_cert_file: "${DEVICE_CONNECTIVITY_MQTT_SSL_PEM_CERT:mqttserver.pem}"
coap:
enabled: "${DEVICE_CONNECTIVITY_COAP_ENABLED:true}"
host: "${DEVICE_CONNECTIVITY_COAP_HOST:}"
port: "${DEVICE_CONNECTIVITY_COAP_PORT:5683}"
coaps:
enabled: "${DEVICE_CONNECTIVITY_COAPS_ENABLED:false}"
host: "${DEVICE_CONNECTIVITY_COAPS_HOST:}"
port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}"
# Edges parameters
edges:
enabled: "${EDGES_ENABLED:true}"

96
application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java

@ -32,6 +32,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -529,6 +531,100 @@ public class AlarmControllerTest extends AbstractControllerTest {
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED);
}
@Test
public void testUnassignTenantUserAlarmOnUserRemoving() throws Exception {
loginDifferentTenant();
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("tenantForAssign@thingsboard.org");
User savedUser = createUser(user, "password");
Device device = createDevice("Different tenant device", "default", "differentTenantTest");
Alarm alarm = Alarm.builder()
.type(TEST_ALARM_TYPE)
.tenantId(savedDifferentTenant.getId())
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(alarm);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
beforeAssignmentTs = System.currentTimeMillis();
Mockito.reset(tbClusterService, auditLogService);
loginSysAdmin();
doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk());
loginDifferentTenant();
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
}
@Test
public void testUnassignAlarmOnUserRemoving() throws Exception {
loginDifferentTenant();
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("tenantForAssign@thingsboard.org");
User savedUser = createUser(user, "password");
Device device = createDevice("Different tenant device", "default", "differentTenantTest");
Alarm alarm = Alarm.builder()
.type(TEST_ALARM_TYPE)
.tenantId(savedDifferentTenant.getId())
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(alarm);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
beforeAssignmentTs = System.currentTimeMillis();
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
}
@Test
public void testFindAlarmsViaCustomerUser() throws Exception {
loginCustomerUser();

340
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -0,0 +1,340 @@
/**
* 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.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
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.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
@TestPropertySource(properties = {
"device.connectivity.https.enabled=true",
"device.connectivity.mqtts.enabled=true",
"device.connectivity.coaps.enabled=true",
})
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<Device>> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() {
};
private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic";
private static final String CHECK_DOCUMENTATION = "Check documentation";
ListeningExecutorService executor;
private Tenant savedTenant;
private User tenantAdmin;
private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
DeviceProfile mqttProfile = new DeviceProfile();
mqttProfile.setName("Mqtt device profile");
mqttProfile.setType(DeviceProfileType.DEFAULT);
mqttProfile.setTransportType(DeviceTransportType.MQTT);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration();
transportConfiguration.setDeviceTelemetryTopic(DEVICE_TELEMETRY_TOPIC);
deviceProfileData.setTransportConfiguration(transportConfiguration);
mqttProfile.setProfileData(deviceProfileData);
mqttProfile.setDefault(false);
mqttProfile.setDefaultRuleChainId(null);
mqttDeviceProfileId = doPost("/api/deviceProfile", mqttProfile, DeviceProfile.class).getId();
DeviceProfile coapProfile = new DeviceProfile();
coapProfile.setName("Coap device profile");
coapProfile.setType(DeviceProfileType.DEFAULT);
coapProfile.setTransportType(DeviceTransportType.COAP);
DeviceProfileData deviceProfileData2 = new DeviceProfileData();
deviceProfileData2.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData2.setTransportConfiguration(new CoapDeviceProfileTransportConfiguration());
coapProfile.setProfileData(deviceProfileData);
coapProfile.setDefault(false);
coapProfile.setDefaultRuleChainId(null);
coapDeviceProfileId = doPost("/api/deviceProfile", coapProfile, DeviceProfile.class).getId();
}
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(3);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " +
"-u %s -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " +
"-t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry " +
"-t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry" +
" -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForMqttDeviceWithAccessToken() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(mqttDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " +
"-t %s -u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -u %s -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -u %s -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForDeviceWithMqttBasicCreds() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(mqttDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
credentials.setCredentialsId(null);
credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC);
BasicMqttCredentials basicMqttCredentials = new BasicMqttCredentials();
String clientId = "testClientId";
String userName = "testUsername";
String password = "testPassword";
basicMqttCredentials.setClientId(clientId);
basicMqttCredentials.setUserName(userName);
basicMqttCredentials.setPassword(password);
credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials));
doPost("/api/device/credentials", credentials)
.andExpect(status().isOk());
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " +
"-t %s -i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
}
@Test
public void testFetchPublishTelemetryCommandsForDeviceWithX509Creds() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(mqttDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
credentials.setCredentialsId(null);
credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
credentials.setCredentialsValue("testValue");
doPost("/api/device/credentials", credentials)
.andExpect(status().isOk());
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
assertThat(commands.get(MQTT).get(MQTTS).asText()).isEqualTo(CHECK_DOCUMENTATION);
assertThat(commands.get(MQTT).get(DOCKER)).isNull();
}
@Test
public void testFetchPublishTelemetryCommandsForCoapDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(coapDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
JsonNode linuxCommands = commands.get(COAP);
assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForCoapDeviceWithX509Creds() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(coapDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
credentials.setCredentialsId(null);
credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
credentials.setCredentialsValue("testValue");
doPost("/api/device/credentials", credentials)
.andExpect(status().isOk());
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION);
}
}

32
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -38,6 +38,8 @@ import org.thingsboard.server.common.data.audit.ActionType;
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.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -216,6 +218,36 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr))));
}
@Test
public void testShoudNotDeleteTbResourceIfAssignedToWidgetType() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
Mockito.reset(tbClusterService, auditLogService);
String resourceIdStr = savedResource.getId().getId().toString();
//create widget type
WidgetsBundle widgetsBundle = new WidgetsBundle();
widgetsBundle.setTitle("My widgets bundle");
WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":{\"entityType\":\"TB_RESOURCE\",\"id\":\"%s\"},\"isModule\":true}]}", savedResource.getId()), JsonNode.class));
doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
doDelete("/api/resource/" + resourceIdStr)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Following widget types uses current resource: ["
+ widgetType .getName()+ "]")));
}
@Test
public void testFindTenantTbResources() throws Exception {

160
application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java

@ -0,0 +1,160 @@
/**
* 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.controller.plugin;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import javax.websocket.RemoteEndpoint;
import javax.websocket.SendHandler;
import javax.websocket.SendResult;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@Slf4j
class TbWebSocketHandlerTest {
TbWebSocketHandler wsHandler;
NativeWebSocketSession session;
Session nativeSession;
RemoteEndpoint.Async asyncRemote;
WebSocketSessionRef sessionRef;
int maxMsgQueuePerSession;
TbWebSocketHandler.SessionMetaData sendHandler;
ExecutorService executor;
@BeforeEach
void setUp() throws IOException {
maxMsgQueuePerSession = 100;
executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getSimpleName()));
wsHandler = spy(new TbWebSocketHandler());
willDoNothing().given(wsHandler).close(any(), any());
session = mock(NativeWebSocketSession.class);
nativeSession = mock(Session.class);
willReturn(nativeSession).given(session).getNativeSession(Session.class);
asyncRemote = mock(RemoteEndpoint.Async.class);
willReturn(asyncRemote).given(nativeSession).getAsyncRemote();
sessionRef = mock(WebSocketSessionRef.class, Mockito.RETURNS_DEEP_STUBS); //prevent NPE on logs
sendHandler = spy(wsHandler.new SessionMetaData(session, sessionRef, maxMsgQueuePerSession));
}
@AfterEach
void tearDown() {
if (executor != null) {
executor.shutdownNow();
}
}
@Test
void sendHandler_sendMsg_parallel_no_race() throws InterruptedException {
CountDownLatch finishLatch = new CountDownLatch(maxMsgQueuePerSession * 2);
AtomicInteger sendersCount = new AtomicInteger();
willAnswer(invocation -> {
assertThat(sendersCount.incrementAndGet()).as("no race").isEqualTo(1);
String text = invocation.getArgument(0);
SendHandler onResultHandler = invocation.getArgument(1);
SendResult sendResult = new SendResult();
executor.submit(() -> {
sendersCount.decrementAndGet();
onResultHandler.onResult(sendResult);
finishLatch.countDown();
});
return null;
}).given(asyncRemote).sendText(anyString(), any());
assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse();
//first batch
IntStream.range(0, maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i));
Awaitility.await("first batch processed").atMost(30, TimeUnit.SECONDS).until(() -> finishLatch.getCount() == maxMsgQueuePerSession);
assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse();
//second batch - to test pause between big msg batches
IntStream.range(100, 100 + maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i));
assertThat(finishLatch.await(30, TimeUnit.SECONDS)).as("all callbacks fired").isTrue();
verify(sendHandler, never()).closeSession(any());
verify(sendHandler, times(maxMsgQueuePerSession * 2)).onResult(any());
assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse();
}
@Test
void sendHandler_sendMsg_message_order() throws InterruptedException {
CountDownLatch finishLatch = new CountDownLatch(maxMsgQueuePerSession);
Collection<String> outputs = new ConcurrentLinkedQueue<>();
willAnswer(invocation -> {
String text = invocation.getArgument(0);
outputs.add(text);
SendHandler onResultHandler = invocation.getArgument(1);
SendResult sendResult = new SendResult();
executor.submit(() -> {
onResultHandler.onResult(sendResult);
finishLatch.countDown();
});
return null;
}).given(asyncRemote).sendText(anyString(), any());
List<String> inputs = IntStream.range(0, maxMsgQueuePerSession).mapToObj(i -> "msg " + i).collect(Collectors.toList());
inputs.forEach(s -> sendHandler.sendMsg(s));
assertThat(finishLatch.await(30, TimeUnit.SECONDS)).as("all callbacks fired").isTrue();
assertThat(outputs).as("inputs exactly the same as outputs").containsExactlyElementsOf(inputs);
verify(sendHandler, never()).closeSession(any());
verify(sendHandler, times(maxMsgQueuePerSession)).onResult(any());
}
@Test
void sendHandler_sendMsg_queue_size_exceed() {
willDoNothing().given(asyncRemote).sendText(anyString(), any()); // send text will never call back, so queue will grow each sendMsg
sendHandler.sendMsg("first message to stay in-flight all the time during this test");
IntStream.range(0, maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i));
verify(sendHandler, never()).closeSession(any());
sendHandler.sendMsg("excessive message");
verify(sendHandler, times(1)).closeSession(eq(new CloseStatus(1008, "Max pending updates limit reached!")));
verify(asyncRemote, times(1)).sendText(anyString(), any());
}
}

29
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java

@ -0,0 +1,29 @@
/**
* 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.device;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.core.io.Resource;
import org.thingsboard.server.common.data.Device;
import java.net.URISyntaxException;
public interface DeviceConnectivityService {
JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException;
Resource getPemCertFile(String protocol);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -53,6 +53,8 @@ public interface RelationService {
void deleteEntityRelations(TenantId tenantId, EntityId entity);
void deleteEntityCommonRelations(TenantId tenantId, EntityId entity);
List<EntityRelation> findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup);
ListenableFuture<List<EntityRelation>> findByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.widget;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType;
@ -40,6 +41,8 @@ public interface WidgetTypeService extends EntityDaoService {
List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId);
WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias);
void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);

18
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -35,9 +35,9 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
@NoXss
@Length(fieldName = "title")
@ApiModelProperty(position = 3, value = "Title of the customer", example = "Company A")
@ApiModelProperty(position = 3, required = true, value = "Title of the customer", example = "Company A")
private String title;
@ApiModelProperty(position = 5, required = true, value = "JSON object with Tenant Id")
@ApiModelProperty(position = 5, value = "JSON object with Tenant Id")
private TenantId tenantId;
@Getter @Setter
@ -89,43 +89,43 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
return super.getCreatedTime();
}
@ApiModelProperty(position = 6, required = true, value = "Country", example = "US")
@ApiModelProperty(position = 6, value = "Country", example = "US")
@Override
public String getCountry() {
return super.getCountry();
}
@ApiModelProperty(position = 7, required = true, value = "State", example = "NY")
@ApiModelProperty(position = 7, value = "State", example = "NY")
@Override
public String getState() {
return super.getState();
}
@ApiModelProperty(position = 8, required = true, value = "City", example = "New York")
@ApiModelProperty(position = 8, value = "City", example = "New York")
@Override
public String getCity() {
return super.getCity();
}
@ApiModelProperty(position = 9, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@ApiModelProperty(position = 9, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override
public String getAddress() {
return super.getAddress();
}
@ApiModelProperty(position = 10, required = true, value = "Address Line 2", example = "")
@ApiModelProperty(position = 10, value = "Address Line 2", example = "")
@Override
public String getAddress2() {
return super.getAddress2();
}
@ApiModelProperty(position = 11, required = true, value = "Zip code", example = "10004")
@ApiModelProperty(position = 11, value = "Zip code", example = "10004")
@Override
public String getZip() {
return super.getZip();
}
@ApiModelProperty(position = 12, required = true, value = "Phone number", example = "+1(415)777-7777")
@ApiModelProperty(position = 12, value = "Phone number", example = "+1(415)777-7777")
@Override
public String getPhone() {
return super.getPhone();

2
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -85,7 +85,7 @@ public class DashboardInfo extends BaseData<DashboardId> implements HasName, Has
this.tenantId = tenantId;
}
@ApiModelProperty(position = 4, value = "Title of the dashboard.")
@ApiModelProperty(position = 4, required = true, value = "Title of the dashboard.")
public String getTitle() {
return title;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -146,7 +146,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.name = name;
}
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor")
@ApiModelProperty(position = 6, value = "Device Profile Name", example = "Temperature Sensor")
public String getType() {
return type;
}
@ -155,7 +155,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.type = type;
}
@ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "Room 234 Sensor")
@ApiModelProperty(position = 7, value = "Label that may be used in widgets", example = "Room 234 Sensor")
public String getLabel() {
return label;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -40,7 +40,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
private static final long serialVersionUID = 5582010124562018986L;
@ApiModelProperty(position = 7, required = true, value = "JSON object with the referenced Entity Id (Device or Asset).")
@ApiModelProperty(position = 7, value = "JSON object with the referenced Entity Id (Device or Asset).")
private EntityId entityId;
private TenantId tenantId;
private CustomerId customerId;
@ -52,7 +52,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
@Length(fieldName = "type")
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor")
private String type;
@ApiModelProperty(position = 8, required = true, value = "Set of telemetry and attribute keys to expose via Entity View.")
@ApiModelProperty(position = 8, value = "Set of telemetry and attribute keys to expose via Entity View.")
private TelemetryEntityView keys;
@ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;")
private long startTimeMs;

18
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -34,14 +34,14 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
@Length(fieldName = "title")
@NoXss
@ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A")
@ApiModelProperty(position = 3, required = true, value = "Title of the tenant", example = "Company A")
private String title;
@NoXss
@Length(fieldName = "region")
@ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America")
private String region;
@ApiModelProperty(position = 6, required = true, value = "JSON object with Tenant Profile Id")
@ApiModelProperty(position = 6, value = "JSON object with Tenant Profile Id")
private TenantProfileId tenantProfileId;
public Tenant() {
@ -111,43 +111,43 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
return super.getCreatedTime();
}
@ApiModelProperty(position = 7, required = true, value = "Country", example = "US")
@ApiModelProperty(position = 7, value = "Country", example = "US")
@Override
public String getCountry() {
return super.getCountry();
}
@ApiModelProperty(position = 8, required = true, value = "State", example = "NY")
@ApiModelProperty(position = 8, value = "State", example = "NY")
@Override
public String getState() {
return super.getState();
}
@ApiModelProperty(position = 9, required = true, value = "City", example = "New York")
@ApiModelProperty(position = 9, value = "City", example = "New York")
@Override
public String getCity() {
return super.getCity();
}
@ApiModelProperty(position = 10, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@ApiModelProperty(position = 10, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override
public String getAddress() {
return super.getAddress();
}
@ApiModelProperty(position = 11, required = true, value = "Address Line 2", example = "")
@ApiModelProperty(position = 11, value = "Address Line 2", example = "")
@Override
public String getAddress2() {
return super.getAddress2();
}
@ApiModelProperty(position = 12, required = true, value = "Zip code", example = "10004")
@ApiModelProperty(position = 12, value = "Zip code", example = "10004")
@Override
public String getZip() {
return super.getZip();
}
@ApiModelProperty(position = 13, required = true, value = "Phone number", example = "+1(415)777-7777")
@ApiModelProperty(position = 13, value = "Phone number", example = "+1(415)777-7777")
@Override
public String getPhone() {
return super.getPhone();

6
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -129,7 +129,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.authority = authority;
}
@ApiModelProperty(position = 8, required = true, value = "First name of the user", example = "John")
@ApiModelProperty(position = 8, required = false, value = "First name of the user", example = "John")
public String getFirstName() {
return firstName;
}
@ -138,7 +138,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.firstName = firstName;
}
@ApiModelProperty(position = 9, required = true, value = "Last name of the user", example = "Doe")
@ApiModelProperty(position = 9, required = false, value = "Last name of the user", example = "Doe")
public String getLastName() {
return lastName;
}
@ -147,7 +147,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.lastName = lastName;
}
@ApiModelProperty(position = 10, required = true, value = "Phone number of the user", example = "38012345123")
@ApiModelProperty(position = 10, required = false, value = "Phone number of the user", example = "38012345123")
public String getPhone() {
return phone;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -131,7 +131,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.name = name;
}
@ApiModelProperty(position = 6, required = true, value = "Asset type", example = "Building")
@ApiModelProperty(position = 6, value = "Asset type", example = "Building")
public String getType() {
return type;
}
@ -140,7 +140,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.type = type;
}
@ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "NY Building")
@ApiModelProperty(position = 7, value = "Label that may be used in widgets", example = "NY Building")
public String getLabel() {
return label;
}
@ -149,7 +149,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.label = label;
}
@ApiModelProperty(position = 8, required = true, value = "JSON object with Asset Profile Id.")
@ApiModelProperty(position = 8, value = "JSON object with Asset Profile Id.")
public AssetProfileId getAssetProfileId() {
return assetProfileId;
}

42
common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.queue.discovery;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.ProtocolStringList;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
@ -44,8 +45,10 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -66,6 +69,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
private Integer zkSessionTimeout;
@Value("${zk.zk_dir}")
private String zkDir;
@Value("${zk.recalculate_delay:60000}")
private Long recalculateDelay;
protected final ConcurrentHashMap<String, ScheduledFuture<?>> delayedTasks;
private final TbServiceInfoProvider serviceInfoProvider;
private final PartitionService partitionService;
@ -82,6 +89,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
PartitionService partitionService) {
this.serviceInfoProvider = serviceInfoProvider;
this.partitionService = partitionService;
delayedTasks = new ConcurrentHashMap<>();
}
@PostConstruct
@ -287,11 +295,39 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
log.error("Failed to decode server instance for node {}", data.getPath(), e);
throw e;
}
log.debug("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), instance.getServiceId());
String serviceId = instance.getServiceId();
ProtocolStringList serviceTypesList = instance.getServiceTypesList();
log.trace("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), serviceId);
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
ScheduledFuture<?> task = delayedTasks.remove(serviceId);
if (task != null) {
if (task.cancel(false)) {
log.debug("[{}] Recalculate partitions ignored. Service was restarted in time [{}].",
serviceId, serviceTypesList);
} else {
log.debug("[{}] Going to recalculate partitions. Service was not restarted in time [{}]!",
serviceId, serviceTypesList);
recalculatePartitions();
}
} else {
log.trace("[{}] Going to recalculate partitions due to adding new node [{}].",
serviceId, serviceTypesList);
recalculatePartitions();
}
break;
case CHILD_REMOVED:
recalculatePartitions();
ScheduledFuture<?> future = zkExecutorService.schedule(() -> {
log.debug("[{}] Going to recalculate partitions due to removed node [{}]",
serviceId, serviceTypesList);
ScheduledFuture<?> removedTask = delayedTasks.remove(serviceId);
if (removedTask != null) {
recalculatePartitions();
}
}, recalculateDelay, TimeUnit.MILLISECONDS);
delayedTasks.put(serviceId, future);
break;
default:
break;
@ -303,6 +339,8 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
* Synchronized to ensure that other servers info is up to date
* */
synchronized void recalculatePartitions() {
delayedTasks.values().forEach(future -> future.cancel(false));
delayedTasks.clear();
partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers());
}

189
common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java

@ -0,0 +1,189 @@
/**
* 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.queue.discovery;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_ADDED;
import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_REMOVED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ZkDiscoveryServiceTest {
@Mock
private TbServiceInfoProvider serviceInfoProvider;
@Mock
private PartitionService partitionService;
@Mock
private CuratorFramework client;
@Mock
private PathChildrenCache cache;
@Mock
private CuratorFramework curatorFramework;
private ZkDiscoveryService zkDiscoveryService;
private static final long RECALCULATE_DELAY = 100L;
final TransportProtos.ServiceInfo currentInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-rule-engine-0").build();
final ChildData currentData = new ChildData("/thingsboard/nodes/0000000010", null, currentInfo.toByteArray());
final TransportProtos.ServiceInfo childInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-rule-engine-1").build();
final ChildData childData = new ChildData("/thingsboard/nodes/0000000020", null, childInfo.toByteArray());
@Before
public void setup() {
zkDiscoveryService = Mockito.spy(new ZkDiscoveryService(serviceInfoProvider, partitionService));
ScheduledExecutorService zkExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("zk-discovery"));
when(client.getState()).thenReturn(CuratorFrameworkState.STARTED);
ReflectionTestUtils.setField(zkDiscoveryService, "stopped", false);
ReflectionTestUtils.setField(zkDiscoveryService, "client", client);
ReflectionTestUtils.setField(zkDiscoveryService, "cache", cache);
ReflectionTestUtils.setField(zkDiscoveryService, "nodePath", "/thingsboard/nodes/0000000010");
ReflectionTestUtils.setField(zkDiscoveryService, "zkExecutorService", zkExecutorService);
ReflectionTestUtils.setField(zkDiscoveryService, "recalculateDelay", RECALCULATE_DELAY);
ReflectionTestUtils.setField(zkDiscoveryService, "zkDir", "/thingsboard");
when(serviceInfoProvider.getServiceInfo()).thenReturn(currentInfo);
List<ChildData> dataList = new ArrayList<>();
dataList.add(currentData);
when(cache.getCurrentData()).thenReturn(dataList);
}
@Test
public void restartNodeInTimeTest() throws Exception {
startNode(childData);
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo)));
reset(partitionService);
stopNode(childData);
assertEquals(1, zkDiscoveryService.delayedTasks.size());
verify(partitionService, never()).recalculatePartitions(any(), any());
startNode(childData);
verify(partitionService, never()).recalculatePartitions(any(), any());
Thread.sleep(RECALCULATE_DELAY * 2);
verify(partitionService, never()).recalculatePartitions(any(), any());
assertTrue(zkDiscoveryService.delayedTasks.isEmpty());
}
@Test
public void restartNodeNotInTimeTest() throws Exception {
startNode(childData);
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo)));
reset(partitionService);
stopNode(childData);
assertEquals(1, zkDiscoveryService.delayedTasks.size());
Thread.sleep(RECALCULATE_DELAY * 2);
assertTrue(zkDiscoveryService.delayedTasks.isEmpty());
startNode(childData);
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(Collections.emptyList()));
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo)));
reset(partitionService);
}
@Test
public void startAnotherNodeDuringRestartTest() throws Exception {
var anotherInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-transport").build();
var anotherData = new ChildData("/thingsboard/nodes/0000000030", null, anotherInfo.toByteArray());
startNode(childData);
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo)));
reset(partitionService);
stopNode(childData);
assertEquals(1, zkDiscoveryService.delayedTasks.size());
startNode(anotherData);
assertTrue(zkDiscoveryService.delayedTasks.isEmpty());
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(anotherInfo)));
reset(partitionService);
Thread.sleep(RECALCULATE_DELAY * 2);
verify(partitionService, never()).recalculatePartitions(any(), any());
startNode(childData);
verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(anotherInfo, childInfo)));
}
private void startNode(ChildData data) throws Exception {
cache.getCurrentData().add(data);
zkDiscoveryService.childEvent(curatorFramework, new PathChildrenCacheEvent(CHILD_ADDED, data));
}
private void stopNode(ChildData data) throws Exception {
cache.getCurrentData().remove(data);
zkDiscoveryService.childEvent(curatorFramework, new PathChildrenCacheEvent(CHILD_REMOVED, data));
}
}

7
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -66,8 +66,6 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
protected final Map<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
protected final Map<String, TbelScript> scriptMap = new ConcurrentHashMap<>();
private final String tbelSwitch = "switch";
private final String tbelSwitchErrorMsg = "TBEL does not support the 'switch'.";
protected Cache<String, Serializable> compiledScriptsCache;
private SandboxedParserConfiguration parserConfig;
@ -183,11 +181,6 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
lock.unlock();
}
return scriptId;
} catch (CompileException ce) {
if ( ce.getExpr() != null && new String(ce.getExpr()).contains(tbelSwitch)) {
ce = new CompileException(tbelSwitchErrorMsg, ce.getExpr(), ce.getCursor(), ce.getCause());
}
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, ce);
} catch (Exception e) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
}

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java

@ -52,7 +52,7 @@ import java.util.stream.Collectors;
public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService {
@Autowired
private TbLwM2MModelConfigStore modelStore;
TbLwM2MModelConfigStore modelStore;
@Autowired
@Lazy
@ -67,14 +67,14 @@ public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService {
@Autowired
private LwM2MTelemetryLogService logService;
private ConcurrentMap<String, LwM2MModelConfig> currentModelConfigs;
ConcurrentMap<String, LwM2MModelConfig> currentModelConfigs;
@AfterStartUp(order = AfterStartUp.BEFORE_TRANSPORT_SERVICE)
private void init() {
public void init() {
List<LwM2MModelConfig> models = modelStore.getAll();
log.debug("Fetched model configs: {}", models);
currentModelConfigs = models.stream()
.collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m));
.collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m, (existing, replacement) -> existing));
}
@Override

73
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java

@ -0,0 +1,73 @@
/**
* 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.transport.lwm2m.server.model;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MModelConfigStore;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
class LwM2MModelConfigServiceImplTest {
LwM2MModelConfigServiceImpl service;
TbLwM2MModelConfigStore modelStore;
@BeforeEach
void setUp() {
service = new LwM2MModelConfigServiceImpl();
modelStore = mock(TbLwM2MModelConfigStore.class);
service.modelStore = modelStore;
}
@Test
void testInitWithDuplicatedModels() {
LwM2MModelConfig config = new LwM2MModelConfig("urn:imei:951358811362976");
List<LwM2MModelConfig> models = List.of(config, config);
willReturn(models).given(modelStore).getAll();
service.init();
assertThat(service.currentModelConfigs).containsExactlyEntriesOf(Map.of(config.getEndpoint(), config));
}
@Test
void testInitWithNonUniqueEndpoints() {
LwM2MModelConfig configAlfa = new LwM2MModelConfig("urn:imei:951358811362976");
LwM2MModelConfig configBravo = new LwM2MModelConfig("urn:imei:151358811362976");
LwM2MModelConfig configDelta = new LwM2MModelConfig("urn:imei:151358811362976");
assertThat(configBravo.getEndpoint()).as("non-unique endpoints provided").isEqualTo(configDelta.getEndpoint());
List<LwM2MModelConfig> models = List.of(configAlfa, configBravo, configDelta);
willReturn(models).given(modelStore).getAll();
service.init();
assertThat(service.currentModelConfigs).containsExactlyInAnyOrderEntriesOf(Map.of(
configAlfa.getEndpoint(), configAlfa,
configBravo.getEndpoint(), configBravo
));
}
@Test
void testInitWithEmptyModels() {
willReturn(Collections.emptyList()).given(modelStore).getAll();
service.init();
assertThat(service.currentModelConfigs).isEmpty();
}
}

34
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "device")
@Data
public class DeviceConnectivityConfiguration {
private Map<String, DeviceConnectivityInfo> connectivity;
public boolean isEnabled(String protocol) {
var info = connectivity.get(protocol);
return info != null && info.isEnabled();
}
}

26
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java

@ -0,0 +1,26 @@
/**
* 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.device;
import lombok.Data;
@Data
public class DeviceConnectivityInfo {
private boolean enabled;
private String host;
private String port;
private String pemCertFile;
}

268
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -0,0 +1,268 @@
/**
* 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.device;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
@Service("DeviceConnectivityDaoService")
@Slf4j
public class DeviceConnectivityServiceImpl implements DeviceConnectivityService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId ";
public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private DeviceConnectivityConfiguration deviceConnectivityConfiguration;
@Override
public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException {
DeviceId deviceId = device.getId();
log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId);
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
DeviceTransportType transportType = deviceProfile.getTransportType();
ObjectNode commands = JacksonUtil.newObjectNode();
switch (transportType) {
case DEFAULT:
Optional.ofNullable(getHttpTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(HTTP, v));
Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(MQTT, v));
Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(COAP, v));
break;
case MQTT:
MqttDeviceProfileTransportConfiguration transportConfiguration =
(MqttDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
//TODO: add sparkplug command with emulator (check SSL)
if (transportConfiguration.isSparkplug()) {
ObjectNode sparkplug = JacksonUtil.newObjectNode();
sparkplug.put("sparkplug", CHECK_DOCUMENTATION);
commands.set(MQTT, sparkplug);
} else {
String topicName = transportConfiguration.getDeviceTelemetryTopic();
Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, topicName, creds))
.ifPresent(v -> commands.set(MQTT, v));
}
break;
case COAP:
Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(COAP, v));
break;
default:
commands.put(transportType.name(), CHECK_DOCUMENTATION);
}
return commands;
}
@Override
public Resource getPemCertFile(String protocol) {
String certFilePath = deviceConnectivityConfiguration.getConnectivity()
.get(protocol)
.getPemCertFile();
if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) {
return new ClassPathResource(certFilePath);
} else {
return null;
}
}
private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException {
ObjectNode httpCommands = JacksonUtil.newObjectNode();
Optional.ofNullable(getHttpPublishCommand(HTTP, defaultHostname, deviceCredentials))
.ifPresent(v -> httpCommands.put(HTTP, v));
Optional.ofNullable(getHttpPublishCommand(HTTPS, defaultHostname, deviceCredentials))
.ifPresent(v -> httpCommands.put(HTTPS, v));
return httpCommands.isEmpty() ? null : httpCommands;
}
private String getHttpPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
if (properties == null || !properties.isEnabled() ||
deviceCredentials.getCredentialsType() != DeviceCredentialsType.ACCESS_TOKEN) {
return null;
}
String hostName = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials);
}
private JsonNode getMqttTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
return getMqttTransportPublishCommands(baseUrl, DEFAULT_DEVICE_TELEMETRY_TOPIC, deviceCredentials);
}
private JsonNode getMqttTransportPublishCommands(String baseUrl, String topic, DeviceCredentials deviceCredentials) throws URISyntaxException {
ObjectNode mqttCommands = JacksonUtil.newObjectNode();
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
mqttCommands.put(MQTTS, CHECK_DOCUMENTATION);
return mqttCommands;
}
ObjectNode dockerMqttCommands = JacksonUtil.newObjectNode();
if (deviceConnectivityConfiguration.isEnabled(MQTT)) {
Optional.ofNullable(getMqttPublishCommand(baseUrl, topic, deviceCredentials)).
ifPresent(v -> mqttCommands.put(MQTT, v));
Optional.ofNullable(getDockerMqttPublishCommand(MQTT, baseUrl, topic, deviceCredentials))
.ifPresent(v -> dockerMqttCommands.put(MQTT, v));
}
if (deviceConnectivityConfiguration.isEnabled(MQTTS)) {
List<String> mqttsPublishCommand = getMqttsPublishCommand(baseUrl, topic, deviceCredentials);
if (mqttsPublishCommand != null) {
ArrayNode arrayNode = mqttCommands.putArray(MQTTS);
mqttsPublishCommand.forEach(arrayNode::add);
}
Optional.ofNullable(getDockerMqttPublishCommand(MQTTS, baseUrl, topic, deviceCredentials))
.ifPresent(v -> dockerMqttCommands.put(MQTTS, v));
}
if (!dockerMqttCommands.isEmpty()) {
mqttCommands.set(DOCKER, dockerMqttCommands);
}
return mqttCommands.isEmpty() ? null : mqttCommands;
}
private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(MQTT);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
}
private List<String> getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(MQTTS);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
ArrayList<String> commands = new ArrayList<>();
if (pubCommand != null) {
commands.add(DeviceConnectivityUtil.getCurlPemCertCommand(baseUrl, MQTTS));
commands.add(pubCommand);
return commands;
}
return null;
}
private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
}
private JsonNode getCoapTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
ObjectNode coapCommands = JacksonUtil.newObjectNode();
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
coapCommands.put(COAPS, CHECK_DOCUMENTATION);
return coapCommands;
}
ObjectNode dockerCoapCommands = JacksonUtil.newObjectNode();
if (deviceConnectivityConfiguration.isEnabled(COAP)) {
Optional.ofNullable(getCoapPublishCommand(COAP, baseUrl, deviceCredentials))
.ifPresent(v -> coapCommands.put(COAP, v));
Optional.ofNullable(getDockerCoapPublishCommand(COAP, baseUrl, deviceCredentials))
.ifPresent(v -> dockerCoapCommands.put(COAP, v));
}
if (deviceConnectivityConfiguration.isEnabled(COAPS)) {
Optional.ofNullable(getCoapPublishCommand(COAPS, baseUrl, deviceCredentials))
.ifPresent(v -> coapCommands.put(COAPS, v));
Optional.ofNullable(getDockerCoapPublishCommand(COAPS, baseUrl, deviceCredentials))
.ifPresent(v -> dockerCoapCommands.put(COAPS, v));
}
if (!dockerCoapCommands.isEmpty()) {
coapCommands.set(DOCKER, dockerCoapCommands);
}
return coapCommands.isEmpty() ? null : coapCommands;
}
private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
String hostName = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials);
}
private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
String host = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials);
}
private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException {
return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost();
}
}

9
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java

@ -19,13 +19,13 @@ import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.common.util.ThingsBoardExecutors;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by ashvayka on 21.02.17.
@ -34,9 +34,12 @@ public abstract class CassandraAbstractAsyncDao extends CassandraAbstractDao {
protected ExecutorService readResultsProcessingExecutor;
@Value("${cassandra.query.result_processing_threads:50}")
private int threadPoolSize;
@PostConstruct
public void startExecutor() {
readResultsProcessingExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("cassandra-callback"));
readResultsProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "cassandra-callback");
}
@PreDestroy

31
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -220,17 +220,36 @@ public class BaseRelationService implements RelationService {
return future;
}
@Transactional
@Override
public void deleteEntityCommonRelations(TenantId tenantId, EntityId entityId) {
deleteEntityRelations(tenantId, entityId, RelationTypeGroup.COMMON);
}
@Transactional
@Override
public void deleteEntityRelations(TenantId tenantId, EntityId entityId) {
deleteEntityRelations(tenantId, entityId, null);
}
@Transactional
public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
validate(entityId);
List<EntityRelation> inboundRelations = new ArrayList<>(relationDao.findAllByTo(tenantId, entityId));
List<EntityRelation> outboundRelations = new ArrayList<>(relationDao.findAllByFrom(tenantId, entityId));
List<EntityRelation> inboundRelations = relationTypeGroup == null
? relationDao.findAllByTo(tenantId, entityId)
: relationDao.findAllByTo(tenantId, entityId, relationTypeGroup);
List<EntityRelation> outboundRelations = relationTypeGroup == null
? relationDao.findAllByFrom(tenantId, entityId)
: relationDao.findAllByFrom(tenantId, entityId, relationTypeGroup);
if (!inboundRelations.isEmpty()) {
try {
relationDao.deleteInboundRelations(tenantId, entityId);
if (relationTypeGroup == null) {
relationDao.deleteInboundRelations(tenantId, entityId);
} else {
relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup);
}
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e);
}
@ -241,7 +260,11 @@ public class BaseRelationService implements RelationService {
}
if (!outboundRelations.isEmpty()) {
relationDao.deleteOutboundRelations(tenantId, entityId);
if (relationTypeGroup == null) {
relationDao.deleteOutboundRelations(tenantId, entityId);
} else {
relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup);
}
for (EntityRelation relation : outboundRelations) {
eventPublisher.publishEvent(EntityRelationEvent.from(relation));

4
dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java

@ -64,8 +64,12 @@ public interface RelationDao {
void deleteOutboundRelations(TenantId tenantId, EntityId entity);
void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup);
void deleteInboundRelations(TenantId tenantId, EntityId entity);
void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup);
ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity);
List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit);

1
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -109,6 +109,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
public void deleteResource(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
resourceValidator.validateDelete(tenantId, resourceId);
resourceDao.removeById(tenantId, resourceId.getId());
}

4
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -22,6 +22,7 @@ import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.TenantEntityWithDataDao;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -82,6 +83,9 @@ public abstract class DataValidator<D extends BaseData<?>> {
return null;
}
public void validateDelete(TenantId tenantId, EntityId entityId) {
}
protected boolean isSameData(D existentData, D actualData) {
return actualData.getId() != null && existentData.getId().equals(actualData.getId());
}

21
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -20,14 +20,21 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.TbResourceDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE;
@ -37,6 +44,9 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Autowired
private TbResourceDao resourceDao;
@Autowired
private WidgetTypeDao widgetTypeDao;
@Autowired
private TenantService tenantService;
@ -77,4 +87,15 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
}
}
}
@Override
public void validateDelete(TenantId tenantId, EntityId resourceId) {
List<WidgetTypeDetails> widgets = widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(),
resourceId.getId());
if (!widgets.isEmpty()) {
List<String> widgetNames = widgets.stream().map(BaseWidgetType::getName).collect(Collectors.toList());
throw new DataValidationException(String.format("Following widget types uses current resource: %s", widgetNames));
}
}
}

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

@ -69,7 +69,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
private static final Map<EntityType, String> entityTableMap = new HashMap<>();
private static final Map<EntityType, String> entityNameColumns = new HashMap<>();
private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" +
" WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id) END as phone";
" WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id)" +
" WHEN entity.entity_type = 'USER' THEN (select phone from tb_user where id = entity_id) END as phone";
private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" +
" WHEN entity.entity_type = 'CUSTOMER' THEN (select zip from customer where id = entity_id) END as zip";
private static final String SELECT_ADDRESS_2 = " CASE WHEN entity.entity_type = 'TENANT'" +

19
dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java

@ -37,6 +37,7 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -205,6 +206,15 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
}
}
@Override
public void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) {
try {
relationRepository.deleteByFromIdAndFromTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name()));
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
}
@Override
public void deleteInboundRelations(TenantId tenantId, EntityId entity) {
try {
@ -214,6 +224,15 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
}
}
@Override
public void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) {
try {
relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name()));
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
}
@Override
public ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) {
return service.submit(

5
dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java

@ -82,4 +82,9 @@ public interface RelationRepository
@Query("DELETE FROM RelationEntity r where r.toId = :toId and r.toType = :toType and r.relationTypeGroup in :relationTypeGroups")
void deleteByToIdAndToTypeAndRelationTypeGroupIn(@Param("toId") UUID toId, @Param("toType") String toType, @Param("relationTypeGroups") List<String> relationTypeGroups);
@Transactional
@Modifying
@Query("DELETE FROM RelationEntity r where r.fromId = :fromId and r.fromType = :fromType and r.relationTypeGroup in :relationTypeGroups")
void deleteByFromIdAndFromTypeAndRelationTypeGroupIn(@Param("fromId") UUID fromId, @Param("fromType") String fromType, @Param("relationTypeGroups") List<String> relationTypeGroups);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -77,6 +77,11 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias));
}
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId));
}
@Override
public EntityType getEntityType() {
return EntityType.WIDGET_TYPE;

7
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java

@ -46,4 +46,11 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId,
@Param("bundleAlias") String bundleAlias,
@Param("alias") String alias);
@Query(value = "SELECT * FROM widget_type wt " +
"WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))",
nativeQuery = true)
List<WidgetTypeDetailsEntity> findWidgetTypesInfosByTenantIdAndResourceId(@Param("tenantId") UUID tenantId,
@Param("resourceId") UUID resourceId);
}

16
dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java

@ -121,7 +121,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
perTenantLimitReached = true;
}
} else if (tenantId == null) {
log.info("Invalid task received: {}", task);
log.info("[{}] Invalid task received: {}", getBufferName(), task);
}
if (!perTenantLimitReached) {
@ -157,7 +157,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
public abstract String getBufferName();
private void dispatch() {
log.info("Buffered rate executor thread started");
log.info("[{}] Buffered rate executor thread started", getBufferName());
while (!Thread.interrupted()) {
int curLvl = concurrencyLevel.get();
AsyncTaskContext<T, V> taskCtx = null;
@ -169,7 +169,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
if (printQueriesIdx.incrementAndGet() >= printQueriesFreq) {
printQueriesIdx.set(0);
String query = queryToString(finalTaskCtx);
log.info("[{}] Cassandra query: {}", taskCtx.getId(), query);
log.info("[{}][{}] Cassandra query: {}", getBufferName(), taskCtx.getId(), query);
}
}
logTask("Processing", finalTaskCtx);
@ -222,7 +222,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
}
}
}
log.info("Buffered rate executor thread stopped");
log.info("[{}] Buffered rate executor thread stopped", getBufferName());
}
private void logTask(String action, AsyncTaskContext<T, V> taskCtx) {
@ -298,7 +298,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
statsBuilder.append(CONCURRENCY_LEVEL).append(" = [").append(concurrencyLevel.get()).append("] ");
stats.getStatsCounters().forEach(StatsCounter::clear);
log.info("Permits {}", statsBuilder);
log.info("[{}] Permits {}", getBufferName(), statsBuilder);
}
stats.getRateLimitedTenants().entrySet().stream()
@ -314,13 +314,13 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
try {
return entityService.fetchEntityName(TenantId.SYS_TENANT_ID, tenantId).orElse(defaultName);
} catch (Exception e) {
log.error("[{}] Failed to get tenant name", tenantId, e);
log.error("[{}][{}] Failed to get tenant name", getBufferName(), tenantId, e);
return defaultName;
}
});
log.info("[{}][{}] Rate limited requests: {}", tenantId, name, rateLimitedRequests);
log.info("[{}][{}][{}] Rate limited requests: {}", getBufferName(), tenantId, name, rateLimitedRequests);
} else {
log.info("[{}] Rate limited requests: {}", tenantId, rateLimitedRequests);
log.info("[{}][{}] Rate limited requests: {}", getBufferName(), tenantId, rateLimitedRequests);
}
});
}

123
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -0,0 +1,123 @@
/**
* 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.util;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials;
public class DeviceConnectivityUtil {
public static final String HTTP = "http";
public static final String HTTPS = "https";
public static final String MQTT = "mqtt";
public static final String LINUX = "linux";
public static final String WINDOWS = "windows";
public static final String DOCKER = "docker";
public static final String MQTTS = "mqtts";
public static final String COAP = "coap";
public static final String COAPS = "coaps";
public static final String PEM_CERT_FILE_NAME = "tb-server-chain.pem";
public static final String CHECK_DOCUMENTATION = "Check documentation";
public static final String JSON_EXAMPLE_PAYLOAD = "\"{temperature:25}\"";
public static final String DOCKER_RUN = "docker run --rm -it ";
public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients ";
public static final String COAP_IMAGE = "thingsboard/coap-clients ";
public static String getHttpPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
return String.format("curl -v -X POST %s://%s%s/api/v1/%s/telemetry --header Content-Type:application/json --data " + JSON_EXAMPLE_PAYLOAD,
protocol, host, port, deviceCredentials.getCredentialsId());
}
public static String getMqttPublishCommand(String protocol, String host, String port, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) {
StringBuilder command = new StringBuilder("mosquitto_pub -d -q 1");
if (MQTTS.equals(protocol)) {
command.append(" --cafile ").append(PEM_CERT_FILE_NAME);
}
command.append(" -h ").append(host).append(port == null ? "" : " -p " + port);
command.append(" -t ").append(deviceTelemetryTopic);
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
command.append(" -u ").append(deviceCredentials.getCredentialsId());
break;
case MQTT_BASIC:
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class);
if (credentials != null) {
if (credentials.getClientId() != null) {
command.append(" -i ").append(credentials.getClientId());
}
if (credentials.getUserName() != null) {
command.append(" -u ").append(credentials.getUserName());
}
if (credentials.getPassword() != null) {
command.append(" -P ").append(credentials.getPassword());
}
} else {
return null;
}
break;
default:
return null;
}
command.append(" -m " + JSON_EXAMPLE_PAYLOAD);
return command.toString();
}
public static String getDockerMqttPublishCommand(String protocol, String baseUrl, String host, String port, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) {
String mqttCommand = getMqttPublishCommand(protocol, host, port, deviceTelemetryTopic, deviceCredentials);
if (mqttCommand == null) {
return null;
}
StringBuilder mqttDockerCommand = new StringBuilder();
mqttDockerCommand.append(DOCKER_RUN).append(MQTT_IMAGE);
if (MQTTS.equals(protocol)) {
mqttDockerCommand.append("/bin/sh -c \"")
.append(getCurlPemCertCommand(baseUrl, protocol))
.append(" && ")
.append(mqttCommand)
.append("\"");
} else {
mqttDockerCommand.append(mqttCommand);
}
return mqttDockerCommand.toString();
}
public static String getCurlPemCertCommand(String baseUrl, String protocol) {
return String.format("curl -f -S -o %s %s/api/device-connectivity/%s/certificate/download", PEM_CERT_FILE_NAME, baseUrl, protocol);
}
public static String getCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
String client = COAPS.equals(protocol) ? "coap-client-openssl" : "coap-client";
return String.format("%s -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s",
client, protocol, host, port, deviceCredentials.getCredentialsId(), JSON_EXAMPLE_PAYLOAD);
default:
return null;
}
}
public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials);
return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN, COAP_IMAGE, coapCommand) : null;
}
}

8
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -83,4 +83,12 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails> {
*/
WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias);
/**
* Find widget types infos by tenantId and resourceId in descriptor.
*
* @param tenantId the tenantId
* @param tbResourceId the resourceId
* @return the list of widget types infos objects
*/
List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId);
}

10
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -21,6 +21,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType;
@ -37,6 +38,7 @@ import java.util.Optional;
public class WidgetTypeServiceImpl implements WidgetTypeService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias ";
@Autowired
private WidgetTypeDao widgetTypeDao;
@ -96,6 +98,14 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
}
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId) {
log.trace("Executing findWidgetTypesInfosByTenantIdAndResourceId, tenantId [{}], tbResourceId [{}]", tenantId, tbResourceId);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateId(tbResourceId, INCORRECT_RESOURCE_ID + tbResourceId);
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(), tbResourceId.getId());
}
@Override
public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) {
log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias);

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

@ -826,7 +826,7 @@ CREATE TABLE IF NOT EXISTS notification_request (
targets VARCHAR(10000) NOT NULL,
template_id UUID,
template VARCHAR(10000000),
info VARCHAR(1000),
info VARCHAR(1000000),
additional_config VARCHAR(1000),
originator_entity_id UUID,
originator_entity_type VARCHAR(32),

40
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
@ -70,6 +71,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
@ -79,6 +81,7 @@ import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.sql.relation.RelationRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList;
import java.util.Arrays;
@ -105,6 +108,8 @@ public class EntityServiceTest extends AbstractServiceTest {
@Autowired
AssetService assetService;
@Autowired
UserService userService;
@Autowired
AttributesService attributesService;
@Autowired
DeviceService deviceService;
@ -227,6 +232,41 @@ public class EntityServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, count);
}
@Test
public void testCountHierarchicalUserEntitiesByQuery() throws InterruptedException {
List<User> users = new ArrayList<>();
createTestUserRelations(tenantId, users);
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId);
filter.setDirection(EntitySearchDirection.FROM);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null);
List<EntityKey> entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "phone"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> entityDataByQuery = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
List<EntityData> data = entityDataByQuery.getData();
Assert.assertEquals(data.size(), 5);
data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone")));
}
private void createTestUserRelations(TenantId tenantId, List<User> users) {
for (int i = 0; i < ENTITY_COUNT; i++) {
User user = new User();
user.setTenantId(tenantId);
user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail(StringUtils.randomAlphabetic(10) + "@gmail.com");
user.setPhone(StringUtils.randomNumeric(10));
user = userService.saveUser(user);
users.add(user);
createRelation(tenantId, "Contains", tenantId, user.getId());
}
}
@Test
public void testCountEdgeEntitiesByQuery() throws InterruptedException {
List<Edge> edges = new ArrayList<>();

25
dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java

@ -131,6 +131,31 @@ public class RelationServiceTest extends AbstractServiceTest {
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
}
@Test
public void testDeleteEntityCommonRelations() {
AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased());
AssetId subChildId = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(parentId, childId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE);
EntityRelation relationD = new EntityRelation(childId, subChildId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
saveRelation(relationD);
relationService.deleteEntityCommonRelations(SYSTEM_TENANT_ID, childId);
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE));
Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.MANAGES_TYPE, RelationTypeGroup.EDGE));
}
@Test
public void testFindFrom() throws ExecutionException, InterruptedException {
AssetId parentA = new AssetId(Uuids.timeBased());

1
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -41,6 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
queue:
type: "${TB_QUEUE_TYPE:kafka}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)

2
pom.xml

@ -78,7 +78,7 @@
<zookeeper.version>3.8.1</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version>
<tbel.version>1.0.6</tbel.version>
<tbel.version>1.0.7</tbel.version>
<lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

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

1
transport/coap/src/main/resources/tb-coap-transport.yml

@ -41,6 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
cache:
type: "${CACHE_TYPE:redis}"

1
transport/http/src/main/resources/tb-http-transport.yml

@ -68,6 +68,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
cache:
type: "${CACHE_TYPE:redis}"

1
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -41,6 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
cache:
type: "${CACHE_TYPE:redis}"

1
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -41,6 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
cache:
type: "${CACHE_TYPE:redis}"

1
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -41,6 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
cache:
type: "${CACHE_TYPE:redis}"

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

@ -30,6 +30,7 @@ import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
@Component({
selector: 'tb-root',
@ -55,44 +56,18 @@ export class AppComponent implements OnInit {
}
});
this.matIconRegistry.addSvgIconLiteral(
'google-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'github-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 32.7 32.7"><path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.2-.4-.7-2.1.2-4.3 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5s2.8.2 4.1.5c3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3v4.5c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill="#211c19"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'facebook-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 263 263"><path d="M263 131.5C263 58.9 204.1 0 131.5 0S0 58.9 0 131.5c0 65.6 48.1 120 110.9 129.9v-91.9H77.5v-38h33.4v-29c0-33 19.6-51.2 49.7-51.2 14.4 0 29.4 2.6 29.4 2.6v32.4h-16.5c-16.3 0-21.4 10.1-21.4 20.5v24.7h36.4l-5.8 38h-30.6v91.9c62.8-9.9 110.9-64.3 110.9-129.9z" fill="#1877f2"/><path d="M182.7 169.5l5.8-38H152v-24.7c0-10.4 5.1-20.5 21.4-20.5H190V53.9s-15-2.6-29.4-2.6c-30 0-49.7 18.2-49.7 51.2v29H77.5v38h33.4v91.9c6.7 1.1 13.6 1.6 20.5 1.6s13.9-.5 20.5-1.6v-91.9h30.8z" fill="#fff"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'apple-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'queues-list',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
)
);
for (const svgIcon of Object.keys(svgIcons)) {
this.matIconRegistry.addSvgIconLiteral(
svgIcon,
this.domSanitizer.bypassSecurityTrustHtml(
svgIcons[svgIcon]
)
);
}
for (const svgIcon of Object.keys(svgIconsUrl)) {
this.matIconRegistry.addSvgIcon(svgIcon, this.domSanitizer.bypassSecurityTrustResourceUrl(svgIconsUrl[svgIcon]));
}
this.storageService.testLocalStorage();

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

@ -17,6 +17,7 @@
import { Action } from '@ngrx/store';
import { User } from '@shared/models/user.model';
import { AuthPayload } from '@core/auth/auth.models';
import { UserSettings } from '@shared/models/user-settings.models';
export enum AuthActionTypes {
AUTHENTICATED = '[Auth] Authenticated',
@ -25,7 +26,9 @@ export enum AuthActionTypes {
UPDATE_USER_DETAILS = '[Auth] Update User Details',
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository',
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section'
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section',
PUT_USER_SETTINGS = '[Preferences] Put user settings',
DELETE_USER_SETTINGS = '[Preferences] Delete user settings',
}
export class ActionAuthAuthenticated implements Action {
@ -68,6 +71,18 @@ export class ActionPreferencesUpdateOpenedMenuSection implements Action {
constructor(readonly payload: { path: string; opened: boolean }) {}
}
export class ActionPreferencesPutUserSettings implements Action {
readonly type = AuthActionTypes.PUT_USER_SETTINGS;
constructor(readonly payload: Partial<UserSettings>) {}
}
export class ActionPreferencesDeleteUserSettings implements Action {
readonly type = AuthActionTypes.DELETE_USER_SETTINGS;
constructor(readonly payload: Array<NestedKeyOf<UserSettings>>) {}
}
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository |
ActionPreferencesUpdateOpenedMenuSection;
ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings;

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

@ -39,4 +39,18 @@ export class AuthEffects {
withLatestFrom(this.store.pipe(select(selectAuthState))),
mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections }))
), {dispatch: false});
putUserSettings = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.PUT_USER_SETTINGS,
),
mergeMap((state) => this.userSettingsService.putUserSettings(state.payload))
), {dispatch: false});
deleteUserSettings = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.DELETE_USER_SETTINGS,
),
mergeMap((state) => this.userSettingsService.deleteUserSettings(state.payload))
), {dispatch: false});
}

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

@ -16,7 +16,8 @@
import { AuthPayload, AuthState } from './auth.models';
import { AuthActions, AuthActionTypes } from './auth.actions';
import { initialUserSettings } from '@shared/models/user-settings.models';
import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models';
import { unset } from '@core/utils';
const emptyUserAuthState: AuthPayload = {
authUser: null,
@ -42,6 +43,7 @@ export const authReducer = (
state: AuthState = initialState,
action: AuthActions
): AuthState => {
let userSettings: UserSettings;
switch (action.type) {
case AuthActionTypes.AUTHENTICATED:
return { ...state, isAuthenticated: true, ...action.payload };
@ -71,7 +73,16 @@ export const authReducer = (
} else {
openedMenuSections.delete(action.payload.path);
}
const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
return { ...state, ...{ userSettings }};
case AuthActionTypes.PUT_USER_SETTINGS:
userSettings = {...state.userSettings, ...action.payload};
return { ...state, ...{ userSettings }};
case AuthActionTypes.DELETE_USER_SETTINGS:
userSettings = {...state.userSettings};
action.payload.forEach(path => unset(userSettings, path));
return { ...state, ...{ userSettings }};
default:

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

@ -21,6 +21,7 @@ import { AuthState } from './auth.models';
import { take } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
import { getDescendantProp } from '@core/utils';
export const selectAuthState = createFeatureSelector< AuthState>(
'auth'
@ -76,6 +77,11 @@ export const selectUserSettings = createSelector(
(state: AuthState) => state.userSettings
);
export const selectUserSettingsProperty = (path: NestedKeyOf<UserSettings>) => createSelector(
selectAuthState,
(state: AuthState) => getDescendantProp(state.userSettings, path)
);
export const selectOpenedMenuSections = createSelector(
selectAuthState,
(state: AuthState) => state.userSettings.openedMenuSections

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

@ -244,7 +244,7 @@ export class AuthService {
if (authState && authState.authUser) {
if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {
if ((this.userHasDefaultDashboard(authState) && authState.forceFullscreen) || authState.authUser.isPublic) {
if (path === 'profile' || path === 'security') {
if (path.startsWith('account')) {
if (this.userHasProfile(authState.authUser)) {
return false;
} else {

10
ui-ngx/src/app/core/http/device.service.ts

@ -25,8 +25,10 @@ import {
ClaimResult,
Device,
DeviceCredentials,
DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery
DeviceInfo,
DeviceInfoQuery,
DeviceSearchQuery,
PublishTelemetryCommand
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { AuthService } from '@core/auth/auth.service';
@ -208,4 +210,8 @@ export class DeviceService {
return this.http.post<BulkImportResult>('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
public getDevicePublishTelemetryCommands(deviceId: string, config?: RequestConfig): Observable<PublishTelemetryCommand> {
return this.http.get<PublishTelemetryCommand>(`/api/device-connectivity/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
}

2
ui-ngx/src/app/core/public-api.ts

@ -18,7 +18,7 @@ export * from './api/public-api';
export * from './http/public-api';
export * from './local-storage/local-storage.service';
export * from './services/public-api';
export * from './ws/telemetry-websocket.service';
export * from './ws/public-api';
export * from './core.state';
export * from './core.module';
export * from './utils';

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

@ -348,10 +348,8 @@ export class DashboardUtilsService {
private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor,
config: WidgetConfig, datasources?: Datasource[]): Datasource[] {
const newDatasources: Datasource[] = [];
if (datasources) {
datasources.forEach(datasource => {
newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource));
});
if (datasources?.length) {
newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasources[0]));
}
return newDatasources;
}

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

@ -24,7 +24,6 @@ export interface MenuSection extends HasUUID{
type: MenuSectionType;
path: string;
icon: string;
isMdiIcon?: boolean;
pages?: Array<MenuSection>;
opened?: boolean;
disabled?: boolean;
@ -39,6 +38,5 @@ export interface HomeSection {
export interface HomeSectionPlace {
name: string;
icon: string;
isMdiIcon?: boolean;
path: string;
}

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

@ -111,8 +111,7 @@ export class MenuService {
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
icon: 'mdi:alpha-t-box',
isMdiIcon: true
icon: 'mdi:alpha-t-box'
},
{
id: 'resources',
@ -133,8 +132,7 @@ export class MenuService {
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
icon: 'mdi:rhombus-split'
}
]
},
@ -144,7 +142,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',
@ -176,8 +173,7 @@ export class MenuService {
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
@ -185,8 +181,7 @@ export class MenuService {
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
icon: 'mdi:message-cog'
}
]
},
@ -218,8 +213,7 @@ export class MenuService {
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
icon: 'mdi:message-badge'
},
{
id: 'queues',
@ -250,16 +244,14 @@ export class MenuService {
name: 'admin.2fa.2fa',
type: 'link',
path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
icon: 'mdi:two-factor-authentication'
},
{
id: 'oauth2',
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account',
isMdiIcon: true
icon: 'mdi:shield-account'
}
]
}
@ -281,7 +273,6 @@ export class MenuService {
{
name: 'tenant-profile.tenant-profiles',
icon: 'mdi:alpha-t-box',
isMdiIcon: true,
path: '/tenantProfiles'
},
]
@ -327,7 +318,6 @@ export class MenuService {
{
name: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true,
path: '/settings/2fa'
},
{
@ -361,8 +351,7 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline',
isMdiIcon: true
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
@ -413,16 +402,14 @@ export class MenuService {
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
icon: 'mdi:alpha-d-box'
},
{
id: 'asset_profiles',
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true
icon: 'mdi:alpha-a-box'
}
]
},
@ -513,8 +500,7 @@ export class MenuService {
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
icon: 'mdi:rhombus-split'
}
]
},
@ -524,7 +510,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',
@ -556,8 +541,7 @@ export class MenuService {
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
@ -565,8 +549,7 @@ export class MenuService {
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
icon: 'mdi:message-cog'
}
]
},
@ -598,8 +581,7 @@ export class MenuService {
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
icon: 'mdi:message-badge'
},
{
id: 'repository_settings',
@ -673,7 +655,6 @@ export class MenuService {
{
name: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true,
path: '/profiles/assetProfiles'
}
]
@ -689,7 +670,6 @@ export class MenuService {
{
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/profiles/deviceProfiles'
},
{
@ -814,8 +794,7 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline',
isMdiIcon: true
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
@ -874,7 +853,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',

50
ui-ngx/src/app/core/services/script/node-script-test.service.ts

@ -23,8 +23,8 @@ import {
NodeScriptTestDialogComponent,
NodeScriptTestDialogData
} from '@shared/components/dialog/node-script-test-dialog.component';
import { sortObjectKeys } from '@core/utils';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
@Injectable({
providedIn: 'root'
@ -37,56 +37,52 @@ export class NodeScriptTestService {
testNodeScript(script: string, scriptType: string, functionTitle: string,
functionName: string, argNames: string[], ruleNodeId: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> {
if (ruleNodeId) {
scriptLang?: ScriptLanguage, debugEventBody?: DebugRuleNodeEventBody): Observable<string> {
if (ruleNodeId && !debugEventBody) {
return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
switchMap((debugIn) => {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (debugIn) {
if (debugIn.data) {
try {
msg = JSON.parse(debugIn.data);
} catch (e) {}
}
if (debugIn.metadata) {
try {
metadata = JSON.parse(debugIn.metadata);
} catch (e) {}
}
msgType = debugIn.msgType;
}
return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, msg, metadata, msgType, helpId, scriptLang);
functionName, argNames, debugIn, helpId, scriptLang);
})
);
} else {
return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, null, null, null, helpId, scriptLang);
functionName, argNames, debugEventBody, helpId, scriptLang);
}
}
private openTestScriptDialog(script: string, scriptType: string,
functionTitle: string, functionName: string, argNames: string[],
msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string,
private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string,
argNames: string[], eventBody: DebugRuleNodeEventBody, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (eventBody && eventBody.data) {
try {
msg = JSON.parse(eventBody.data);
} catch (e) {}
}
if (!msg) {
msg = {
temperature: 22.4,
humidity: 78
};
}
if (eventBody && eventBody.metadata) {
try {
metadata = JSON.parse(eventBody.metadata);
} catch (e) {}
}
if (!metadata) {
metadata = {
deviceName: 'Test Device',
deviceType: 'default',
ts: new Date().getTime() + ''
};
} else {
metadata = sortObjectKeys(metadata);
}
if (!msgType) {
if (eventBody && eventBody.msgType) {
msgType = eventBody.msgType;
} else {
msgType = 'POST_TELEMETRY_REQUEST';
}
return this.dialog.open<NodeScriptTestDialogComponent, NodeScriptTestDialogData, string>(NodeScriptTestDialogComponent,

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

@ -315,6 +315,8 @@ export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b);
export const isEmpty = (a: any): boolean => _.isEmpty(a);
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
if (a === b) {
return true;
@ -774,3 +776,25 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
}
return label;
}
export const getOS = (): string => {
const userAgent = window.navigator.userAgent.toLowerCase();
const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos|mac_powerpc)/i;
const windowsPlatforms = /(win32|win64|windows|wince)/i;
const iosPlatforms = /(iphone|ipad|ipod|darwin|ios)/i;
let os = null;
if (macosPlatforms.test(userAgent)) {
os = 'macos';
} else if (iosPlatforms.test(userAgent)) {
os = 'ios';
} else if (windowsPlatforms.test(userAgent)) {
os = 'windows';
} else if (/android/.test(userAgent)) {
os = 'android';
} else if (/linux/.test(userAgent)) {
os = 'linux';
}
return os;
};

19
ui-ngx/src/app/core/ws/public-api.ts

@ -0,0 +1,19 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './notification-websocket.service';
export * from './telemetry-websocket.service';
export * from './websocket.service';

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

@ -182,6 +182,7 @@ import * as ToggleHeaderComponent from '@shared/components/toggle-header.compone
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
import * as TbIconComponent from '@shared/components/icon.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -484,6 +485,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@shared/components/icon.component': TbIconComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

22
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html

@ -56,25 +56,28 @@
</ng-template>
<ng-template #alarmFilter>
<div class="tb-form-panel no-padding no-border" [formGroup]="alarmFilterConfigForm">
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>alarm.alarm-status-list</div>
<div class="tb-form-row space-between" ngClass.xs="filters-row-mobile">
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>alarm.alarm-status-list</div>
<mat-chip-listbox multiple formControlName="statusList">
<mat-chip-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>alarm.alarm-severity-list</div>
<div class="tb-form-row space-between" ngClass.xs="filters-row-mobile">
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>alarm.alarm-severity-list</div>
<mat-chip-listbox multiple formControlName="severityList">
<mat-chip-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>alarm.alarm-type-list</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-chips">
<div class="tb-form-row" ngClass.xs="filters-row-mobile">
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>alarm.alarm-type-list</div>
<mat-form-field fxFlex appearance="outline"
subscriptSizing="dynamic"
class="tb-chips"
ngClass.xs="filters-fields-width-mobile">
<mat-chip-grid #alarmTypeChipList formControlName="typeList">
<mat-chip-row *ngFor="let type of alarmTypeList()"
[removable]="true" (removed)="removeAlarmType(type)">
@ -89,9 +92,10 @@
</mat-chip-grid>
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>alarm.assignee</div>
<div class="tb-form-row" ngClass.xs="filters-row-mobile">
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>alarm.assignee</div>
<tb-alarm-assignee-select fxFlex inline="true"
ngClass.xs="filters-fields-width-mobile"
[userMode]="userMode"
formControlName="assigneeId">
</tb-alarm-assignee-select>

20
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss

@ -15,11 +15,24 @@
*/
:host {
display: block;
overflow: hidden;
overflow: scroll;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
.filters-row-mobile {
flex-direction: column;
align-items: start;
border: none;
padding: 0;
}
.filters-title-mobile {
font-size: 14px;
}
.filters-fields-width-mobile {
width: 100%;
}
}
:host ::ng-deep {
@ -32,4 +45,9 @@
text-overflow: ellipsis;
}
}
.mat-mdc-chip {
.mdc-evolution-chip__cell, .mat-mdc-chip-action, .mat-mdc-chip-action-label {
overflow: hidden;
}
}
}

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

@ -104,7 +104,7 @@
}
mat-cell.tb-value-cell {
cursor: pointer;
mat-icon {
.mat-icon {
height: 24px;
width: 24px;
font-size: 24px;

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

@ -15,16 +15,18 @@
limitations under the License.
-->
<form [formGroup]="widgetFormGroup" style="width: 1200px;">
<form class="tb-add-widget-dialog" [formGroup]="widgetFormGroup">
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
<div class="tb-add-widget-toolbar">
<h2 fxFlex>{{'widget.add' | translate}}: {{data.widgetInfo.widgetName}}</h2>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap.gt-xs="16px">
<tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" selectMediaBreakpoint="xs"
[(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
</div>
<button mat-icon-button
(click)="cancel()"
@ -32,40 +34,37 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content style="padding: 0;">
<fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;">
<tb-widget-config
[aliasController]="aliasController"
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</fieldset>
<div mat-dialog-content>
<tb-widget-config class="tb-absolute-fill"
[aliasController]="aliasController"
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</div>
<div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
@ -74,14 +73,14 @@
<button *ngIf="!previewMode"
mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
[disabled]="widgetFormGroup.invalid"
(click)="previewMode = true"
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
[disabled]="widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
</div>

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

@ -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.
*/
@import '../../../../../scss/constants';
.tb-add-widget-dialog {
.mat-mdc-dialog-content {
padding: 0;
position: relative;
}
@media #{$mat-gt-xs} {
width: 900px;
.mat-mdc-dialog-content {
height: 900px;
}
}
.tb-add-widget-toolbar {
flex: 1;
min-width: 0;
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 8px;
}
}

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

@ -14,12 +14,12 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnInit, SkipSelf, ViewEncapsulation } from '@angular/core';
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 { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models';
@ -41,7 +41,8 @@ export interface AddWidgetDialogData {
selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html',
providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/],
styleUrls: []
styleUrls: ['./add-widget-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {

25
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -25,8 +25,7 @@
[toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
<div class="tb-dashboard-action-panels" fxLayout="column" fxLayout.gt-sm="row"
fxLayoutAlign="center stretch" fxLayoutAlign.gt-sm="space-between center">
<div class="tb-dashboard-action-panel" fxFlex="auto" fxLayout="row-reverse" [fxHide]="isMobileApp && !isEdit"
fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap.lt-lg="6px" fxLayoutGap.gt-md="12px">
<div class="tb-dashboard-action-panel tb-left-panel" [fxHide]="isMobileApp && !isEdit">
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
</tb-user-menu>
<ng-container *ngIf="isEdit">
@ -74,8 +73,7 @@
<img *ngIf="showDashboardLogo()" [src]="dashboardLogo"
aria-label="dashboard_logo" class="dashboard_logo"/>
</div>
<div class="tb-dashboard-action-panel" fxFlex="1 0 auto" fxLayout="row-reverse"
fxLayoutAlign.gt-sm="start center" fxLayoutAlign="space-between center" fxLayoutGap.lt-lg="6px" fxLayoutGap.gt-md="12px">
<div class="tb-dashboard-action-panel">
<button [fxShow]="showCloseToolbar()" mat-icon-button class="close-action"
matTooltip="{{ 'dashboard.close-toolbar' | translate }}"
matTooltipPosition="below"
@ -235,8 +233,21 @@
</tb-timewindow>
</ng-container>
</ng-template>
<button [fxShow]="isEdit" mat-stroked-button (click)="addWidget($event)">
<mat-icon>add</mat-icon>{{ 'dashboard.add-widget-button-text' | translate }}</button>
<ng-container *ngIf="isEdit">
<button fxHide.gt-xs mat-icon-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
</button>
<button fxHide.lt-sm mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
{{ 'dashboard.add-widget-button-text' | translate }}
</button>
</ng-container>
<tb-filters-edit [fxShow]="!isEdit && displayFilters()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
@ -360,7 +371,7 @@
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" selectMediaBreakpoint="xs" [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>

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

@ -191,7 +191,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
get hideToolbar(): boolean {
return (this.hideToolbarValue || this.hideToolbarSetting()) && !this.isEdit;
return ((this.hideToolbarValue || this.hideToolbarSetting()) && !this.isEdit) || (this.isEditingWidget || this.isAddingWidget);
}
@Input()
@ -1177,6 +1177,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
Widget>(AddWidgetDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
maxWidth: '95vw',
data: {
dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController,

37
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss

@ -126,6 +126,7 @@ tb-dashboard-toolbar {
@media #{$mat-lt-md} {
height: $mobile-toolbar-height;
max-height: $mobile-toolbar-height;
padding: 0 8px !important;
}
.close-action {
@ -150,8 +151,44 @@ tb-dashboard-toolbar {
.tb-dashboard-action-panel {
min-width: 0;
height: $half-mobile-toolbar-height;
flex: 1 0 auto;
display: flex;
flex-direction: row-reverse;
place-content: center space-between;
align-items: center;
&.tb-left-panel {
flex: 1 1 auto;
}
@media #{$mat-lt-md} {
padding-left: 12px;
}
@media #{$mat-xs} {
gap: 3px;
padding-left: 0;
&.tb-left-panel {
padding-left: 12px;
}
}
@media #{$mat-sm} {
gap: 6px;
}
@media #{$mat-md} {
gap: 6px;
}
@media #{$mat-gt-md} {
gap: 12px;
}
@media #{$mat-gt-sm} {
place-content: center flex-start;
&.tb-left-panel {
place-content: center flex-end;
}
height: 46px;
}

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

@ -45,6 +45,8 @@
}
.preview {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;

48
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -32,26 +32,38 @@
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
<div class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<button *ngIf="!previewMode" [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
mat-button color="primary"
(click)="previewMode = true">
<button *ngIf="!previewMode"
fxHide.gt-sm
[disabled]="widgetFormGroup.invalid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
<button [disabled]="(isLoading$ | async) || !widgetFormGroup.dirty"
mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()">
<mat-icon>close</mat-icon>
{{ 'action.decline' | translate }}
</button>
<button [disabled]="(isLoading$ | async) || widgetFormGroup.invalid || !widgetFormGroup.dirty"
mat-flat-button color="primary"
(click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon>
{{ 'action.apply' | translate }}
</div>
<div fxFlex.lt-md class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="12px">
<button *ngIf="!previewMode"
fxHide.lt-md
[disabled]="widgetFormGroup.invalid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<button [disabled]="!widgetFormGroup.dirty"
mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()">
<mat-icon>close</mat-icon>
{{ 'action.decline' | translate }}
</button>
<button [disabled]="widgetFormGroup.invalid || !widgetFormGroup.dirty"
mat-flat-button color="primary"
(click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon>
{{ 'action.apply' | translate }}
</button>
</div>
</div>
</tb-widget-config>
<div *ngIf="previewMode" class="widget-preview-background">
@ -60,7 +72,9 @@
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
</tb-widget-preview>
</div>
</fieldset>

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

@ -16,7 +16,7 @@
:host {
.widget-preview-background {
position: absolute;
top: 72px;
top: 68px;
left: 0;
right: 0;
bottom: 0;

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

@ -21,7 +21,7 @@
<div class="mat-toolbar-tools tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start" [fxHide]="showSearchPane">
<div class="tb-details-title" fxLayout="row" fxLayoutAlign="start center">
<ng-content select=".prefix-title-buttons"></ng-content>
<span>{{ headerTitle }}</span>
<span class="tb-details-title-text">{{ headerTitle }}</span>
</div>
<span class="tb-details-subtitle">{{ headerSubtitle }}</span>
<span style="width: 100%;">

13
ui-ngx/src/app/modules/home/components/details-panel.component.scss

@ -32,16 +32,14 @@
max-height: 120px;
&.tb-details-title-header {
min-width: 0;
padding: 0 16px 0 8px;
}
}
.tb-details-title {
width: inherit;
margin: 20px 8px 0 0;
overflow: hidden;
font-size: 1rem;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
@media #{$mat-gt-sm} {
font-size: 1.5rem;
@ -49,13 +47,16 @@
}
.tb-details-subtitle {
width: inherit;
margin: 10px 0;
overflow: hidden;
font-size: 1rem;
opacity: .8;
}
.tb-details-title-text, .tb-details-subtitle {
width: inherit;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: .8;
}
tb-dashboard {

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

@ -86,8 +86,7 @@
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)">
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [svgIcon]="actionDescriptor.icon"></mat-icon>
<mat-icon *ngIf="!actionDescriptor.isMdiIcon">{{actionDescriptor.icon}}</mat-icon>
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}"
@ -218,7 +217,7 @@
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 48) + 'px',
maxWidth: (cellActionDescriptors.length * 48) + 'px',
width: (cellActionDescriptors.length * 48) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<div [fxHide]="cellActionDescriptors.length !== 1" fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)"
*ngFor="let actionDescriptor of cellActionDescriptors"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
@ -229,7 +228,7 @@
{{actionDescriptor.icon}}</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length">
<div fxHide [fxShow.lt-lg]="cellActionDescriptors.length !== 1" *ngIf="cellActionDescriptors.length">
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">

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

@ -637,6 +637,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
}
}
cellActionDescriptorsUpdated() {
this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors];
}
headerCellStyle(column: EntityColumn<BaseData<HasId>>) {
const index = this.entitiesTableConfig.columns.indexOf(column);
let res = this.headerCellStyleCache[index];

30
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -21,7 +21,7 @@ import {
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models';
import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models';
import { TimePageLink } from '@shared/models/page/page-link';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
@ -41,7 +41,7 @@ import {
} from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import {
EVENT_FILTER_PANEL_DATA,
@ -61,6 +61,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) {
this.eventTypeValue = eventType;
this.updateCellAction();
this.updateColumns(true);
this.updateFilterColumns();
}
@ -84,7 +85,9 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private debugEventTypes: Array<DebugEventType> = null,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
super();
this.loadDataOnInit = false;
this.tableTitle = '';
@ -120,12 +123,12 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.updateColumns();
this.updateCellAction();
this.updateFilterColumns();
this.headerActionDescriptors.push({
name: this.translate.instant('event.clear-filter'),
icon: 'mdi:filter-variant-remove',
isMdiIcon: true,
isEnabled: () => !isEqual(this.filterParams, {}),
onAction: ($event) => {
this.clearFiter($event);
@ -349,6 +352,25 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}
}
updateCellAction() {
this.cellActionDescriptors = [];
switch (this.eventType) {
case DebugEventType.DEBUG_RULE_NODE:
if (this.testButtonLabel) {
this.cellActionDescriptors.push({
name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report',
isEnabled: (entity) => entity.body.type === 'IN' || entity.body.error !== undefined,
onAction: ($event, entity) => {
this.debugEventSelected.next(entity.body);
}
});
}
break;
}
this.getTable()?.cellActionDescriptorsUpdated();
}
showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void {
if ($event) {
$event.stopPropagation();

37
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -17,10 +17,10 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Component, EventEmitter,
Input,
OnDestroy,
OnInit,
OnInit, Output,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -32,9 +32,10 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c
import { EventTableConfig } from './event-table-config';
import { EventService } from '@core/http/event.service';
import { DialogService } from '@core/services/dialog.service';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { DebugEventType, EventBody, EventType } from '@shared/models/event.models';
import { Overlay } from '@angular/cdk/overlay';
import { Subscription } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
@Component({
selector: 'tb-event-table',
@ -59,6 +60,10 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
dirtyValue = false;
entityIdValue: EntityId;
get active(): boolean {
return this.activeValue;
}
@Input()
set active(active: boolean) {
if (this.activeValue !== active) {
@ -83,6 +88,28 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
private functionTestButtonLabelValue: string;
get functionTestButtonLabel(): string {
return this.functionTestButtonLabelValue;
}
@Input()
set functionTestButtonLabel(value: string) {
if (isNotEmptyStr(value)) {
this.functionTestButtonLabelValue = value;
} else {
this.functionTestButtonLabelValue = '';
}
if (this.eventTableConfig) {
this.eventTableConfig.testButtonLabel = this.functionTestButtonLabel;
this.eventTableConfig.updateCellAction();
}
}
@Output()
debugEventSelected = new EventEmitter<EventBody>();
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
eventTableConfig: EventTableConfig;
@ -114,7 +141,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.debugEventTypes,
this.overlay,
this.viewContainerRef,
this.cd
this.cd,
this.functionTestButtonLabel,
this.debugEventSelected
);
}

2
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html

@ -35,7 +35,7 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value && !disabled && editProfileEnabled"
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'asset-profile.edit' | translate }}"

2
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -35,7 +35,7 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && editProfileEnabled"
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'device-profile.edit' | translate }}"

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

@ -20,12 +20,12 @@
<nav fxFlex *ngIf="!routerOutlet?.activatedRouteData?.hideTabs && !hideCurrentTabs && (tabs$ | async).length > 1" mat-tab-nav-bar mat-stretch-tabs="false" class="tb-router-tabs" [tabPanel]="tabPanel">
<a *ngFor="let tab of tabs$ | async"
routerLink="{{tab.path}}"
replaceUrl="{{ replaceUrl }}"
routerLinkActive
#rla="routerLinkActive"
mat-tab-link
[active]="rla.isActive">
<mat-icon *ngIf="!tab.isMdiIcon && tab.icon !== null" class="tb-mat-18">{{tab.icon}}</mat-icon>
<mat-icon *ngIf="tab.isMdiIcon && tab.icon !== null" [svgIcon]="tab.icon" class="tb-mat-18"></mat-icon>
<tb-icon *ngIf="tab.icon !== null" class="tb-mat-18">{{tab.icon}}</tb-icon>
<span>{{tab.name | translate}}</span>
</a>
</nav>

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

@ -26,7 +26,7 @@
line-height: 40px;
min-width: 200px;
border-bottom: none;
mat-icon {
.mat-icon {
margin-right: 8px;
margin-left: 0;
}

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

Loading…
Cancel
Save