diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index f3929d88f1..3aef1f30fd 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -11,13 +11,13 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 6967b3c13b..58b9296379 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/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 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 { diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 3a39eb5177..9e05dc991d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/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. "; diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index d93afab66d..6ebb6fffe8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/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) diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index 77e34fc18c..ca8b11ee80 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/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) diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java new file mode 100644 index 0000000000..912a1ca7b2 --- /dev/null +++ b/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 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 getQueueStatsByIds( + @Parameter(description = "A list of queue stats ids, separated by comma ','", required = true) + @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { + checkArrayParameter("queueStatsIds", strQueueStatsIds); + List queueStatsIds = new ArrayList<>(); + for (String queueStatsId : strQueueStatsIds) { + queueStatsIds.add(new QueueStatsId(toUUID(queueStatsId))); + } + return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 097aec6306..9c42683b00 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/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" + diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index d8ba443d85..6636de3ff1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/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 { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 454ca9427f..4e5458bd18 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/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}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java index ad52934587..6620b1a448 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java +++ b/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 queueStatsList = queueStatsService.findByTenantId(tenantId); - assertThat(queueStatsList).hasSize(1); - QueueStats queueStats = queueStatsList.get(0); + PageData 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 ttlCaptor = ArgumentCaptor.forClass(Long.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 388f725665..50c82260eb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/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 entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + + PageData data = + doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + + 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 dataAfterUnassign = + doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() { + }); + + 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") diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java index a1986ca2c2..b7569bc816 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java +++ b/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 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(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java index c9e3f605c6..b34239909f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueStatsService.java +++ b/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 findQueueStatsByIds(TenantId tenantId, List queueStatsId); + QueueStats findByTenantIdAndNameAndServiceId(TenantId tenantId, String queueName, String serviceId); - List findByTenantId(TenantId tenantId); + PageData findByTenantId(TenantId tenantId, PageLink pageLink); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 11702100d5..7aa472bea1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/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 configExtractor; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java index e1fb5056fc..d670382462 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java +++ b/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; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java index f03b446b3b..7d40dfe805 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java +++ b/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; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java index 9a7b88f22c..31b2029bfc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java +++ b/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 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); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index d990c300da..40f4b7f42b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/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; diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index f5e5403fd6..274b5a4e70 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/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(); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index c420685963..809a47e937 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/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 { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java index 6ba41c245e..e0b8e261f8 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java +++ b/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(); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java index ae58c3f1a9..c7239673f6 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java +++ b/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; } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java index 975dd21fab..d070465c23 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java +++ b/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 tenantAllowed = new ConcurrentHashMap<>(); private final ConcurrentMap> tenantDevices = new ConcurrentHashMap<>(); private final ConcurrentMap> tenantGateways = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantGatewayDevices = new ConcurrentHashMap<>(); private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); private final ConcurrentMap perGatewayLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap perGatewayDeviceLimits = new ConcurrentHashMap<>(); private final Map ipMap = new ConcurrentHashMap<>(); private final TransportTenantProfileCache tenantProfileCache; @@ -72,21 +75,23 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi } @Override - public TbPair checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints) { + public TbPair 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 getTenantGatewayDevices(TenantId tenantId) { + return tenantGatewayDevices.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet()); + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java index ab65b07414..0c822f9589 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java +++ b/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 } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java index e33f9db240..aefcecb630 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java +++ b/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 checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints); + TbPair checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints, boolean isGateway); void update(TenantProfileUpdateResult update); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index e82e7812fb..cc91baca0c 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/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 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()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java index 57e95cc56f..5ec717da09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/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 mobileAppSettingsDataValidator; @@ -49,7 +55,7 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService findQueueStatsByIds(TenantId tenantId, List 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 findByTenantId(TenantId tenantId) { + public PageData 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java index 1c3db8bb54..5466a8afcc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueStatsDao.java +++ b/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 findByTenantIdQueueNameAndServiceId(TenantId tenantId, String queueName, String serviceId); - List findByTenantId(TenantId tenantId); + PageData findByTenantId(TenantId tenantId, PageLink pageLink); void deleteByTenantId(TenantId tenantId); + List findByIds(TenantId tenantId, List queueStatsIds); + } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index fef55380e4..dd1b4278d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/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"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java index 1cfdbf0b38..f680a42dc1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java +++ b/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 findByTenantId(TenantId tenantId) { - return DaoUtil.convertDataList(queueStatsRepository.findByTenantId(tenantId.getId())); + public PageData 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 findByIds(TenantId tenantId, List queueStatsIds) { + return DaoUtil.convertDataList(queueStatsRepository.findByTenantIdAndIdIn(tenantId.getId(), toUUIDs(queueStatsIds))); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java index 65df3a65fa..585f010469 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueStatsRepository.java +++ b/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 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 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 findByTenantIdAndIdIn(UUID tenantId, List queueStatsIds); + } \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java index 45a49ba5d2..7919893d00 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java +++ b/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 queueStatsList = queueStatsService.findByTenantId(tenantId); - Assert.assertEquals(2, queueStatsList.size()); - assertThat(queueStatsList).containsOnly(retrievedQueueStatsById, retrievedQueueStatsById2); + PageData 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()); diff --git a/packaging/java/scripts/windows/install.bat b/packaging/java/scripts/windows/install.bat index 075755886e..ca17408dfe 100644 --- a/packaging/java/scripts/windows/install.bat +++ b/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" diff --git a/packaging/java/scripts/windows/install_dev_db.bat b/packaging/java/scripts/windows/install_dev_db.bat index 522d0bfc93..c2d416c030 100644 --- a/packaging/java/scripts/windows/install_dev_db.bat +++ b/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 diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 8777cc9780..b5b2b0e796 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/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); } diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index b300a1c0a7..ed2a189e0d 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/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> { + return this.http.get>(`/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 { + return this.http.get(`/api/queueStats/${queueStatId}`, defaultHttpOptionsFromConfig(config)).pipe( + map(queueStat => this.parseQueueStatName(queueStat))); + } + + public getQueueStatisticsByIds(queueStatIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`, + defaultHttpOptionsFromConfig(config)).pipe( + map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat)) + ) + ); + } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 61d22dcff4..d1b39a8748 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -37,7 +37,8 @@ @@ -65,7 +67,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts index 0bc19aff01..31b31960f8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts +++ b/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; + listEntityTypes: Array; + 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]] }); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 91f96c7aba..31724191cf 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -520,13 +520,17 @@ -
+ +
-
+ +
@@ -547,7 +551,9 @@ -
+ +
( [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 - - - warning - +
@@ -110,6 +101,12 @@
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts index 25f091df7b..f415e1d17c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html index 4780983685..c9821f1a88 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html @@ -100,6 +100,8 @@ [previewText]="valuePreviewFn"> @@ -137,6 +139,8 @@
{{ 'widgets.progress-bar.bar-color' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html index 84deb5ceaf..9f170dff8c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html @@ -53,17 +53,9 @@
- {{ 'widgets.gauge.min-and-max-value' | translate }} + {{ 'widgets.gauge.min-and-max-label' | translate }}
-
widgets.gauge.min-value-short
- - - -
widgets.gauge.max-value-short
- - - @@ -74,6 +66,19 @@
+
+
{{ 'widgets.gauge.min-and-max-value' | translate }}
+
+
widgets.gauge.min-value-short
+ + + +
widgets.gauge.max-value-short
+ + + +
+
@@ -117,14 +122,6 @@
-
-
widgets.gauge.default-color
- - -
-
widgets.gauge.gauge-bar-background
{{ 'widgets.gauge.bar-color' | translate }}
{ - 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) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index 9031620f84..ede5962922 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 6dc33eb07f..1426d02a0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -16,6 +16,7 @@ -->
@@ -24,11 +25,18 @@
-
-
- - +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index b13fbe0f8f..5bd423d796 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/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%; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index 6aeef19b19..b484b9eed1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/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; 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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html index ea849e4f30..6da0bb97c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html +++ b/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 }} - - - warning - +
@@ -69,5 +60,11 @@
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index d4bae4624e..8215c2e14a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/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, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html index 7b849f0b00..f9543b11cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html @@ -44,6 +44,8 @@ [previewText]="valuePreviewFn"> @@ -78,6 +80,8 @@
{{ 'widgets.progress-bar.bar-color' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html index 2c0787c778..3bd47d9921 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html @@ -38,20 +38,22 @@
-
-
-
+
+
widgets.color.from
+
widgets.color.to
+ @@ -64,15 +66,15 @@ matTooltipPosition="above"> delete -
+
@@ -103,9 +105,10 @@
- +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss index 63265f6dbc..21cbd8e287 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss +++ b/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; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts index cbc258caa7..c12ba38718 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts +++ b/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, 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()); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss index 1b0fda35b3..31b5f3745c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss +++ b/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) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts index 94cb084512..89edf5323a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts +++ b/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()); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts index 11837804df..0c879f52e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss index 38f320b940..25e14cb0d5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts index e326cff0d4..8b78190f5a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts +++ b/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, 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() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html index 86cd0ff825..62cacf0730 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html @@ -41,17 +41,9 @@
- {{ 'widgets.gauge.min-and-max-value' | translate }} + {{ 'widgets.gauge.min-and-max-label' | translate }}
-
widgets.gauge.min-value-short
- - - -
widgets.gauge.max-value-short
- - - @@ -63,6 +55,20 @@
+
+
{{ 'widgets.gauge.min-and-max-value' | translate }}
+
+
widgets.gauge.min-value-short
+ + + +
widgets.gauge.max-value-short
+ + + +
+
+
{{ 'widgets.gauge.value' | translate }} @@ -160,14 +166,6 @@
-
-
{{ 'widgets.gauge.default-color' | translate }}
- - -
-
{{ 'widgets.gauge.gauge-bar-background' | translate }}
{{ 'widgets.gauge.bar-color' | translate }}
- + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts index 9f55ad90a1..5a4289a8d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts +++ b/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], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html index 2f83df7efa..c34b51a36b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html @@ -63,8 +63,6 @@
+
+
+
{{ 'admin.mobile-app.google-play-link' | translate }}
+ + + + warning + + +
+
@@ -101,6 +119,24 @@
+
+
+
{{ 'admin.mobile-app.app-store-link' | translate }}
+ + + + warning + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index edba183919..0f2527d833 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/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}); } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 9792d9f1e9..8703e0a10b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/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'; diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index 1bd2861b70..04754e4a20 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 885ea90929..045e95983c 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -367,7 +367,11 @@ export const entityTypeTranslations = new Map( [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'], diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index 7b98da0744..b879a9eb45 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/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 { diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index eb23cc1269..5e1559cda9 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -126,3 +126,8 @@ export interface QueueInfo extends BaseData, HasTenantId { duplicateMsgToAllPartitions?: boolean; }; } + +export interface QueueStatisticsInfo extends Omit, 'label'>, HasTenantId { + queueName: string; + serviceId: string; +} diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts index 681913c24c..5124458ff0 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/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; } - 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) { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 88d537220e..01a7e98215 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -73,7 +73,7 @@ export const widgetTypesData = new Map( icon: 'timeline', configHelpLinkId: 'widgetsConfigTimeseries', template: { - fullFqn: 'system.charts.basic_timeseries' + fullFqn: 'system.time_series_chart' } } ], diff --git a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json index bb38c53b6f..20e3b0b48a 100644 --- a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json +++ b/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, diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index bdc90f529f..e3277ac88a 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/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, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c9cf8ef663..b922ff1b10 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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" },