Browse Source

Merge branch 'master' into feature/scada-symbol

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
19630f6c36
  1. 4
      application/src/main/data/json/system/widget_types/mobile_app_qr_code.json
  2. 2
      application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
  3. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  4. 5
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  5. 15
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  6. 101
      application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java
  7. 3
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  8. 36
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  9. 15
      application/src/main/resources/thingsboard.yml
  10. 6
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  11. 68
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  12. 90
      application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java
  13. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java
  14. 1
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java
  16. 2
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java
  17. 17
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java
  18. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  19. 6
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  20. 2
      common/proto/src/main/proto/queue.proto
  21. 1
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java
  22. 1
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java
  23. 55
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java
  24. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java
  25. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java
  26. 37
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  27. 12
      dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java
  28. 18
      dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java
  29. 7
      dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java
  30. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  31. 14
      dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java
  32. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java
  33. 8
      dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java
  34. 2
      packaging/java/scripts/windows/install.bat
  35. 2
      packaging/java/scripts/windows/install_dev_db.bat
  36. 11
      ui-ngx/src/app/core/http/entity.service.ts
  37. 30
      ui-ngx/src/app/core/http/queue.service.ts
  38. 9
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  39. 7
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts
  40. 12
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  41. 3
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  42. 9
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  43. 17
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.html
  44. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts
  45. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html
  46. 32
      ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html
  47. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts
  48. 3
      ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts
  49. 14
      ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts
  50. 21
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts
  51. 2
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  52. 18
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html
  53. 30
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss
  54. 12
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts
  55. 17
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html
  56. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts
  57. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html
  58. 35
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html
  59. 44
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss
  60. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts
  61. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss
  62. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  63. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  64. 53
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss
  65. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts
  66. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html
  67. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts
  68. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html
  69. 36
      ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html
  70. 14
      ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts
  71. 5
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  72. 5
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
  73. 6
      ui-ngx/src/app/shared/models/entity-type.models.ts
  74. 4
      ui-ngx/src/app/shared/models/limited-api.models.ts
  75. 6
      ui-ngx/src/app/shared/models/mobile-app.models.ts
  76. 5
      ui-ngx/src/app/shared/models/queue.models.ts
  77. 13
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  78. 2
      ui-ngx/src/app/shared/models/widget.models.ts
  79. 2
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  80. 2
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  81. 29
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/src/main/data/json/system/widget_types/mobile_app_qr_code.json

@ -11,13 +11,13 @@
"resources": [],
"templateHtml": "<tb-mobile-app-qrcode-widget\n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-mobile-app-qrcode-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};",
"controllerScript": "self.onInit = function() {\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-mobile-app-qr-code-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-mobile-app-qr-code-basic-config",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useSystemSettings\":true,\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"enableFullscreen\":false,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}"
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useSystemSettings\":true,\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\"},\"title\":\"Download mobile app\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"enableFullscreen\":false,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"1.5\"},\"titleColor\":\"#000000\",\"iconSize\":\"26px\",\"titleIcon\":\"qr_code\",\"iconColor\":\"#000000\",\"actions\":{}}"
},
"tags": [
"ios",

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

@ -68,7 +68,7 @@ public class ComponentDescriptorController extends BaseController {
@RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET)
@ResponseBody
public List<ComponentDescriptor> getComponentDescriptorsByType(
@Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = {"ENRICHMENT", "FILTER,TRANSFORMATION", "ACTION,EXTERNAL"}, requiredMode = Schema.RequiredMode.REQUIRED))
@Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = {"ENRICHMENT", "FILTER", "TRANSFORMATION", "ACTION", "EXTERNAL"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("componentType") String strComponentType,
@Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"}))
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {

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

@ -107,7 +107,9 @@ public class ControllerConstants {
protected static final String ASSET_PROFILE_INFO_DESCRIPTION = "Asset Profile Info is a lightweight object that includes main information about Asset Profile. ";
protected static final String QUEUE_SERVICE_TYPE_DESCRIPTION = "Service type (implemented only for the TB-RULE-ENGINE)";
protected static final String QUEUE_QUEUE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the queue name.";
protected static final String QUEUE_STATS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the queue name or service id.";
protected static final String QUEUE_ID_PARAM_DESCRIPTION = "A string value representing the queue id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String QUEUE_STATS_ID_PARAM_DESCRIPTION = "A string value representing the queue stats id. For example, '687f294c-42b6-435a-983c-b7beff2784f9'";
protected static final String QUEUE_NAME_PARAM_DESCRIPTION = "A string value representing the queue id. For example, 'Main'";
protected static final String OTA_PACKAGE_INFO_DESCRIPTION = "OTA Package Info is a lightweight object that includes main information about the OTA Package excluding the heavyweight data. ";
protected static final String OTA_PACKAGE_DESCRIPTION = "OTA Package is a heavyweight object that includes main information about the OTA Package and also data. ";

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

@ -25,6 +25,7 @@ import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
@ -95,7 +96,7 @@ public class ImageController extends BaseController {
private static final String IMAGE_KEY_PARAM_DESCRIPTION = "Image resource key, for example thermostats_dashboard_background.jpeg";
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping("/api/image")
@PostMapping(value = "/api/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo uploadImage(@RequestPart MultipartFile file,
@RequestPart(required = false) String title,
@RequestPart(required = false) String imageSubType) throws Exception {
@ -128,7 +129,7 @@ public class ImageController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(IMAGE_URL)
@PutMapping(value = IMAGE_URL, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo updateImage(@Parameter(description = IMAGE_TYPE_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"tenant", "system"}), required = true)
@PathVariable String type,
@Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true)

15
application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java

@ -58,6 +58,8 @@ public class MobileApplicationController extends BaseController {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
@Value("${mobileApp.domain:demo.thingsboard.io}")
private String defaultAppDomain;
public static final String ASSET_LINKS_PATTERN = "[{\n" +
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" +
@ -81,11 +83,8 @@ public class MobileApplicationController extends BaseController {
" }\n" +
"}";
public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app";
public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695";
public static final String SECRET = "secret";
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key";
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io";
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s";
private final SystemSecurityService systemSecurityService;
@ -151,7 +150,7 @@ public class MobileApplicationController extends BaseController {
if (!mobileAppSettings.isUseDefaultApp()) {
appDomain = platformDomain;
} else {
appDomain = DEFAULT_APP_DOMAIN;
appDomain = defaultAppDomain;
}
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl);
if (!appDomain.equals(platformDomain)) {
@ -171,13 +170,17 @@ public class MobileApplicationController extends BaseController {
@GetMapping(value = "/api/noauth/qr")
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
boolean useDefaultApp = mobileAppSettings.isUseDefaultApp();
String googlePlayLink = useDefaultApp ? mobileAppSettings.getDefaultGooglePlayLink() : mobileAppSettings.getAndroidConfig().getStoreLink();
String appStoreLink = useDefaultApp ? mobileAppSettings.getDefaultAppStoreLink() : mobileAppSettings.getIosConfig().getStoreLink();
if (userAgent.contains("Android")) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", ANDROID_APPLICATION_STORE_LINK)
.header("Location", googlePlayLink)
.build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", APPLE_APPLICATION_STORE_LINK)
.header("Location", appStoreLink)
.build();
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)

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

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.QueueStatsId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.QUEUE_STATS_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.QUEUE_STATS_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class QueueStatsController extends BaseController {
private final QueueStatsService queueStatsService;
@ApiOperation(value = "Get Queue Stats entities (getTenantQueueStats)",
notes = "Returns a page of queue stats objects that are designed to collect queue statistics for every service. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats", params = {"pageSize", "page"})
public PageData<QueueStats> getTenantQueueStats(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = QUEUE_STATS_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return queueStatsService.findByTenantId(getTenantId(), pageLink);
}
@ApiOperation(value = "Get Queue stats entity by id (getQueueStatsById)",
notes = "Fetch the Queue stats object based on the provided Queue stats id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats/{queueStatsId}")
public QueueStats getQueueStatsById(@Parameter(description = QUEUE_STATS_ID_PARAM_DESCRIPTION)
@PathVariable("queueStatsId") String queueStatsIdStr) throws ThingsboardException {
checkParameter("queueStatsId", queueStatsIdStr);
QueueStatsId queueStatsId = new QueueStatsId(UUID.fromString(queueStatsIdStr));
return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId));
}
@ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)",
notes = "Fetch the Queue stats objects based on the provided ids. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats", params = {"queueStatsIds"})
public List<QueueStats> getQueueStatsByIds(
@Parameter(description = "A list of queue stats ids, separated by comma ','", required = true)
@RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException {
checkArrayParameter("queueStatsIds", strQueueStatsIds);
List<QueueStatsId> queueStatsIds = new ArrayList<>();
for (String queueStatsId : strQueueStatsIds) {
queueStatsIds.add(new QueueStatsId(toUUID(queueStatsId)));
}
return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds);
}
}

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

@ -141,6 +141,9 @@ public class TenantProfileController extends BaseController {
" \"transportGatewayMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayDeviceMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
" \"maxTransportMessages\": 10000000,\n" +
" \"maxTransportDataPoints\": 10000000,\n" +
" \"maxREExecutions\": 4000000,\n" +

36
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -105,17 +105,35 @@ public class DefaultDataUpdateService implements DataUpdateService {
var configurationOpt = tenantProfile.getProfileConfiguration();
configurationOpt.ifPresent(configuration -> {
boolean updated = false;
if (configuration.getTransportDeviceMsgRateLimit() != null && configuration.getTransportGatewayMsgRateLimit() == null) {
configuration.setTransportGatewayMsgRateLimit(configuration.getTransportDeviceMsgRateLimit());
updated = true;
if (configuration.getTransportDeviceMsgRateLimit() != null) {
if (configuration.getTransportGatewayMsgRateLimit() == null) {
configuration.setTransportGatewayMsgRateLimit(configuration.getTransportDeviceMsgRateLimit());
updated = true;
}
if (configuration.getTransportGatewayDeviceMsgRateLimit() == null) {
configuration.setTransportGatewayDeviceMsgRateLimit(configuration.getTransportDeviceMsgRateLimit());
updated = true;
}
}
if (configuration.getTransportDeviceTelemetryMsgRateLimit() != null && configuration.getTransportGatewayTelemetryMsgRateLimit() == null) {
configuration.setTransportGatewayTelemetryMsgRateLimit(configuration.getTransportDeviceTelemetryMsgRateLimit());
updated = true;
if (configuration.getTransportDeviceTelemetryMsgRateLimit() != null) {
if (configuration.getTransportGatewayTelemetryMsgRateLimit() == null) {
configuration.setTransportGatewayTelemetryMsgRateLimit(configuration.getTransportDeviceTelemetryMsgRateLimit());
updated = true;
}
if (configuration.getTransportGatewayDeviceTelemetryMsgRateLimit() == null) {
configuration.setTransportGatewayDeviceTelemetryMsgRateLimit(configuration.getTransportDeviceTelemetryMsgRateLimit());
updated = true;
}
}
if (configuration.getTransportDeviceTelemetryDataPointsRateLimit() != null && configuration.getTransportGatewayTelemetryDataPointsRateLimit() == null) {
configuration.setTransportGatewayTelemetryDataPointsRateLimit(configuration.getTransportDeviceTelemetryDataPointsRateLimit());
updated = true;
if (configuration.getTransportDeviceTelemetryDataPointsRateLimit() != null) {
if (configuration.getTransportGatewayTelemetryDataPointsRateLimit() == null) {
configuration.setTransportGatewayTelemetryDataPointsRateLimit(configuration.getTransportDeviceTelemetryDataPointsRateLimit());
updated = true;
}
if (configuration.getTransportGatewayDeviceTelemetryDataPointsRateLimit() == null) {
configuration.setTransportGatewayDeviceTelemetryDataPointsRateLimit(configuration.getTransportDeviceTelemetryDataPointsRateLimit());
updated = true;
}
}
if (updated) {
try {

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

@ -1463,9 +1463,9 @@ queue:
- key: max.poll.interval.ms
# Example of specific consumer properties value per topic for VC
value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}"
# tb_rule_engine.sq:
# - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"
# tb_rule_engine.sq:
# - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"
tb_housekeeper:
# Consumer properties for Housekeeper tasks topic
- key: max.poll.records
@ -1794,3 +1794,12 @@ management:
elasticsearch:
# Enable the org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator.doHealthCheck
enabled: "false"
# Mobile application settings for Thingsboard mobile application
mobileApp:
# Server domain name for Thingsboard Live mobile application
domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}"
# Link to Google Play store for Thingsboard Live mobile application
googlePlayLink: "${TB_MOBILE_APP_GOOGLE_PLAY_LINK:https://play.google.com/store/apps/details?id=org.thingsboard.demo.app}"
# Link to App Store for Thingsboard Live mobile application
appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}"

6
application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java

@ -198,9 +198,9 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
});
ruleEngineStatisticsService.reportQueueStats(System.currentTimeMillis(), testStats);
List<QueueStats> queueStatsList = queueStatsService.findByTenantId(tenantId);
assertThat(queueStatsList).hasSize(1);
QueueStats queueStats = queueStatsList.get(0);
PageData<QueueStats> queueStatsList = queueStatsService.findByTenantId(tenantId, new PageLink(10));
assertThat(queueStatsList.getData()).hasSize(1);
QueueStats queueStats = queueStatsList.getData().get(0);
assertThat(queueStats.getQueueName()).isEqualTo(queue.getName());
ArgumentCaptor<Long> ttlCaptor = ArgumentCaptor.forClass(Long.class);

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

@ -22,9 +22,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
@ -33,6 +36,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
@ -76,6 +80,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class EntityQueryControllerTest extends AbstractControllerTest {
private static final String CUSTOMER_USER_EMAIL = "entityQueryCustomer@thingsboard.org";
private static final String TENANT_PASSWORD = "testPassword1";
private static final String CUSTOMER_USER_PASSWORD = "customer";
private static final String TENANT_EMAIL = "entityQueryTenant@thingsboard.org";
private Tenant savedTenant;
private User tenantAdmin;
@ -94,11 +103,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setEmail(TENANT_EMAIL);
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
tenantAdmin = createUserAndLogin(tenantAdmin, TENANT_PASSWORD);
}
@After
@ -807,6 +816,61 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
checkEntitiesByQuery(customerEntitiesQuery, 0, null, null);
}
@Test
public void testFindCustomerDashboards() throws Exception {
Dashboard dashboard = new Dashboard();
dashboard.setTitle("My dashboard");
Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
Customer customer = new Customer();
customer.setTitle("My customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
//assign dashboard
doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
// check entity data query by customer
User customerUser = new User();
customerUser.setAuthority(Authority.CUSTOMER_USER);
customerUser.setTenantId(savedTenant.getId());
customerUser.setCustomerId(savedCustomer.getId());
customerUser.setEmail(CUSTOMER_USER_EMAIL);
createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
EntityTypeFilter filter = new EntityTypeFilter();
filter.setEntityType(EntityType.DASHBOARD);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> data =
doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(1, data.getTotalElements());
Assert.assertEquals(1, data.getTotalPages());
Assert.assertEquals(1, data.getData().size());
// unnassign dashboard
login(TENANT_EMAIL, TENANT_PASSWORD);
doDelete("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
PageData<EntityData> dataAfterUnassign =
doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(0, dataAfterUnassign.getTotalElements());
Assert.assertEquals(0, dataAfterUnassign.getTotalPages());
Assert.assertEquals(0, dataAfterUnassign.getData().size());
}
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception {
Awaitility.await()
.alias("data by query")

90
application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.mqtt;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -25,7 +26,7 @@ import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -34,12 +35,14 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY;
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE;
@DaoSqlTest
@TestPropertySource(properties = {
@ -48,11 +51,14 @@ import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESS
})
public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
private static final String TOPIC = "v1/gateway/telemetry";
private static final String GATEWAY_TOPIC = "v1/gateway/telemetry";
private static final String DEVICE_TOPIC = "v1/devices/me/telemetry";
private static final String DEVICE_A = "DeviceA";
private static final String DEVICE_B = "DeviceB";
private DeviceId gatewayId;
private static final String DEVICE_PAYLOAD = "{\"temperature\": 42}";
private Device gateway;
private String gatewayAccessToken;
@SpyBean
@ -71,6 +77,10 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
profileConfiguration.setTransportGatewayTelemetryMsgRateLimit(null);
profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit(null);
profileConfiguration.setTransportGatewayDeviceMsgRateLimit(null);
profileConfiguration.setTransportGatewayDeviceTelemetryMsgRateLimit(null);
profileConfiguration.setTransportGatewayDeviceTelemetryDataPointsRateLimit(null);
doPost("/api/tenantProfile", tenantProfile);
loginTenantAdmin();
@ -107,24 +117,24 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_A));
client.publishAndWait(GATEWAY_TOPIC, getGatewayPayload(DEVICE_A));
loginTenantAdmin();
Device deviceA = getDeviceByName(DEVICE_A);
var deviceATrigger = createRateLimitsTrigger(deviceA);
var deviceATrigger = createRateLimitsTrigger(deviceA, TRANSPORT_MESSAGES_PER_GATEWAY);
Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(deviceATrigger));
try {
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_B));
client.publishAndWait(GATEWAY_TOPIC, getGatewayPayload(DEVICE_B));
} catch (Exception t) {
}
Device deviceB = getDeviceByName(DEVICE_B);
var deviceBTrigger = createRateLimitsTrigger(deviceB);
var deviceBTrigger = createRateLimitsTrigger(deviceB, TRANSPORT_MESSAGES_PER_GATEWAY);
Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(deviceBTrigger);
@ -133,6 +143,62 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
}
}
@Test
public void transportGatewayDeviceMsgRateLimitTest() throws Exception {
transportGatewayDeviceRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayDeviceMsgRateLimit("3:600"));
}
@Test
public void transportGatewayDeviceTelemetryMsgRateLimitTest() throws Exception {
transportGatewayDeviceRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayDeviceTelemetryMsgRateLimit("1:600"));
}
@Test
public void transportGatewayDeviceTelemetryDataPointsRateLimitTest() throws Exception {
transportGatewayDeviceRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayDeviceTelemetryDataPointsRateLimit("1:600"));
}
private void transportGatewayDeviceRateLimitTest(Consumer<DefaultTenantProfileConfiguration> profileConfiguration) throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
Assert.assertNotNull(tenantProfile);
profileConfiguration.accept((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration());
doPost("/api/tenantProfile", tenantProfile);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
client.publishAndWait(DEVICE_TOPIC, DEVICE_PAYLOAD.getBytes());
client.disconnect();
var gatewayTrigger = createRateLimitsTrigger(gateway, TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE);
Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(gatewayTrigger));
loginTenantAdmin();
client = new MqttTestClient();
try {
client.connectAndWait(gatewayAccessToken);
client.publishAndWait(DEVICE_TOPIC, DEVICE_PAYLOAD.getBytes());
if (client.isConnected()) {
client.disconnect();
}
} catch (Exception t) {
}
Awaitility.await()
.atMost(2, TimeUnit.SECONDS)
.untilAsserted(() -> Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(gatewayTrigger));
if (client.isConnected()) {
client.disconnect();
}
}
private void createGateway() throws Exception {
Device device = new Device();
device.setName("gateway");
@ -141,7 +207,7 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
device.setAdditionalInfo(additionalInfo);
device = doPost("/api/device", device, Device.class);
assertNotNull(device);
gatewayId = device.getId();
var gatewayId = device.getId();
assertNotNull(gatewayId);
DeviceCredentials deviceCredentials = doGet("/api/device/" + gatewayId + "/credentials", DeviceCredentials.class);
@ -149,6 +215,8 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
assertEquals(gatewayId, deviceCredentials.getDeviceId());
gatewayAccessToken = deviceCredentials.getCredentialsId();
assertNotNull(gatewayAccessToken);
this.gateway = device;
}
private Device getDeviceByName(String deviceName) throws Exception {
@ -158,13 +226,13 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
}
private byte[] getGatewayPayload(String deviceName) {
return String.format("{\"%s\": [{\"values\": {\"temperature\": 42}}]}", deviceName).getBytes();
return String.format("{\"%s\": [{\"values\": %s}]}", deviceName, DEVICE_PAYLOAD).getBytes();
}
private RateLimitsTrigger createRateLimitsTrigger(Device device) {
private RateLimitsTrigger createRateLimitsTrigger(Device device, LimitedApi limitedApi) {
return RateLimitsTrigger.builder()
.tenantId(tenantId)
.api(TRANSPORT_MESSAGES_PER_GATEWAY)
.api(limitedApi)
.limitLevel(device.getId())
.limitLevelEntityName(device.getName())
.build();

6
common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java

@ -17,6 +17,8 @@ package org.thingsboard.server.dao.queue;
import org.thingsboard.server.common.data.id.QueueStatsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.dao.entity.EntityDaoService;
@ -28,8 +30,10 @@ public interface QueueStatsService extends EntityDaoService {
QueueStats findQueueStatsById(TenantId tenantId, QueueStatsId queueStatsId);
List<QueueStats> findQueueStatsByIds(TenantId tenantId, List<QueueStatsId> queueStatsId);
QueueStats findByTenantIdAndNameAndServiceId(TenantId tenantId, String queueName, String serviceId);
List<QueueStats> findByTenantId(TenantId tenantId);
PageData<QueueStats> findByTenantId(TenantId tenantId, PageLink pageLink);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -41,6 +41,7 @@ public enum LimitedApi {
TRANSPORT_MESSAGES_PER_TENANT("transport messages", true),
TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false),
TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false),
TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE("transport messages per gateway device", false),
EMAILS("emails sending", true);
private Function<DefaultTenantProfileConfiguration, String> configExtractor;

2
common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java

@ -34,5 +34,7 @@ public class AndroidConfig {
private String appPackage;
@NoXss
private String sha256CertFingerprints;
@NoXss
private String storeLink;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java

@ -32,5 +32,7 @@ public class IosConfig {
private boolean enabled;
@NoXss
private String appId;
@NoXss
private String storeLink;
}

17
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java

@ -15,29 +15,46 @@
*/
package org.thingsboard.server.common.data.mobile;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.MobileAppSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements HasTenantId {
private static final long serialVersionUID = 2628323657987010348L;
@Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY)
private TenantId tenantId;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of application: true means use default Thingsboard app", example = "true")
private boolean useDefaultApp;
@Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Android mobile app configuration.")
private AndroidConfig androidConfig;
@Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Ios mobile app configuration.")
private IosConfig iosConfig;
@Valid
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "QR code config configuration.")
private QRCodeConfig qrCodeConfig;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String defaultGooglePlayLink;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String defaultAppStoreLink;
public MobileAppSettings() {
}
public MobileAppSettings(MobileAppSettingsId id) {
super(id);
}

3
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -50,6 +50,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String transportGatewayMsgRateLimit;
private String transportGatewayTelemetryMsgRateLimit;
private String transportGatewayTelemetryDataPointsRateLimit;
private String transportGatewayDeviceMsgRateLimit;
private String transportGatewayDeviceTelemetryMsgRateLimit;
private String transportGatewayDeviceTelemetryDataPointsRateLimit;
private String tenantEntityExportRateLimit;
private String tenantEntityImportRateLimit;

6
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -91,6 +91,8 @@ import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.GATEWAY_PARAMETER;
@Slf4j
public class ProtoUtils {
@ -1020,6 +1022,10 @@ public class ProtoUtils {
.setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits())
.setAdditionalInfo(JacksonUtil.toString(device.getAdditionalInfo()));
if (device.getAdditionalInfo().has(GATEWAY_PARAMETER)) {
builder.setIsGateway(device.getAdditionalInfo().get(GATEWAY_PARAMETER).booleanValue());
}
PowerSavingConfiguration psmConfiguration = switch (device.getDeviceData().getTransportConfiguration().getType()) {
case LWM2M -> (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
case COAP -> (CoapDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();

2
common/proto/src/main/proto/queue.proto

@ -98,6 +98,7 @@ message SessionInfoProto {
int64 customerIdLSB = 15;
optional int64 gatewayIdMSB = 16;
optional int64 gatewayIdLSB = 17;
bool isGateway = 18;
}
enum SessionEvent {
@ -184,6 +185,7 @@ message DeviceInfoProto {
int64 edrxCycle = 13;
int64 psmActivityTimer = 14;
int64 pagingTransmissionWindow = 15;
bool isGateway = 16;
}
message DeviceProto {

1
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java

@ -46,6 +46,7 @@ public class SessionInfoCreator {
.setDeviceType(msg.getDeviceInfo().getDeviceType())
.setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileId().getId().getMostSignificantBits())
.setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileId().getId().getLeastSignificantBits())
.setIsGateway(msg.getDeviceInfo().isGateway())
.build();
}

1
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java

@ -38,4 +38,5 @@ public class TransportDeviceInfo implements Serializable {
private Long edrxCycle;
private Long psmActivityTimer;
private Long pagingTransmissionWindow;
private boolean gateway;
}

55
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java

@ -41,6 +41,7 @@ import java.util.function.BiConsumer;
import java.util.function.Function;
import static org.thingsboard.server.common.transport.limits.TransportLimitsType.DEVICE_LIMITS;
import static org.thingsboard.server.common.transport.limits.TransportLimitsType.GATEWAY_DEVICE_LIMITS;
import static org.thingsboard.server.common.transport.limits.TransportLimitsType.GATEWAY_LIMITS;
import static org.thingsboard.server.common.transport.limits.TransportLimitsType.TENANT_LIMITS;
@ -53,9 +54,11 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
private final ConcurrentMap<TenantId, Boolean> tenantAllowed = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<DeviceId>> tenantDevices = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<DeviceId>> tenantGateways = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<DeviceId>> tenantGatewayDevices = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, EntityTransportRateLimits> perTenantLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, EntityTransportRateLimits> perDeviceLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, EntityTransportRateLimits> perGatewayLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, EntityTransportRateLimits> perGatewayDeviceLimits = new ConcurrentHashMap<>();
private final Map<InetAddress, InetAddressRateLimitStats> ipMap = new ConcurrentHashMap<>();
private final TransportTenantProfileCache tenantProfileCache;
@ -72,21 +75,23 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
}
@Override
public TbPair<EntityType, Boolean> checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints) {
public TbPair<EntityType, Boolean> checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints, boolean isGateway) {
if (!tenantAllowed.getOrDefault(tenantId, Boolean.TRUE)) {
return TbPair.of(EntityType.API_USAGE_STATE, false);
}
if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) {
return TbPair.of(EntityType.TENANT, false);
}
if (isGateway && !checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) {
return TbPair.of(EntityType.DEVICE, true);
}
if (gatewayId != null && !checkEntityRateLimit(dataPoints, getGatewayRateLimits(tenantId, gatewayId))) {
return TbPair.of(EntityType.DEVICE, true);
}
if (deviceId != null && !checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) {
if (!isGateway && deviceId != null && !checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) {
return TbPair.of(EntityType.DEVICE, false);
}
return null;
}
@ -104,8 +109,9 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(update.getProfile(), TENANT_LIMITS);
EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(update.getProfile(), DEVICE_LIMITS);
EntityTransportRateLimits gatewayRateLimitPrototype = createRateLimits(update.getProfile(), GATEWAY_LIMITS);
EntityTransportRateLimits gatewayDeviceRateLimitPrototype = createRateLimits(update.getProfile(), GATEWAY_DEVICE_LIMITS);
for (TenantId tenantId : update.getAffectedTenants()) {
update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype);
update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype, gatewayDeviceRateLimitPrototype);
}
}
@ -114,26 +120,34 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), TENANT_LIMITS);
EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), DEVICE_LIMITS);
EntityTransportRateLimits gatewayRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_LIMITS);
update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype);
EntityTransportRateLimits gatewayDeviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_DEVICE_LIMITS);
update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype, gatewayDeviceRateLimitPrototype);
}
private void update(TenantId tenantId, EntityTransportRateLimits tenantRateLimitPrototype,
EntityTransportRateLimits deviceRateLimitPrototype, EntityTransportRateLimits gatewayRateLimitPrototype) {
private void update(TenantId tenantId, EntityTransportRateLimits tenantRateLimitPrototype, EntityTransportRateLimits deviceRateLimitPrototype,
EntityTransportRateLimits gatewayRateLimitPrototype, EntityTransportRateLimits gatewayDeviceRateLimitPrototype) {
mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put);
getTenantDevices(tenantId).forEach(deviceId -> mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put));
getTenantGateways(tenantId).forEach(deviceId -> mergeLimits(deviceId, gatewayRateLimitPrototype, perGatewayLimits::get, perGatewayLimits::put));
getTenantGateways(tenantId).forEach(gatewayId -> mergeLimits(gatewayId, gatewayRateLimitPrototype, perGatewayLimits::get, perGatewayLimits::put));
getTenantGatewayDevices(tenantId).forEach(gatewayId -> mergeLimits(gatewayId, gatewayDeviceRateLimitPrototype, perGatewayDeviceLimits::get, perGatewayDeviceLimits::put));
}
@Override
public void remove(TenantId tenantId) {
perTenantLimits.remove(tenantId);
tenantDevices.remove(tenantId);
tenantGateways.remove(tenantId);
tenantGatewayDevices.remove(tenantId);
}
@Override
public void remove(DeviceId deviceId) {
perDeviceLimits.remove(deviceId);
perGatewayLimits.remove(deviceId);
perGatewayDeviceLimits.remove(deviceId);
tenantDevices.values().forEach(set -> set.remove(deviceId));
tenantGateways.values().forEach(set -> set.remove(deviceId));
tenantGatewayDevices.values().forEach(set -> set.remove(deviceId));
}
@Override
@ -273,6 +287,11 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
telemetryMsgRateLimit = newLimit(profile.getTransportGatewayTelemetryMsgRateLimit());
telemetryDpRateLimit = newLimit(profile.getTransportGatewayTelemetryDataPointsRateLimit());
}
case GATEWAY_DEVICE_LIMITS -> {
regularMsgRateLimit = newLimit(profile.getTransportGatewayDeviceMsgRateLimit());
telemetryMsgRateLimit = newLimit(profile.getTransportGatewayDeviceTelemetryMsgRateLimit());
telemetryDpRateLimit = newLimit(profile.getTransportGatewayDeviceTelemetryDataPointsRateLimit());
}
default -> throw new IllegalStateException("Unknown limits type: " + limitsType);
}
@ -296,10 +315,18 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
});
}
private EntityTransportRateLimits getGatewayRateLimits(TenantId tenantId, DeviceId deviceId) {
return perGatewayLimits.computeIfAbsent(deviceId, k -> {
private EntityTransportRateLimits getGatewayRateLimits(TenantId tenantId, DeviceId gatewayId) {
return perGatewayLimits.computeIfAbsent(gatewayId, k -> {
EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_LIMITS);
getTenantGateways(tenantId).add(deviceId);
getTenantGateways(tenantId).add(gatewayId);
return limits;
});
}
private EntityTransportRateLimits getGatewayDeviceRateLimits(TenantId tenantId, DeviceId gatewayId) {
return perGatewayDeviceLimits.computeIfAbsent(gatewayId, k -> {
EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_DEVICE_LIMITS);
getTenantGatewayDevices(tenantId).add(gatewayId);
return limits;
});
}
@ -312,4 +339,8 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
return tenantGateways.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet());
}
private Set<DeviceId> getTenantGatewayDevices(TenantId tenantId) {
return tenantGatewayDevices.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet());
}
}

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.transport.limits;
public enum TransportLimitsType {
TENANT_LIMITS, DEVICE_LIMITS, GATEWAY_LIMITS
TENANT_LIMITS, DEVICE_LIMITS, GATEWAY_LIMITS, GATEWAY_DEVICE_LIMITS
}

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java

@ -25,7 +25,7 @@ import java.net.InetSocketAddress;
public interface TransportRateLimitService {
TbPair<EntityType, Boolean> checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints);
TbPair<EntityType, Boolean> checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints, boolean isGateway);
void update(TenantProfileUpdateResult update);

37
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -429,8 +429,8 @@ public class DefaultTransportService extends TransportActivityManager implements
@Override
public void process(TenantId tenantId, TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback<GetOrCreateDeviceFromGatewayResponse> callback) {
log.trace("Processing msg: {}", requestMsg);
DeviceId gatewayid = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB()));
if (!checkLimits(tenantId, gatewayid, null, requestMsg.getDeviceName(), requestMsg, callback, 0)) {
DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB()));
if (!checkLimits(tenantId, gatewayId, null, requestMsg.getDeviceName(), requestMsg, callback, 0, false)) {
return;
}
@ -476,6 +476,7 @@ public class DefaultTransportService extends TransportActivityManager implements
tdi.setAdditionalInfo(di.getAdditionalInfo());
tdi.setDeviceName(di.getDeviceName());
tdi.setDeviceType(di.getDeviceType());
tdi.setGateway(di.getIsGateway());
if (StringUtils.isNotEmpty(di.getPowerMode())) {
tdi.setPowerMode(PowerMode.valueOf(di.getPowerMode()));
tdi.setEdrxCycle(di.getEdrxCycle());
@ -838,15 +839,15 @@ public class DefaultTransportService extends TransportActivityManager implements
gatewayId = new DeviceId(new UUID(sessionInfo.getGatewayIdMSB(), sessionInfo.getGatewayIdLSB()));
}
return checkLimits(tenantId, gatewayId, deviceId, sessionInfo.getDeviceName(), msg, callback, dataPoints);
return checkLimits(tenantId, gatewayId, deviceId, sessionInfo.getDeviceName(), msg, callback, dataPoints, sessionInfo.getIsGateway());
}
private boolean checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, String deviceName, Object msg, TransportServiceCallback<?> callback, int dataPoints) {
private boolean checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, String deviceName, Object msg, TransportServiceCallback<?> callback, int dataPoints, boolean isGateway) {
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Processing msg: {}", tenantId, deviceName, msg);
}
var rateLimitedPair = rateLimitService.checkLimits(tenantId, gatewayId, deviceId, dataPoints);
var rateLimitedPair = rateLimitService.checkLimits(tenantId, gatewayId, deviceId, dataPoints, isGateway);
if (rateLimitedPair == null) {
return true;
} else {
@ -856,9 +857,15 @@ public class DefaultTransportService extends TransportActivityManager implements
}
if (rateLimitedEntityType == EntityType.DEVICE || rateLimitedEntityType == EntityType.TENANT) {
LimitedApi limitedApi =
rateLimitedEntityType == EntityType.TENANT ? LimitedApi.TRANSPORT_MESSAGES_PER_TENANT :
rateLimitedPair.getSecond() ? LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY : LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE;
LimitedApi limitedApi;
if (rateLimitedEntityType == EntityType.TENANT) {
limitedApi = LimitedApi.TRANSPORT_MESSAGES_PER_TENANT;
} else if (rateLimitedPair.getSecond()) {
limitedApi = isGateway ? LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE : LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY;
} else {
limitedApi = LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE;
}
EntityId limitLevel = rateLimitedEntityType == EntityType.DEVICE ? deviceId == null ? gatewayId : deviceId : tenantId;
@ -1023,16 +1030,20 @@ public class DefaultTransportService extends TransportActivityManager implements
} else {
newDeviceProfile = null;
}
JsonNode deviceAdditionalInfo = device.getAdditionalInfo();
boolean isGateway = deviceAdditionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& deviceAdditionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean();
TransportProtos.SessionInfoProto newSessionInfo = TransportProtos.SessionInfoProto.newBuilder()
.mergeFrom(md.getSessionInfo())
.setDeviceProfileIdMSB(deviceProfileIdMSB)
.setDeviceProfileIdLSB(deviceProfileIdLSB)
.setDeviceName(device.getName())
.setDeviceType(device.getType()).build();
JsonNode deviceAdditionalInfo = device.getAdditionalInfo();
if (deviceAdditionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& deviceAdditionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean()
&& deviceAdditionalInfo.has(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER)
.setDeviceType(device.getType())
.setIsGateway(isGateway).build();
if (isGateway && deviceAdditionalInfo.has(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER)
&& deviceAdditionalInfo.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).isBoolean()) {
md.setOverwriteActivityTime(deviceAdditionalInfo.get(DataConstants.OVERWRITE_ACTIVITY_TIME_PARAMETER).asBoolean());
}

12
dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.mobile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.id.TenantId;
@ -40,6 +41,11 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService<Te
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app";
@Value("${mobileApp.googlePlayLink:https://play.google.com/store/apps/details?id=org.thingsboard.demo.app}")
private String googlePlayLink;
@Value("${mobileApp.appStoreLink:https://apps.apple.com/us/app/thingsboard-live/id1594355695}")
private String appStoreLink;
private final MobileAppSettingsDao mobileAppSettingsDao;
private final DataValidator<MobileAppSettings> mobileAppSettingsDataValidator;
@ -49,7 +55,7 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService<Te
try {
MobileAppSettings savedMobileAppSettings = mobileAppSettingsDao.save(tenantId, mobileAppSettings);
publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
return savedMobileAppSettings;
return constructMobileAppSettings(savedMobileAppSettings);
} catch (Exception e) {
handleEvictEvent(new MobileAppSettingsEvictEvent(tenantId));
checkConstraintViolation(e, Map.of(
@ -104,6 +110,10 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService<Te
mobileAppSettings.setAndroidConfig(androidConfig);
mobileAppSettings.setIosConfig(iosConfig);
}
if (mobileAppSettings.isUseDefaultApp()) {
mobileAppSettings.setDefaultGooglePlayLink(googlePlayLink);
mobileAppSettings.setDefaultAppStoreLink(appStoreLink);
}
return mobileAppSettings;
}

18
dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java

@ -23,14 +23,18 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.QueueStatsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.Validator;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@Service("QueueStatsDaoService")
@Slf4j
@ -57,6 +61,14 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu
return queueStatsDao.findById(tenantId, queueStatsId.getId());
}
@Override
public List<QueueStats> findQueueStatsByIds(TenantId tenantId, List<QueueStatsId> queueStatsIds) {
log.trace("Executing findQueueStatsByIds, tenantId [{}], queueStatsIds [{}]", tenantId, queueStatsIds);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateIds(queueStatsIds, ids -> "Incorrect queueStatsIds " + ids);
return queueStatsDao.findByIds(tenantId, queueStatsIds);
}
@Override
public QueueStats findByTenantIdAndNameAndServiceId(TenantId tenantId, String queueName, String serviceId) {
log.trace("Executing findByTenantIdAndNameAndServiceId, tenantId: [{}], queueName: [{}], serviceId: [{}]", tenantId, queueName, serviceId);
@ -65,10 +77,10 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu
}
@Override
public List<QueueStats> findByTenantId(TenantId tenantId) {
public PageData<QueueStats> findByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findByTenantId, tenantId: [{}]", tenantId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return queueStatsDao.findByTenantId(tenantId);
Validator.validatePageLink(pageLink);
return queueStatsDao.findByTenantId(tenantId, pageLink);
}
@Override

7
dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java

@ -15,7 +15,10 @@
*/
package org.thingsboard.server.dao.queue;
import org.thingsboard.server.common.data.id.QueueStatsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.dao.Dao;
@ -25,8 +28,10 @@ public interface QueueStatsDao extends Dao<QueueStats> {
QueueStats findByTenantIdQueueNameAndServiceId(TenantId tenantId, String queueName, String serviceId);
List<QueueStats> findByTenantId(TenantId tenantId);
PageData<QueueStats> findByTenantId(TenantId tenantId, PageLink pageLink);
void deleteByTenantId(TenantId tenantId);
List<QueueStats> findByIds(TenantId tenantId, List<QueueStatsId> queueStatsIds);
}

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

@ -583,6 +583,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
return "e.tenant_id=:permissions_tenant_id and e.id=:permissions_customer_id";
} else if (ctx.getEntityType() == EntityType.API_USAGE_STATE) {
return "e.tenant_id=:permissions_tenant_id and e.entity_id=:permissions_customer_id";
} else if (ctx.getEntityType() == EntityType.DASHBOARD) {
return "e.tenant_id=:permissions_tenant_id and e.assigned_customers like concat('%', :permissions_customer_id, '%')";
} else {
return "e.tenant_id=:permissions_tenant_id and e.customer_id=:permissions_customer_id";
}

14
dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java

@ -20,7 +20,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.QueueStatsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.QueueStatsEntity;
@ -31,6 +34,8 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
@Slf4j
@Component
@SqlDao
@ -55,8 +60,8 @@ public class JpaQueueStatsDao extends JpaAbstractDao<QueueStatsEntity, QueueStat
}
@Override
public List<QueueStats> findByTenantId(TenantId tenantId) {
return DaoUtil.convertDataList(queueStatsRepository.findByTenantId(tenantId.getId()));
public PageData<QueueStats> findByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(queueStatsRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@Override
@ -64,6 +69,11 @@ public class JpaQueueStatsDao extends JpaAbstractDao<QueueStatsEntity, QueueStat
queueStatsRepository.deleteByTenantId(tenantId.getId());
}
@Override
public List<QueueStats> findByIds(TenantId tenantId, List<QueueStatsId> queueStatsIds) {
return DaoUtil.convertDataList(queueStatsRepository.findByTenantIdAndIdIn(tenantId.getId(), toUUIDs(queueStatsIds)));
}
@Override
public EntityType getEntityType() {
return EntityType.QUEUE_STATS;

11
dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.dao.sql.queue;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@ -29,11 +31,18 @@ public interface QueueStatsRepository extends JpaRepository<QueueStatsEntity, UU
QueueStatsEntity findByTenantIdAndQueueNameAndServiceId(UUID tenantId, String queueName, String serviceId);
List<QueueStatsEntity> findByTenantId(UUID tenantId);
@Query("SELECT q FROM QueueStatsEntity q WHERE q.tenantId = :tenantId " +
"AND (:textSearch IS NULL OR ilike(q.queueName, CONCAT('%', :textSearch, '%')) = true " +
"OR ilike(q.serviceId, CONCAT('%', :textSearch, '%')) = true)")
Page<QueueStatsEntity> findByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Transactional
@Modifying
@Query("DELETE FROM QueueStatsEntity t WHERE t.tenantId = :tenantId")
void deleteByTenantId(@Param("tenantId") UUID tenantId);
List<QueueStatsEntity> findByTenantIdAndIdIn(UUID tenantId, List<UUID> queueStatsIds);
}

8
dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java

@ -24,6 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.queue.QueueStatsService;
@ -80,9 +82,9 @@ public class QueueStatsServiceTest extends AbstractServiceTest {
QueueStats retrievedQueueStatsById2 = queueStatsService.findQueueStatsById(tenantId, savedQueueStats2.getId());
Assert.assertEquals(retrievedQueueStatsById2.getQueueName(), secondQueueName);
List<QueueStats> queueStatsList = queueStatsService.findByTenantId(tenantId);
Assert.assertEquals(2, queueStatsList.size());
assertThat(queueStatsList).containsOnly(retrievedQueueStatsById, retrievedQueueStatsById2);
PageData<QueueStats> queueStatsList = queueStatsService.findByTenantId(tenantId, new PageLink(10));
Assert.assertEquals(2, queueStatsList.getData().size());
assertThat(queueStatsList.getData()).containsOnly(retrievedQueueStatsById, retrievedQueueStatsById2);
queueStatsService.deleteByTenantId(tenantId);
QueueStats retrievedQueueStatsAfterDelete = queueStatsService.findQueueStatsById(tenantId, savedQueueStats.getId());

2
packaging/java/scripts/windows/install.bat

@ -23,7 +23,7 @@ if "%1" == "--loadDemo" (
SET BASE=%~dp0
SET LOADER_PATH=%BASE%\conf,%BASE%\extensions
SET SQL_DATA_FOLDER=%BASE%\data\sql
SET jarfile=%BASE%\lib\thingsboard.jar
SET jarfile=%BASE%\lib\${pkg.name}.jar
SET installDir=%BASE%\data
PUSHD "%BASE%\conf"

2
packaging/java/scripts/windows/install_dev_db.bat

@ -5,7 +5,7 @@ setlocal ENABLEEXTENSIONS
SET BASE=${project.basedir}\target
SET LOADER_PATH=%BASE%\conf,%BASE%\extensions
SET jarfile=%BASE%\thingsboard-${project.version}-boot.jar
SET jarfile=%BASE%\${pkg.name}-${project.version}-boot.jar
SET installDir=%BASE%\data
SET loadDemo=true

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

@ -169,6 +169,8 @@ export class EntityService {
case EntityType.QUEUE:
observable = this.queueService.getQueueById(entityId, config);
break;
case EntityType.QUEUE_STATS:
observable = this.queueService.getQueueStatisticsById(entityId, config);
}
return observable;
}
@ -267,6 +269,9 @@ export class EntityService {
case EntityType.NOTIFICATION_TARGET:
observable = this.notificationService.getNotificationTargetsByIds(entityIds, config);
break;
case EntityType.QUEUE_STATS:
observable = this.queueService.getQueueStatisticsByIds(entityIds, config);
break;
}
return observable;
}
@ -444,6 +449,9 @@ export class EntityService {
pageLink.sortOrder.property = 'title';
entitiesObservable = this.resourceService.getTenantResources(pageLink, config);
break;
case EntityType.QUEUE_STATS:
pageLink.sortOrder.property = 'createdTime';
entitiesObservable = this.queueService.getQueueStatistics(pageLink, config);
}
return entitiesObservable;
}
@ -716,11 +724,12 @@ export class EntityService {
entityTypes.push(EntityType.CUSTOMER);
entityTypes.push(EntityType.USER);
entityTypes.push(EntityType.DASHBOARD);
entityTypes.push(EntityType.QUEUE_STATS);
if (authState.edgesSupportEnabled) {
entityTypes.push(EntityType.EDGE);
}
if (useAliasEntityTypes) {
entityTypes.push(EntityType.QUEUE_STATS);
entityTypes.push(AliasEntityType.CURRENT_CUSTOMER);
entityTypes.push(AliasEntityType.CURRENT_TENANT);
}

30
ui-ngx/src/app/core/http/queue.service.ts

@ -18,9 +18,10 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { QueueInfo, ServiceType } from '@shared/models/queue.models';
import { QueueInfo, QueueStatisticsInfo, ServiceType } from '@shared/models/queue.models';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@ -53,4 +54,31 @@ export class QueueService {
public deleteQueue(queueId: string) {
return this.http.delete(`/api/queues/${queueId}`);
}
private parseQueueStatName = (queueStat: QueueStatisticsInfo) => Object.defineProperty(queueStat, 'name', {
get() { return `${this.queueName} (${this.serviceId})`; }
});
public getQueueStatistics(pageLink: PageLink, config?: RequestConfig): Observable<PageData<QueueStatisticsInfo>> {
return this.http.get<PageData<QueueStatisticsInfo>>(`/api/queueStats${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config)).pipe(
map(queueData => {
queueData.data.map(queueStat => this.parseQueueStatName(queueStat));
return queueData;
})
);
}
public getQueueStatisticsById(queueStatId: string, config?: RequestConfig): Observable<QueueStatisticsInfo> {
return this.http.get<QueueStatisticsInfo>(`/api/queueStats/${queueStatId}`, defaultHttpOptionsFromConfig(config)).pipe(
map(queueStat => this.parseQueueStatName(queueStat)));
}
public getQueueStatisticsByIds(queueStatIds: Array<string>, config?: RequestConfig): Observable<Array<QueueStatisticsInfo>> {
return this.http.get<Array<QueueStatisticsInfo>>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`,
defaultHttpOptionsFromConfig(config)).pipe(
map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat))
)
);
}
}

9
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -37,7 +37,8 @@
<ng-template [ngSwitchCase]="aliasFilterType.entityList">
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="allowedEntityTypes"
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<tb-entity-list required
@ -49,7 +50,8 @@
<ng-template [ngSwitchCase]="aliasFilterType.entityName">
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="allowedEntityTypes"
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<mat-form-field class="mat-block">
@ -65,7 +67,8 @@
<ng-template [ngSwitchCase]="aliasFilterType.entityType">
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="allowedEntityTypes"
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
</ng-template>

7
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts

@ -50,6 +50,8 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit, OnDe
aliasFilterTypes: Array<AliasFilterType>;
listEntityTypes: Array<EntityType | AliasEntityType>;
aliasFilterType = AliasFilterType;
aliasFilterTypeTranslations = aliasFilterTypeTranslationMap;
entityType = EntityType;
@ -71,6 +73,11 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit, OnDe
this.aliasFilterTypes = this.entityService.getAliasFilterTypesByEntityTypes(this.allowedEntityTypes);
this.listEntityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, false);
if (!this.allowedEntityTypes?.length || this.allowedEntityTypes.includes(EntityType.QUEUE_STATS)) {
this.listEntityTypes.push(EntityType.QUEUE_STATS);
}
this.entityFilterFormGroup = this.fb.group({
type: [null, [Validators.required]]
});

12
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -520,13 +520,17 @@
<tb-rate-limits fxFlex formControlName="transportGatewayMsgRateLimit"
[type]="rateLimitsType.GATEWAY_MESSAGES">
</tb-rate-limits>
<div fxFlex></div>
<tb-rate-limits fxFlex formControlName="transportGatewayDeviceMsgRateLimit"
[type]="rateLimitsType.GATEWAY_DEVICE_MESSAGES">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex formControlName="transportGatewayTelemetryMsgRateLimit"
[type]="rateLimitsType.GATEWAY_TELEMETRY_MESSAGES">
</tb-rate-limits>
<div fxFlex></div>
<tb-rate-limits fxFlex formControlName="transportGatewayDeviceTelemetryMsgRateLimit"
[type]="rateLimitsType.GATEWAY_DEVICE_TELEMETRY_MESSAGES">
</tb-rate-limits>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
@ -547,7 +551,9 @@
<tb-rate-limits fxFlex formControlName="transportGatewayTelemetryDataPointsRateLimit"
[type]="rateLimitsType.GATEWAY_TELEMETRY_DATA_POINTS">
</tb-rate-limits>
<div fxFlex></div>
<tb-rate-limits fxFlex formControlName="transportGatewayDeviceTelemetryDataPointsRateLimit"
[type]="rateLimitsType.GATEWAY_DEVICE_TELEMETRY_DATA_POINTS">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex formControlName="tenantServerRestLimitsConfiguration"

3
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -77,6 +77,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
transportGatewayMsgRateLimit: [null, []],
transportGatewayTelemetryMsgRateLimit: [null, []],
transportGatewayTelemetryDataPointsRateLimit: [null, []],
transportGatewayDeviceMsgRateLimit: [null, []],
transportGatewayDeviceTelemetryMsgRateLimit: [null, []],
transportGatewayDeviceTelemetryDataPointsRateLimit: [null, []],
tenantEntityExportRateLimit: [null, []],
tenantEntityImportRateLimit: [null, []],
tenantNotificationRequestsRateLimit: [null, []],

9
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts

@ -29,6 +29,9 @@ export enum RateLimitsType {
GATEWAY_MESSAGES = 'GATEWAY_MESSAGES',
GATEWAY_TELEMETRY_MESSAGES = 'GATEWAY_TELEMETRY_MESSAGES',
GATEWAY_TELEMETRY_DATA_POINTS = 'GATEWAY_TELEMETRY_DATA_POINTS',
GATEWAY_DEVICE_MESSAGES = 'GATEWAY_DEVICE_MESSAGES',
GATEWAY_DEVICE_TELEMETRY_MESSAGES = 'GATEWAY_DEVICE_TELEMETRY_MESSAGES',
GATEWAY_DEVICE_TELEMETRY_DATA_POINTS = 'GATEWAY_DEVICE_TELEMETRY_DATA_POINTS',
TENANT_TELEMETRY_MESSAGES = 'TENANT_TELEMETRY_MESSAGES',
TENANT_TELEMETRY_DATA_POINTS = 'TENANT_TELEMETRY_DATA_POINTS',
TENANT_SERVER_REST_LIMITS_CONFIGURATION = 'TENANT_SERVER_REST_LIMITS_CONFIGURATION',
@ -56,6 +59,9 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.GATEWAY_MESSAGES, 'tenant-profile.rate-limits.transport-gateway-msg'],
[RateLimitsType.GATEWAY_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-gateway-telemetry-msg'],
[RateLimitsType.GATEWAY_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-gateway-telemetry-data-points'],
[RateLimitsType.GATEWAY_DEVICE_MESSAGES, 'tenant-profile.rate-limits.transport-gateway-device-msg'],
[RateLimitsType.GATEWAY_DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-gateway-device-telemetry-msg'],
[RateLimitsType.GATEWAY_DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-gateway-device-telemetry-data-points'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rest-requests-for-tenant'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.customer-rest-limits'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.ws-limit-updates-per-session'],
@ -83,6 +89,9 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.GATEWAY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-gateway-msg-title'],
[RateLimitsType.GATEWAY_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-gateway-telemetry-msg-title'],
[RateLimitsType.GATEWAY_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-gateway-telemetry-data-points-title'],
[RateLimitsType.GATEWAY_DEVICE_MESSAGES, 'tenant-profile.rate-limits.edit-transport-gateway-device-msg-title'],
[RateLimitsType.GATEWAY_DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-gateway-device-telemetry-msg-title'],
[RateLimitsType.GATEWAY_DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-gateway-device-telemetry-data-points-title'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-customer-rest-limits-title'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.rate-limits.edit-ws-limit-updates-per-session-title'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-cassandra-tenant-limits-configuration-title'],

17
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.html

@ -45,16 +45,7 @@
{{ 'admin.mobile-app.label' | translate }}
</mat-slide-toggle>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="qrCodeLabel" maxlength="50" placeholder="{{'admin.mobile-app.set' | translate}}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'admin.mobile-app.label-required' | translate"
*ngIf="mobileAppQrCodeWidgetConfigForm.get('qrCodeLabel').hasError('required')
&& mobileAppQrCodeWidgetConfigForm.get('qrCodeLabel').touched"
class="tb-error">
warning
</mat-icon>
<input matInput formControlName="qrCodeLabel" placeholder="{{'admin.mobile-app.set' | translate}}">
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
@ -110,6 +101,12 @@
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts

@ -82,6 +82,7 @@ export class MobileAppQrCodeBasicConfigComponent extends BasicWidgetConfigCompon
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
@ -110,6 +111,7 @@ export class MobileAppQrCodeBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;

4
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html

@ -100,6 +100,8 @@
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor"
[minValue]="progressBarWidgetConfigForm.get('tickMin').value"
[maxValue]="progressBarWidgetConfigForm.get('tickMax').value"
settingsKey="{{'widgets.progress-bar.value' | translate }}">
</tb-color-settings>
</div>
@ -137,6 +139,8 @@
<div class="tb-form-row space-between">
<div>{{ 'widgets.progress-bar.bar-color' | translate }}</div>
<tb-color-settings formControlName="barColor"
[minValue]="progressBarWidgetConfigForm.get('tickMin').value"
[maxValue]="progressBarWidgetConfigForm.get('tickMax').value"
settingsKey="{{'widgets.progress-bar.bar-color' | translate }}">
</tb-color-settings>
</div>

32
ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html

@ -53,17 +53,9 @@
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showMinMax">
{{ 'widgets.gauge.min-and-max-value' | translate }}
{{ 'widgets.gauge.min-and-max-label' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.gauge.min-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="minMaxFont"
disabledLineHeight
[previewText]="previewFn">
@ -74,6 +66,19 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width">{{ 'widgets.gauge.min-and-max-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.gauge.min-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
@ -117,14 +122,6 @@
</div>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.gauge.default-color</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="defaultColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.gauge.gauge-bar-background</div>
<tb-color-input asBoxInput
@ -137,6 +134,7 @@
<div class="fixed-title-width">{{ 'widgets.gauge.bar-color' | translate }}</div>
<tb-color-settings formControlName="barColor"
rangeAdvancedMode
gradientAdvancedMode
[minValue]="simpleGaugeWidgetConfigForm.get('minValue').value"
[maxValue]="simpleGaugeWidgetConfigForm.get('maxValue').value"
[aliasController]="aliasController"

4
ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts

@ -98,7 +98,7 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: DigitalGaugeSettings = {...defaultDigitalSimpleGaugeOptions, ...(configData.config.settings || {})};
convertLevelColorsSettingsToColorProcessor(settings);
convertLevelColorsSettingsToColorProcessor(settings, settings.defaultColor || '#2196f3');
this.simpleGaugeWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
@ -124,7 +124,6 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
titleFont: [settings.titleFont, []],
titleColor: [settings.titleFont?.color, []],
defaultColor: [settings.defaultColor, []],
gaugeColor: [settings.gaugeColor, []],
barColor: [settings.barColor, []],
@ -165,7 +164,6 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
this.widgetConfig.config.settings.titleFont = config.titleFont;
this.widgetConfig.config.settings.titleFont.color = config.titleColor;
this.widgetConfig.config.settings.defaultColor = config.defaultColor;
this.widgetConfig.config.settings.gaugeColor = config.gaugeColor;
this.widgetConfig.config.settings.barColor = config.barColor;
const barColor: ColorSettings = config.barColor;

3
ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts

@ -50,5 +50,6 @@ export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
},
padding: '12px'
}

14
ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts

@ -131,7 +131,12 @@ export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewI
this.showValue = this.settings.showValue;
this.valueStyle = textStyle(this.settings.valueFont);
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.valueColor = ColorProcessor.fromColorProcessorSettings({
settings: this.settings.valueColor,
ctx: this.ctx,
minGradientValue: this.settings.tickMin,
maxGradientValue: this.settings.tickMax
});
this.showTitleValueRow = this.showValue ||
(this.layout === ProgressBarLayout.simplified && this.widgetComponent.dashboardWidget.showWidgetTitlePanel);
@ -142,7 +147,12 @@ export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewI
this.titleValueRowClass = (this.layout === ProgressBarLayout.simplified &&
!this.widgetComponent.dashboardWidget.showWidgetTitlePanel) ? 'flex-end' : '';
this.barColor = ColorProcessor.fromSettings(this.settings.barColor);
this.barColor = ColorProcessor.fromColorProcessorSettings({
settings: this.settings.barColor,
ctx: this.ctx,
minGradientValue: this.settings.tickMin,
maxGradientValue: this.settings.tickMax
});;
this.showTicks = this.settings.showTicks && this.layout === ProgressBarLayout.default;
if (this.showTicks) {

21
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts

@ -22,6 +22,7 @@ import {
ColorSettings,
ColorType,
constantColor,
defaultGradient,
ValueSourceConfig,
ValueSourceType
} from '@shared/models/widget-settings.models';
@ -161,9 +162,19 @@ export const backwardCompatibilityTicks = (ticksValue: AttributeSourceProperty[]
return ticks;
};
export const convertLevelColorsSettingsToColorProcessor = (settings: DigitalGaugeSettings) => {
if (!settings.barColor) {
settings.barColor = constantColor(settings.gaugeColor);
export const convertLevelColorsSettingsToColorProcessor = (settings: DigitalGaugeSettings, defaultColor?: string) => {
if (settings.barColor) {
if (!settings.barColor.color) {
settings.barColor.color = defaultColor;
}
if (isDefinedAndNotNull(settings.barColor.gradient)) {
settings.barColor.gradient.minValue = settings.minValue;
settings.barColor.gradient.maxValue = settings.maxValue;
} else {
settings.barColor.gradient = defaultGradient(settings.minValue, settings.maxValue);
}
} else {
settings.barColor = constantColor(defaultColor);
if (settings.fixedLevelColors?.length) {
settings.barColor.rangeList = {
advancedMode: settings.useFixedLevelColor,
@ -175,7 +186,9 @@ export const convertLevelColorsSettingsToColorProcessor = (settings: DigitalGaug
settings.barColor.gradient = {
advancedMode: false,
gradient: settings.levelColors as string[],
gradientAdvanced: null
gradientAdvanced: null,
minValue: settings.minValue,
maxValue: settings.maxValue
};
}
if (settings.useFixedLevelColor) {

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

@ -60,7 +60,7 @@ export class TbCanvasDigitalGauge {
this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75;
this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString();
convertLevelColorsSettingsToColorProcessor(settings);
convertLevelColorsSettingsToColorProcessor(settings, keyColor);
this.localSettings.barColor = settings.barColor;
this.localSettings.showTicks = settings.showTicks || false;

18
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html

@ -16,6 +16,7 @@
-->
<div class="tb-mobile-app-qrcode-panel"
[style.padding]="padding"
[class.preview]="previewMode"
[style]="backgroundStyle$ | async">
<div class="tb-mobile-app-qrcode-overlay" [style]="overlayStyle"></div>
@ -24,11 +25,18 @@
<div class="tb-flex row center align-center shrink"
[class.row]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.RIGHT"
[class.row-reverse]="mobileAppSettings?.qrCodeConfig.badgePosition === badgePosition.LEFT">
<div class="tb-qrcode" (click)="navigateByDeepLink($event)" [innerHTML]="qrCodeSVG | safe: 'html'" ></div>
<div class="tb-badges" *ngIf="(mobileAppSettings?.iosConfig.enabled || mobileAppSettings?.androidConfig.enabled) &&
mobileAppSettings?.qrCodeConfig.badgeEnabled && showBadgeContainer">
<img *ngIf="mobileAppSettings?.iosConfig.enabled" src="assets/android-ios-stores-badges/ios_store_en_black_badge.svg">
<img *ngIf="mobileAppSettings?.androidConfig.enabled" src="assets/android-ios-stores-badges/android_store_en_black_badge.svg">
<div class="tb-qrcode" (click)="navigateByDeepLink($event)" [innerHTML]="qrCodeSVG | safe: 'html'"></div>
<div class="tb-badges" [class.tb-no-interaction]="previewMode"
*ngIf="(mobileAppSettings?.iosConfig.enabled || mobileAppSettings?.androidConfig.enabled) &&
mobileAppSettings?.qrCodeConfig.badgeEnabled && showBadgeContainer">
<a *ngIf="mobileAppSettings?.iosConfig.enabled" [href]="appStoreLink" target="_blank">
<img alt="{{ 'widgets.mobile-app-qr-code.download-on-the-app-store' | translate }}"
src="assets/android-ios-stores-badges/ios_store_en_black_badge.svg">
</a>
<a *ngIf="mobileAppSettings?.androidConfig.enabled" [href]="googlePlayLink" target="_blank">
<img alt="{{ 'widgets.mobile-app-qr-code.get-it-on-google-play' | translate }}"
src="assets/android-ios-stores-badges/android_store_en_black_badge.svg">
</a>
</div>
</div>
<div class="tb-qrcode-label" *ngIf="mobileAppSettings?.qrCodeConfig.qrCodeLabelEnabled">

30
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss

@ -47,10 +47,6 @@
right: 12px;
}
div.tb-widget-title {
padding: 0;
}
.tb-qrcode-label {
align-self: center;
text-align: center;
@ -60,7 +56,7 @@
.tb-qrcode {
display: flex;
flex: 0.3;
flex: 0.33;
min-width: 100px;
padding: 3px;
border-radius: 6px;
@ -70,8 +66,30 @@
.tb-badges {
display: flex;
flex-direction: column;
flex: 0.4;
flex: 0.44;
gap: 8px;
&.tb-no-interaction {
pointer-events: none;
}
a {
display: contents;
border-bottom: none;
&:hover {
border-bottom: none;
}
}
}
}
:host ::ng-deep {
.tb-mobile-app-qrcode-panel {
.tb-widget-title {
padding: 0;
z-index: 1;
}
.tb-qrcode svg {
width: 100%;
height: 100%;
}
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts

@ -45,6 +45,9 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
private deepLinkTTL: number;
private deepLinkTTLTimeoutID: NodeJS.Timeout;
googlePlayLink: string;
appStoreLink: string;
previewMode = false;
badgePosition = BadgePosition;
@ -52,6 +55,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
qrCodeSVG = '';
@ -86,6 +90,13 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
if (!this.mobileAppSettings) {
this.mobileAppService.getMobileAppSettings().subscribe((settings => {
this.mobileAppSettings = settings;
const useDefaultApp = this.mobileAppSettings.useDefaultApp;
this.appStoreLink = useDefaultApp ? this.mobileAppSettings.defaultAppStoreLink :
this.mobileAppSettings.iosConfig.storeLink;
this.googlePlayLink = useDefaultApp ? this.mobileAppSettings.defaultGooglePlayLink :
this.mobileAppSettings.androidConfig.storeLink;
if (isDefinedAndNotNull(this.ctx.settings.useSystemSettings) && !this.ctx.settings.useSystemSettings) {
this.mobileAppSettings = mergeDeep(this.mobileAppSettings, this.ctx.settings);
}
@ -101,6 +112,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
this.widgetResize$.observe(this.elementRef.nativeElement);
this.backgroundStyle$ = backgroundStyle(this.ctx.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.ctx.settings.background.overlay);
this.padding = this.ctx.settings.background.overlay.enabled ? undefined : this.ctx.settings.padding;
this.cd.markForCheck();
}));
} else {

17
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html

@ -48,16 +48,7 @@
{{ 'admin.mobile-app.label' | translate }}
</mat-slide-toggle>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="qrCodeLabel" maxlength="50" placeholder="{{'admin.mobile-app.set' | translate}}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'admin.mobile-app.label-required' | translate"
*ngIf="mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').hasError('required')
&& mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').touched"
class="tb-error">
warning
</mat-icon>
<input matInput formControlName="qrCodeLabel" placeholder="{{'admin.mobile-app.set' | translate}}">
</mat-form-field>
</div>
</div>
@ -69,5 +60,11 @@
</tb-background-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</section>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts

@ -55,7 +55,8 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon
qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled],
qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel]
}),
background: [settings.background]
background: [settings.background],
padding: [settings.padding, []]
});
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html

@ -44,6 +44,8 @@
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor"
[minValue]="progressBarWidgetSettingsForm.get('tickMin').value"
[maxValue]="progressBarWidgetSettingsForm.get('tickMax').value"
settingsKey="{{'widgets.progress-bar.value' | translate }}">
</tb-color-settings>
</div>
@ -78,6 +80,8 @@
<div class="tb-form-row space-between">
<div>{{ 'widgets.progress-bar.bar-color' | translate }}</div>
<tb-color-settings formControlName="barColor"
[minValue]="progressBarWidgetSettingsForm.get('tickMin').value"
[maxValue]="progressBarWidgetSettingsForm.get('tickMax').value"
settingsKey="{{'widgets.progress-bar.bar-color' | translate }}">
</tb-color-settings>
</div>

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

@ -38,20 +38,22 @@
<ng-template [ngSwitchCase]="false">
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="rangeDrop($event, 'range')">
<div cdkDrag class="tb-form-row no-padding no-border tb-draggable"
<div cdkDrag class="tb-draggable tb-advanced-range-drag"
[formGroup]="rangeFormGroup"
*ngFor="let rangeFormGroup of rangeListFormGroups; trackBy: trackByRange; let $index = index; last as isLast;"
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<div class="range">
<div class="range-container">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="tb-form-row range-container">
<div class="tb-value-range-text" translate>widgets.color.from</div>
<mat-form-field fxFlex appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput type="number" formControlName="from" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-value-range-text tb-value-range-text-to" translate>widgets.color.to</div>
<mat-form-field fxFlex appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput type="number" formControlName="to" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
@ -64,15 +66,15 @@
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button class="tb-box-button"
mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
<button class="tb-box-button"
mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</ng-template>
@ -103,9 +105,10 @@
</ng-template>
</div>
<button class="tb-add-color-range"
mat-stroked-button
(click)="addRange()">
<mat-icon>add</mat-icon>
</button>
<div class="tb-add-color-range">
<button mat-stroked-button
(click)="addRange()">
<mat-icon>add</mat-icon>
</button>
</div>
</ng-container>

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

@ -13,24 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
:host {
.range {
display: flex;
flex: 1;
align-items: center;
flex-direction: row;
&-container {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
gap: 12px;
padding: 8px;
margin-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
}
.range-container {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
gap: 12px;
padding: 8px;
margin-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
}
.tb-add-color-range {
button {
width: 100%;
}
}
.tb-value-range-text {
width: 64px;
font-size: 14px;
color: rgba(0, 0, 0, 0.38);
@media #{$mat-xs} {
width: auto;
}
&.tb-value-range-text-to {
text-align: center;
}
}

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -62,7 +62,8 @@ export function advancedRangeValidator(control: AbstractControl): ValidationErro
useExisting: forwardRef(() => ColorRangeListComponent),
multi: true
}
]
],
encapsulation: ViewEncapsulation.None
})
export class ColorRangeListComponent implements OnInit, ControlValueAccessor, OnDestroy {
@ -108,6 +109,9 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
this.colorRangeListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
this.colorRangeListFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => Promise.resolve().then(() => this.popover?.updatePosition()));
}
ngOnDestroy() {
@ -171,6 +175,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
public removeAdvancedRange(index: number) {
(this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index);
Promise.resolve().then(() => this.popover?.updatePosition());
}
get advancedRangeFormArray(): UntypedFormArray {
@ -184,7 +189,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
removeRange(index: number) {
this.rangeListFormArray.removeAt(index);
this.colorRangeListFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
Promise.resolve().then(() => this.popover?.updatePosition());
}
rangeDrop(event: CdkDragDrop<string[]>, range: string) {
@ -207,6 +212,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
const advancedRangeColorsArray = this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray;
const advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]);
advancedRangeColorsArray.push(advancedRangeColorControl);
Promise.resolve().then(() => this.popover?.updatePosition());
}
addRange() {
@ -218,7 +224,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
};
this.rangeListFormArray.push(this.colorRangeControl(newRange));
this.colorRangeListFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
Promise.resolve().then(() => this.popover?.updatePosition());
}
}

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

@ -55,17 +55,6 @@
}
.tb-form-row {
height: auto;
.tb-value-range-text {
width: 64px;
font-size: 14px;
color: rgba(0, 0, 0, 0.38);
@media #{$mat-xs} {
width: auto;
}
&.tb-value-range-text-to {
text-align: center;
}
}
}
button.mat-mdc-button-base.tb-add-color-range {
&:not(:disabled) {

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

@ -20,7 +20,8 @@ import {
ColorSettings,
ColorType,
colorTypeTranslations,
defaultGradient
defaultGradient,
defaultRange
} from '@shared/models/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@ -102,12 +103,12 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
type: [this.colorSettings?.type || ColorType.constant, []],
color: [this.colorSettings?.color, []],
gradient: [this.colorSettings?.gradient || defaultGradient(this.minValue, this.maxValue), []],
rangeList: [this.colorSettings?.rangeList, []],
rangeList: [this.colorSettings?.rangeList || defaultRange(), []],
colorFunction: [this.colorSettings?.colorFunction, []]
}
);
this.colorSettingsFormGroup.get('type').valueChanges.subscribe(() => {
setTimeout(() => {this.popover?.updatePosition();}, 0);
Promise.resolve().then(() => this.popover?.updatePosition());
});
}

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

@ -103,11 +103,11 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD
@Input()
@coerceNumber()
minValue: number;
minValue = 0;
@Input()
@coerceNumber()
maxValue: number;
maxValue = 100;
colorType = ColorType;

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

@ -16,11 +16,36 @@
@import "../../../../../../../../scss/constants";
:host {
overflow: auto;
height: 100%;
max-height: 420px;
.gradient-preview {
width: 100%;
padding: 40px 12px 0;
.gradient-background {
position: relative;
height: 56px;
border-radius: 8px;
}
}
.gradient-settings {
flex: 1;
gap: 16px;
display: flex;
flex-direction: column;
}
.tb-add-gradient {
margin-right: 92px;
@media #{$mat-lt-lg} {
margin-right: 52px;
}
}
}
::ng-deep {
.gradient {
display: flex;
flex: 1;
@ -67,32 +92,6 @@
}
}
}
.gradient-preview {
width: 100%;
padding: 40px 12px 0;
.gradient-background {
position: relative;
height: 56px;
border-radius: 8px;
}
}
.gradient-settings {
flex: 1;
gap: 16px;
display: flex;
flex-direction: column;
}
.tb-add-gradient {
margin-right: 92px;
@media #{$mat-lt-lg} {
margin-right: 52px;
}
}
}
::ng-deep {
.gradient-background {
.pointer {
position: absolute;

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

@ -31,7 +31,7 @@ import { takeUntil } from 'rxjs/operators';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { IAliasController } from '@core/api/widget-api.models';
import { DomSanitizer } from '@angular/platform-browser';
import { coerceBoolean } from '@shared/decorators/coercion';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import { isDefinedAndNotNull } from '@core/utils';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@ -71,10 +71,12 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
datasource: Datasource;
@Input()
minValue: string;
@coerceNumber()
minValue: number;
@Input()
maxValue: string;
@coerceNumber()
maxValue: number;
@Input()
@coerceBoolean()
@ -115,6 +117,9 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
this.gradientFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => Promise.resolve().then(() => this.popover?.updatePosition()));
}
ngOnDestroy() {
@ -228,7 +233,7 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.removeAt(index);
}
this.gradientFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
Promise.resolve().then(() => this.popover?.updatePosition());
}
gradientDrop(event: CdkDragDrop<string[]>, advanced = false) {
@ -247,7 +252,7 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)'));
}
this.gradientFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
Promise.resolve().then(() => this.popover?.updatePosition());
}
updateModel() {

47
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html

@ -41,17 +41,9 @@
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showMinMax">
{{ 'widgets.gauge.min-and-max-value' | translate }}
{{ 'widgets.gauge.min-and-max-label' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.gauge.min-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="minMaxFont"
disabledLineHeight
[previewText]="previewFn">
@ -63,6 +55,20 @@
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width">{{ 'widgets.gauge.min-and-max-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.gauge.min-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
{{ 'widgets.gauge.value' | translate }}
@ -160,14 +166,6 @@
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width">{{ 'widgets.gauge.default-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="defaultColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width">{{ 'widgets.gauge.gauge-bar-background' | translate }}</div>
<tb-color-input asBoxInput
@ -180,6 +178,7 @@
<div class="fixed-title-width">{{ 'widgets.gauge.bar-color' | translate }}</div>
<tb-color-settings formControlName="barColor"
rangeAdvancedMode
gradientAdvancedMode
[minValue]="digitalGaugeWidgetSettingsForm.get('minValue').value"
[maxValue]="digitalGaugeWidgetSettingsForm.get('maxValue').value"
[aliasController]="aliasController"
@ -222,13 +221,23 @@
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="tickValueDrop($event)">
<div cdkDrag class="tb-draggable" *ngFor="let tickValueControl of tickValuesFormArray().controls; trackBy: trackByTickValue;
let $index = index; last as isLast;" fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-tick-value [formControl]="tickValueControl"
let $index = index; last as isLast;" fxLayout="row" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-tick-value fxFlex
[formControl]="tickValueControl"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
(removeTickValue)="removeTickValue($index)">
</tb-tick-value>
<button class="tb-box-button"
fxHide.lt-lg
mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
<div *ngIf="!tickValuesFormArray().controls.length">

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts

@ -138,7 +138,7 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
protected onSettingsSet(settings: WidgetSettings) {
if (!settings.barColor) {
settings.barColor = constantColor(settings.gaugeColor);
settings.barColor = constantColor(settings.defaultColor || '#2196f3');
if (settings.fixedLevelColors.length) {
settings.barColor.rangeList = {
@ -192,7 +192,6 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
dashThickness: [settings.dashThickness, [Validators.min(0)]],
roundedLineCap: [settings.roundedLineCap, []],
defaultColor: [settings.defaultColor, []],
gaugeColor: [settings.gaugeColor, []],
barColor: [settings.barColor],

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

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

36
ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html

@ -76,6 +76,24 @@
</mat-form-field>
</div>
</div>
<div class="tb-form-panel no-border no-padding-top"
*ngIf="!mobileAppSettingsForm.get('useDefaultApp').value && mobileAppSettingsForm.get('androidConfig.enabled').value">
<div class="tb-form-row column-xs">
<div class="fixed-title-width-230">{{ 'admin.mobile-app.google-play-link' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="storeLink" placeholder="{{'admin.mobile-app.set' | translate}}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'admin.mobile-app.google-play-link-required' | translate"
*ngIf="mobileAppSettingsForm.get('androidConfig.storeLink').hasError('required')
&& mobileAppSettingsForm.get('androidConfig.storeLink').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel stroked no-padding no-gap" formGroupName="iosConfig">
<div class="tb-form-row no-border no-padding-bottom">
@ -101,6 +119,24 @@
</mat-form-field>
</div>
</div>
<div class="tb-form-panel no-border no-padding-top"
*ngIf="!mobileAppSettingsForm.get('useDefaultApp').value && mobileAppSettingsForm.get('iosConfig.enabled').value">
<div class="tb-form-row column-xs">
<div class="fixed-title-width-230">{{ 'admin.mobile-app.app-store-link' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="storeLink" placeholder="{{'admin.mobile-app.set' | translate}}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'admin.mobile-app.app-store-link-required' | translate"
*ngIf="mobileAppSettingsForm.get('iosConfig.storeLink').hasError('required')
&& mobileAppSettingsForm.get('iosConfig.storeLink').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="qrCodeConfig">

14
ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts

@ -57,14 +57,18 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf
if (value) {
this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false});
this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false});
this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false});
} else {
if (this.mobileAppSettingsForm.get('androidConfig.enabled').value) {
this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false});
}
if (this.mobileAppSettingsForm.get('iosConfig.enabled').value) {
this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false});
this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false});
}
}
});
@ -128,11 +132,13 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf
androidConfig: this.fb.group({
enabled: [true],
appPackage: [{value: '', disabled: true}, [Validators.required]],
sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]]
sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]],
storeLink: ['', [Validators.required]]
}),
iosConfig: this.fb.group({
enabled: [true],
appId: [{value: '', disabled: true}, [Validators.required]]
appId: [{value: '', disabled: true}, [Validators.required]],
storeLink: ['', [Validators.required]]
}),
qrCodeConfig: this.fb.group({
showOnHomePage: [true],
@ -154,10 +160,12 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf
if (!this.mobileAppSettingsForm.get('useDefaultApp').value) {
this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.storeLink').enable({emitEvent: false});
}
} else {
this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false});
this.mobileAppSettingsForm.get('androidConfig.storeLink').disable({emitEvent: false});
}
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true});
}
@ -166,9 +174,11 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf
if (value) {
if (!this.mobileAppSettingsForm.get('useDefaultApp').value) {
this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false});
this.mobileAppSettingsForm.get('iosConfig.storeLink').enable({emitEvent: false});
}
} else {
this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false});
this.mobileAppSettingsForm.get('iosConfig.storeLink').disable({emitEvent: false});
}
this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true});
}

5
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -249,6 +249,11 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
this.noEntitiesMatchingText = 'alarm.no-alarms-matching';
this.entityRequiredText = 'alarm.alarm-required';
break;
case EntityType.QUEUE_STATS:
this.entityText = 'queue-statistics.queue-statistics';
this.noEntitiesMatchingText = 'queue-statistics.no-queue-statistics-matching';
this.entityRequiredText = 'queue-statistics.queue-statistics-required';
break;
case AliasEntityType.CURRENT_CUSTOMER:
this.entityText = 'customer.default-customer';
this.noEntitiesMatchingText = 'customer.no-customers-matching';

5
ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts

@ -90,8 +90,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
}
ngOnInit() {
this.entityTypes = this.filterAllowedEntityTypes ?
this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes;
this.entityTypes = this.filterAllowedEntityTypes
? this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes)
: this.allowedEntityTypes;
this.entityTypeFormGroup.get('entityType').valueChanges.subscribe(
(value) => {
let modelValue;

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

@ -367,7 +367,11 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
EntityType.QUEUE_STATS,
{
type: 'entity.type-queue-stats',
typePlural: 'entity.type-queues-stats'
typePlural: 'entity.type-queues-stats',
list: 'queue-statistics.list-of-queue-statistics',
noEntities: 'queue-statistics.no-queue-statistics-text',
selectedEntities: 'queue-statistics.selected-queue-statistics',
nameStartsWith: 'queue-statistics.queue-statistics-starts-with',
}
],
[

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

@ -25,6 +25,8 @@ export enum LimitedApi {
CASSANDRA_QUERIES = 'CASSANDRA_QUERIES',
TRANSPORT_MESSAGES_PER_TENANT = 'TRANSPORT_MESSAGES_PER_TENANT',
TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE',
TRANSPORT_MESSAGES_PER_GATEWAY = 'TRANSPORT_MESSAGES_PER_GATEWAY',
TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE = 'TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE',
EDGE_EVENTS = 'EDGE_EVENTS',
EDGE_EVENTS_PER_EDGE = 'EDGE_EVENTS_PER_EDGE',
EDGE_UPLINK_MESSAGES = 'EDGE_UPLINK_MESSAGES',
@ -43,6 +45,8 @@ export const LimitedApiTranslationMap = new Map<LimitedApi, string>(
[LimitedApi.CASSANDRA_QUERIES, 'api-limit.cassandra-queries'],
[LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'],
[LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device'],
[LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY, 'api-limit.transport-messages-per-gateway'],
[LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE, 'api-limit.transport-messages-per-gateway_device'],
[LimitedApi.EDGE_EVENTS, 'api-limit.edge-events'],
[LimitedApi.EDGE_EVENTS_PER_EDGE, 'api-limit.edge-events-per-edge'],
[LimitedApi.EDGE_UPLINK_MESSAGES, 'api-limit.edge-uplink-messages'],

6
ui-ngx/src/app/shared/models/mobile-app.models.ts

@ -21,17 +21,21 @@ export interface MobileAppSettings extends HasTenantId {
androidConfig: AndroidConfig;
iosConfig: IosConfig;
qrCodeConfig: QRCodeConfig;
defaultGooglePlayLink: string;
defaultAppStoreLink: string;
}
export interface AndroidConfig {
enabled: boolean;
appPackage: string;
sha256CertFingerprints: string
sha256CertFingerprints: string;
storeLink: string;
}
export interface IosConfig {
enabled: boolean;
appId: string;
storeLink: string;
}
export interface QRCodeConfig {

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

@ -126,3 +126,8 @@ export interface QueueInfo extends BaseData<QueueId>, HasTenantId {
duplicateMsgToAllPartitions?: boolean;
};
}
export interface QueueStatisticsInfo extends Omit<BaseData<QueueId>, 'label'>, HasTenantId {
queueName: string;
serviceId: string;
}

13
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -297,6 +297,12 @@ export const defaultGradient = (minValue?: number, maxValue?: number): ColorGrad
maxValue: isDefinedAndNotNull(maxValue) ? maxValue : 100
});
export const defaultRange = (): ColorRangeSettings => ({
advancedMode: false,
range: [],
rangeAdvanced: []
});
const updateGradientMinMaxValues = (colorSettings: ColorSettings, minValue?: number, maxValue?: number): void => {
if (isDefinedAndNotNull(colorSettings.gradient)) {
if (isDefinedAndNotNull(minValue)) {
@ -431,7 +437,7 @@ export abstract class AdvancedModeColorProcessor extends ColorProcessor {
protected constructor(protected settings: ColorSettings,
protected ctx: WidgetContext) {
super(settings);
this.advancedMode = this.getCurrentConfig().advancedMode;
this.advancedMode = this.getCurrentConfig()?.advancedMode;
if (this.advancedMode) {
createValueSubscription(
this.ctx,
@ -529,7 +535,7 @@ class RangeColorProcessor extends AdvancedModeColorProcessor {
this.settings.rangeList.range as Array<ColorRange>;
}
if (rangeList.length && isDefinedAndNotNull(value) && isNumeric(value)) {
if (rangeList?.length && isDefinedAndNotNull(value) && isNumeric(value)) {
const num = Number(value);
for (const range of rangeList) {
if (advancedMode ?
@ -571,7 +577,8 @@ class GradientColorProcessor extends AdvancedModeColorProcessor {
update(value: any): void {
const progress = this.calculateProgress(+value, this.minValue, this.maxValue);
super.update(progress);
this.color = this.getGradientColor(progress, this.settings.gradient.gradient);
this.color = this.getGradientColor(progress,
this.advancedMode ? this.settings.gradient.gradientAdvanced : this.settings.gradient.gradient);
}
updatedAdvancedData(data: Array<DatasourceData>) {

2
ui-ngx/src/app/shared/models/widget.models.ts

@ -73,7 +73,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
icon: 'timeline',
configHelpLinkId: 'widgetsConfigTimeseries',
template: {
fullFqn: 'system.charts.basic_timeseries'
fullFqn: 'system.time_series_chart'
}
}
],

2
ui-ngx/src/assets/dashboard/sys_admin_home_page.json

@ -2665,7 +2665,7 @@
"dropShadow": false,
"enableFullscreen": false,
"widgetStyle": {},
"widgetCss": " .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding: 0;\n}\n\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 8px;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n padding: 0;\n}\n\n@media screen and (min-width: 960px) and (max-height: 900px) {\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 0;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode-label {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1190px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n gap: 0;\n }\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n min-width: 150px;\n }\n}\n\n@media screen and (max-width: 960px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 110px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 140px;\n }\n}\n\n@media screen and (min-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 125px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 160px;\n }\n}",
"widgetCss": " .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding: 0;\n}\n\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 8px;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n padding: 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 0;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1190px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n gap: 0;\n }\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n min-width: 150px;\n }\n}\n\n@media screen and (max-width: 960px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 110px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 140px;\n }\n}\n\n@media screen and (min-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 125px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 160px;\n }\n}\n\n@media screen and (min-width: 960px) and (max-height: 960px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode-label {\n display: none;\n }\n}",
"showTitleIcon": false,
"titleTooltip": "",
"titleStyle": null,

2
ui-ngx/src/assets/dashboard/tenant_admin_home_page.json

@ -1208,7 +1208,7 @@
"dropShadow": false,
"enableFullscreen": false,
"widgetStyle": {},
"widgetCss": " .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding: 0;\n}\n\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 8px;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n padding: 0;\n}\n\n@media screen and (min-width: 960px) and (max-height: 900px) {\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 0;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode-label {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1182px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n gap: 0;\n }\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n min-width: 150px;\n }\n}\n\n@media screen and (max-width: 960px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 120px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 160px;\n }\n}\n\n@media screen and (min-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 125px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 160px;\n }\n}",
"widgetCss": " .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding: 0;\n}\n\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 8px;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n padding: 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content .tb-widget-title > .title-row > .title {\n padding-bottom: 0;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1182px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel {\n gap: 0;\n }\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n min-width: 150px;\n }\n}\n\n@media screen and (max-width: 960px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 120px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 160px;\n }\n}\n\n@media screen and (min-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode {\n max-width: 125px;\n }\n \n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-badges {\n max-width: 160px;\n }\n}\n\n@media screen and (min-width: 960px) and (max-height: 960px) {\n .tb-widget-container > .tb-widget > .tb-widget-content > .tb-widget .tb-mobile-app-qrcode-panel .tb-qrcode-label {\n display: none;\n }\n}",
"showTitleIcon": false,
"titleTooltip": "",
"titleStyle": null,

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

@ -437,6 +437,10 @@
"ios": "iOS",
"app-id": "App ID",
"app-id-required": "App ID is required",
"google-play-link": "Google Play link",
"google-play-link-required": "Google Play link is required",
"app-store-link": "App Store link",
"app-store-link-required": "App Store link is required",
"appearance": "Appearance",
"appearance-on-home-page": "Appearance on Home page",
"enabled": "Enabled",
@ -894,6 +898,8 @@
"rest-api-requests-per-customer": "REST API requests per customer",
"transport-messages": "Transport messages",
"transport-messages-per-device": "Transport messages per device",
"transport-messages-per-gateway": "Transport messages per gateway",
"transport-messages-per-gateway-device": "Transport messages per gateway device",
"ws-updates-per-session": "WS updates per session",
"edge-events": "Edge events",
"edge-events-per-edge": "Edge events per edge",
@ -4421,6 +4427,15 @@
"retry-failed-and-timeout-hint": "Retry all failed and timed-out messages from processing pack"
}
},
"queue-statistics": {
"queue-statistics": "Queue statistics",
"no-queue-statistics-matching": "No queue statistics matching '{{entity}}' were found.",
"queue-statistics-required": "Queue statistics is required.",
"list-of-queue-statistics": "{ count, plural, =1 {One queue statistic} other {List of # queue statistics} }",
"selected-queue-statistics": "{ count, plural, =1 {1 queue statistic} other {# queue statistics} } selected",
"no-queue-statistics-text": "No queue statistics found",
"queue-statistics-starts-with": "Queue statistics whose names start with '{{prefix}}'"
},
"server-error": {
"general": "General server error",
"authentication": "Authentication error",
@ -4547,6 +4562,9 @@
"transport-gateway-msg-rate-limit": "Transport gateway messages",
"transport-gateway-telemetry-msg-rate-limit": "Transport gateway telemetry messages",
"transport-gateway-telemetry-data-points-rate-limit": "Transport gateway telemetry data points",
"transport-gateway-device-msg-rate-limit": "Transport gateway device messages",
"transport-gateway-device-telemetry-msg-rate-limit": "Transport gateway device telemetry messages",
"transport-gateway-device-telemetry-data-points-rate-limit": "Transport gateway device telemetry data points",
"tenant-entity-export-rate-limit": "Entity version creation",
"tenant-entity-import-rate-limit": "Entity version load",
"tenant-notification-request-rate-limit": "Notification requests",
@ -4631,6 +4649,9 @@
"edit-transport-gateway-msg-title": "Edit transport gateway messages rate limits",
"edit-transport-gateway-telemetry-msg-title": "Edit transport gateway telemetry messages rate limits",
"edit-transport-gateway-telemetry-data-points-title": "Edit transport gateway telemetry data points rate limits",
"edit-transport-gateway-device-msg-title": "Edit transport gateway device messages rate limits",
"edit-transport-gateway-device-telemetry-msg-title": "Edit transport gateway device telemetry messages rate limits",
"edit-transport-gateway-device-telemetry-data-points-title": "Edit transport gateway device telemetry data points rate limits",
"edit-tenant-rest-limits-title": "Edit REST requests for tenant rate limits",
"edit-customer-rest-limits-title": "Edit REST requests for customer rate limits",
"edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits",
@ -4667,6 +4688,9 @@
"transport-gateway-msg": "Transport gateway messages",
"transport-gateway-telemetry-msg": "Transport gateway telemetry messages",
"transport-gateway-telemetry-data-points": "Transport gateway telemetry data points",
"transport-gateway-device-msg": "Transport gateway device messages",
"transport-gateway-device-telemetry-msg": "Transport gateway device telemetry messages",
"transport-gateway-device-telemetry-data-points": "Transport gateway device telemetry data points",
"sec": "sec"
}
},
@ -5664,7 +5688,9 @@
},
"widgets": {
"mobile-app-qr-code": {
"configuration-hint": "The configuration depends on the Mobile app QR code widget in platform main settings"
"configuration-hint": "The configuration depends on the Mobile app QR code widget in platform main settings",
"get-it-on-google-play": "Get it on Google Play",
"download-on-the-app-store": "Download on the App Store"
},
"action-button": {
"behavior": "Behavior",
@ -6305,6 +6331,7 @@
"gauge-bar-background": "Gauge bar background",
"bar-color": "Bar color",
"min-and-max-value": "Min and max value",
"min-and-max-label": "Min and max label",
"font": "Font",
"tick-width-and-color": "Tick width and color"
},

Loading…
Cancel
Save