diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 858d816338..be46aebec1 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -267,6 +267,30 @@ "basicModeDirective": "tb-value-card-basic-config", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\",\"layout\":\"horizontal\",\"showLabel\":true,\"labelFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"labelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showIcon\":true,\"iconSize\":40,\"iconSizeUnit\":\"px\",\"icon\":\"thermostat\",\"iconColor\":{\"type\":\"constant\",\"color\":\"#5469FF\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":52,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showDate\":true,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\"},\"dateColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null}}" } + }, + { + "alias": "aggregated_value_card", + "name": "Aggregated value card", + "image": null, + "description": null, + "descriptor": { + "type": "timeseries", + "sizeX": 4.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onDataUpdated();\n};\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.aggregatedValueCardWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '400px',\n previewHeight: '300px',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "tb-aggregated-value-card-key-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-aggregated-value-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"Main building\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"watermeter\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Avg watermeter\",\"color\":\"#4caf50\",\"settings\":{\"position\":\"center\",\"font\":{\"size\":52,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"1\"},\"color\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"rangeList\":[],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showArrow\":false},\"_hash\":0.9408410830697858,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m³\",\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Delta percent watermeter\",\"color\":\"#f44336\",\"settings\":{\"position\":\"rightTop\",\"font\":{\"size\":14,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"20px\"},\"color\":{\"type\":\"range\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"rangeList\":[{\"from\":null,\"to\":0,\"color\":\"#198038\"},{\"from\":0,\"to\":0,\"color\":\"rgba(0, 0, 0, 0.87)\"},{\"from\":0,\"to\":null,\"color\":\"#D12730\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showArrow\":true},\"_hash\":0.06392321853157967,\"funcBody\":\"var value = prevValue + Math.random() * 6 - 3;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -25) {\\n\\tvalue = -25;\\n} else if (value > 25) {\\n\\tvalue = 25;\\n} \\nreturn value;\",\"aggregationType\":null,\"units\":\"%\",\"decimals\":0,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random 2\",\"color\":\"#607d8b\",\"settings\":{\"position\":\"rightBottom\",\"font\":{\"size\":11,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.38)\",\"rangeList\":[],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showArrow\":false},\"_hash\":0.44695098620509865,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m³\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1691927717318,\"endTimeMs\":1692014117318},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":null,\"padding\":\"0\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Aggregated value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":true,\"titleIcon\":\"water_drop\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"decimals\":0,\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":false}}" + } } ] } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index ed94c8fe69..a88a7ee4f6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestPreview; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; @@ -299,7 +300,7 @@ public class NotificationController extends BaseController { case PLATFORM_USERS: { PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, - new SortOrder("createdTime", SortOrder.Direction.DESC))); + SortOrder.BY_CREATED_TIME_DESC)); recipientsCount = (int) recipients.getTotalElements(); recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); break; @@ -444,10 +445,23 @@ public class NotificationController extends BaseController { notes = "Returns the list of delivery methods that are properly configured and are allowed to be used for sending notifications." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/notification/deliveryMethods") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public Set getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { - accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); } + + @PostMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings saveUserNotificationSettings(@RequestBody @Valid UserNotificationSettings settings, + @AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.saveUserNotificationSettings(user.getTenantId(), user.getId(), settings); + } + + @GetMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings getUserNotificationSettings(@AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.getUserNotificationSettings(user.getTenantId(), user.getId(), true); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 4f0f5d277a..3c1ec796f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -39,12 +39,16 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -53,6 +57,7 @@ import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; @@ -151,8 +156,8 @@ public final class EdgeGrpcSession implements Closeable { } if (connected) { if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { - if (requestMsg.hasSyncRequestMsg() && requestMsg.getSyncRequestMsg().getSyncRequired()) { - boolean fullSync = true; + if (requestMsg.hasSyncRequestMsg()) { + boolean fullSync = false; if (requestMsg.getSyncRequestMsg().hasFullSync()) { fullSync = requestMsg.getSyncRequestMsg().getFullSync(); } @@ -654,6 +659,11 @@ public final class EdgeGrpcSession implements Closeable { result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData)); } } + if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { + for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { + result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg)); + } + } if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg)); @@ -664,16 +674,36 @@ public final class EdgeGrpcSession implements Closeable { result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsg(edge.getTenantId(), deviceCredentialsUpdateMsg)); } } + if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) { + for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) { + result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg)); + } + } + if (uplinkMsg.getAssetUpdateMsgCount() > 0) { + for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) { + result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); + } + } if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { result.add(ctx.getAlarmProcessor().processAlarmMsg(edge.getTenantId(), alarmUpdateMsg)); } } + if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) { + for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) { + result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg)); + } + } if (uplinkMsg.getRelationUpdateMsgCount() > 0) { for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { result.add(ctx.getRelationProcessor().processRelationMsg(edge.getTenantId(), relationUpdateMsg)); } } + if (uplinkMsg.getDashboardUpdateMsgCount() > 0) { + for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) { + result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg)); + } + } if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index bf3e804f7e..1eab7c3232 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -51,8 +51,6 @@ public class EdgeSyncCursor { fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); - fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); - fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); @@ -61,6 +59,8 @@ public class EdgeSyncCursor { fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } } + fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); + fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService())); fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 56f4f06fa3..ea2236374e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -23,9 +23,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -43,8 +48,11 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -106,6 +114,7 @@ import java.util.concurrent.locks.ReentrantLock; public abstract class BaseEdgeProcessor { protected static final Lock deviceCreationLock = new ReentrantLock(); + protected static final Lock assetCreationLock = new ReentrantLock(); protected static final int DEFAULT_PAGE_SIZE = 100; @@ -197,6 +206,21 @@ public abstract class BaseEdgeProcessor { @Autowired protected DataValidator deviceValidator; + @Autowired + protected DataValidator deviceProfileValidator; + + @Autowired + protected DataValidator assetValidator; + + @Autowired + protected DataValidator assetProfileValidator; + + @Autowired + protected DataValidator dashboardValidator; + + @Autowired + protected DataValidator entityViewValidator; + @Autowired protected EdgeMsgConstructor edgeMsgConstructor; @@ -520,4 +544,28 @@ public abstract class BaseEdgeProcessor { return false; } } + + protected void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(edgeId); + relation.setTo(entityId); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relation.setType(EntityRelation.EDGE_TYPE); + relationService.saveRelation(tenantId, relation); + } + + protected TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { + TbMsgMetaData metaData = getTbMsgMetaData(edge); + if (customerId != null && !customerId.isNullUid()) { + metaData.putValue("customerId", customerId.toString()); + } + return metaData; + } + + protected TbMsgMetaData getTbMsgMetaData(Edge edge) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("edgeId", edge.getId().toString()); + metaData.putValue("edgeName", edge.getName()); + return metaData; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index d7824a7467..3e61a538c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -15,23 +15,113 @@ */ package org.thingsboard.server.service.edge.rpc.processor.asset; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.dao.asset.BaseAssetService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class AssetEdgeProcessor extends BaseEdgeProcessor { +public class AssetEdgeProcessor extends BaseAssetProcessor { + + public ListenableFuture processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg) { + log.trace("[{}] executing processAssetMsgFromEdge [{}] from edge [{}]", tenantId, assetUpdateMsg, edge.getName()); + AssetId assetId = new AssetId(new UUID(assetUpdateMsg.getIdMSB(), assetUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (assetUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + Asset assetToDelete = assetService.findAssetById(tenantId, assetId); + if (assetToDelete != null) { + assetService.unassignAssetFromEdge(tenantId, assetId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(assetUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed asset violated {}", tenantId, assetUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB()); + Pair resultPair = super.saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, customerId); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), assetId); + pushAssetCreatedEventToRuleEngine(tenantId, edge, assetId); + assetService.assignAssetToEdge(tenantId, assetId, edge.getId()); + } + Boolean assetNameUpdated = resultPair.getSecond(); + if (assetNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.UPDATED, assetId, null); + } + } + + private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetId assetId) { + try { + Asset asset = assetService.findAssetById(tenantId, assetId); + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(asset); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, assetId, asset.getCustomerId(), + getActionTbMsgMetaData(edge, asset.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, assetId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", asset); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", asset, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push asset action to rule engine: {}", assetId, DataConstants.ENTITY_CREATED, e); + } + } public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); @@ -43,7 +133,7 @@ public class AssetEdgeProcessor extends BaseEdgeProcessor { case ASSIGNED_TO_CUSTOMER: case UNASSIGNED_FROM_CUSTOMER: Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId); - if (asset != null) { + if (asset != null && !BaseAssetService.TB_SERVICE_QUEUE.equals(asset.getType())) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); AssetUpdateMsg assetUpdateMsg = assetMsgConstructor.constructAssetUpdatedMsg(msgType, asset); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java index ec0e0b9761..3cae9c5123 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java @@ -15,22 +15,91 @@ */ package org.thingsboard.server.service.edge.rpc.processor.asset; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class AssetProfileEdgeProcessor extends BaseEdgeProcessor { +public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { + + public ListenableFuture processAssetProfileMsgFromEdge(TenantId tenantId, Edge edge, AssetProfileUpdateMsg assetProfileUpdateMsg) { + log.trace("[{}] executing processAssetProfileMsgFromEdge [{}] from edge [{}]", tenantId, assetProfileUpdateMsg, edge.getName()); + AssetProfileId assetProfileId = new AssetProfileId(new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (assetProfileUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(assetProfileUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + log.warn("Failed to process AssetProfileUpdateMsg from Edge [{}]", assetProfileUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg, Edge edge) { + boolean created = super.saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), assetProfileId); + pushAssetProfileCreatedEventToRuleEngine(tenantId, edge, assetProfileId); + } + } + + private void pushAssetProfileCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetProfileId assetProfileId) { + try { + AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(assetProfile); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, assetProfileId, getTbMsgMetaData(edge), + TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, assetProfileId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", assetProfile); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", assetProfile, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push asset profile action to rule engine: {}", assetProfileId, DataConstants.ENTITY_CREATED, e); + } + } public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) { AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java new file mode 100644 index 0000000000..5d728cb4e8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; + +@Slf4j +public abstract class BaseAssetProcessor extends BaseEdgeProcessor { + + protected Pair saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, CustomerId customerId) { + boolean created = false; + boolean assetNameUpdated = false; + assetCreationLock.lock(); + try { + Asset asset = assetService.findAssetById(tenantId, assetId); + String assetName = assetUpdateMsg.getName(); + if (asset == null) { + created = true; + asset = new Asset(); + asset.setTenantId(tenantId); + asset.setCreatedTime(Uuids.unixTimestamp(assetId.getId())); + } + Asset assetByName = assetService.findAssetByTenantIdAndName(tenantId, assetName); + if (assetByName != null && !assetByName.getId().equals(assetId)) { + assetName = assetName + "_" + StringUtils.randomAlphanumeric(15); + log.warn("Asset with name {} already exists. Renaming asset name to {}", + assetUpdateMsg.getName(), assetName); + assetNameUpdated = true; + } + asset.setName(assetName); + asset.setType(assetUpdateMsg.getType()); + asset.setLabel(assetUpdateMsg.hasLabel() ? assetUpdateMsg.getLabel() : null); + asset.setAdditionalInfo(assetUpdateMsg.hasAdditionalInfo() + ? JacksonUtil.toJsonNode(assetUpdateMsg.getAdditionalInfo()) : null); + + UUID assetProfileUUID = safeGetUUID(assetUpdateMsg.getAssetProfileIdMSB(), assetUpdateMsg.getAssetProfileIdLSB()); + asset.setAssetProfileId(assetProfileUUID != null ? new AssetProfileId(assetProfileUUID) : null); + + asset.setCustomerId(customerId); + + assetValidator.validate(asset, Asset::getTenantId); + if (created) { + asset.setId(assetId); + } + assetService.saveAsset(asset, false); + } finally { + assetCreationLock.unlock(); + } + return Pair.of(created, assetNameUpdated); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java new file mode 100644 index 0000000000..4fe778402f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@Slf4j +public class BaseAssetProfileProcessor extends BaseEdgeProcessor { + + protected boolean saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg) { + boolean created = false; + assetCreationLock.lock(); + try { + AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + String assetProfileName = assetProfileUpdateMsg.getName(); + if (assetProfile == null) { + created = true; + assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + assetProfile.setCreatedTime(Uuids.unixTimestamp(assetProfileId.getId())); + } + assetProfile.setName(assetProfileName); + assetProfile.setDefault(assetProfileUpdateMsg.getDefault()); + assetProfile.setDefaultQueueName(assetProfileUpdateMsg.hasDefaultQueueName() ? assetProfileUpdateMsg.getDefaultQueueName() : null); + assetProfile.setDescription(assetProfileUpdateMsg.hasDescription() ? assetProfileUpdateMsg.getDescription() : null); + assetProfile.setImage(assetProfileUpdateMsg.hasImage() + ? new String(assetProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); + + UUID defaultRuleChainUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultRuleChainIdMSB(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()); + assetProfile.setDefaultRuleChainId(defaultRuleChainUUID != null ? new RuleChainId(defaultRuleChainUUID) : null); + + UUID defaultDashboardUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultDashboardIdMSB(), assetProfileUpdateMsg.getDefaultDashboardIdLSB()); + assetProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); + + assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId); + if (created) { + assetProfile.setId(assetProfileId); + } + assetProfileService.saveAssetProfile(assetProfile, false); + } finally { + assetCreationLock.unlock(); + } + return created; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java new file mode 100644 index 0000000000..b055c03cf7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.dashboard; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.Set; + +@Slf4j +public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { + + protected boolean saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, CustomerId customerId) { + boolean created = false; + Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); + if (dashboard == null) { + created = true; + dashboard = new Dashboard(); + dashboard.setTenantId(tenantId); + dashboard.setCreatedTime(Uuids.unixTimestamp(dashboardId.getId())); + } + dashboard.setTitle(dashboardUpdateMsg.getTitle()); + dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardUpdateMsg.getConfiguration())); + Set assignedCustomers = null; + if (dashboardUpdateMsg.hasAssignedCustomers()) { + assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() { + }); + dashboard.setAssignedCustomers(assignedCustomers); + } + + dashboardValidator.validate(dashboard, Dashboard::getTenantId); + if (created) { + dashboard.setId(dashboardId); + } + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard, false); + if (assignedCustomers != null && !assignedCustomers.isEmpty()) { + for (ShortCustomerInfo assignedCustomer : assignedCustomers) { + if (assignedCustomer.getCustomerId().equals(customerId)) { + dashboardService.assignDashboardToCustomer(tenantId, dashboardId, assignedCustomer.getCustomerId()); + } + } + } else { + unassignCustomersFromDashboard(tenantId, savedDashboard); + } + return created; + } + + private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard) { + if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) { + for (ShortCustomerInfo assignedCustomer : dashboard.getAssignedCustomers()) { + dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId()); + } + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index 14f566db0a..59f12ec64b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -15,22 +15,103 @@ */ package org.thingsboard.server.service.edge.rpc.processor.dashboard; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class DashboardEdgeProcessor extends BaseEdgeProcessor { +public class DashboardEdgeProcessor extends BaseDashboardProcessor { + + public ListenableFuture processDashboardMsgFromEdge(TenantId tenantId, Edge edge, DashboardUpdateMsg dashboardUpdateMsg) { + log.trace("[{}] executing processDashboardMsgFromEdge [{}] from edge [{}]", tenantId, dashboardUpdateMsg, edge.getName()); + DashboardId dashboardId = new DashboardId(new UUID(dashboardUpdateMsg.getIdMSB(), dashboardUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (dashboardUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + Dashboard dashboardToDelete = dashboardService.findDashboardById(tenantId, dashboardId); + if (dashboardToDelete != null) { + dashboardService.unassignDashboardFromEdge(tenantId, dashboardId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(dashboardUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed dashboard violated {}", tenantId, dashboardUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(dashboardUpdateMsg.getCustomerIdMSB(), dashboardUpdateMsg.getCustomerIdLSB()); + boolean created = super.saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, customerId); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), dashboardId); + pushDashboardCreatedEventToRuleEngine(tenantId, edge, dashboardId); + dashboardService.assignDashboardToEdge(tenantId, dashboardId, edge.getId()); + } + } + + private void pushDashboardCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DashboardId dashboardId) { + try { + Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(dashboard); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, dashboardId, null, + getActionTbMsgMetaData(edge, null), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, dashboardId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", dashboard); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", dashboard, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push dashboard action to rule engine: {}", dashboardId, DataConstants.ENTITY_CREATED, e); + } + } public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java index 1421cf32c0..580f752ac1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -57,13 +57,13 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { device = new Device(); device.setTenantId(tenantId); device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId())); - Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); - if (deviceByName != null) { - deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); - log.warn("Device with name {} already exists. Renaming device name to {}", - deviceUpdateMsg.getName(), deviceName); - deviceNameUpdated = true; - } + } + Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); + if (deviceByName != null && !deviceByName.getId().equals(deviceId)) { + deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); + log.warn("Device with name {} already exists. Renaming device name to {}", + deviceUpdateMsg.getName(), deviceName); + deviceNameUpdated = true; } device.setName(deviceName); device.setType(deviceUpdateMsg.getType()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java new file mode 100644 index 0000000000..662a497528 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; + +@Slf4j +public class BaseDeviceProfileProcessor extends BaseEdgeProcessor { + + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + protected boolean saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + boolean created = false; + deviceCreationLock.lock(); + try { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + if (deviceProfile == null) { + created = true; + deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setCreatedTime(Uuids.unixTimestamp(deviceProfileId.getId())); + } + deviceProfile.setName(deviceProfileUpdateMsg.getName()); + deviceProfile.setDescription(deviceProfileUpdateMsg.hasDescription() ? deviceProfileUpdateMsg.getDescription() : null); + deviceProfile.setDefault(deviceProfileUpdateMsg.getDefault()); + deviceProfile.setType(DeviceProfileType.valueOf(deviceProfileUpdateMsg.getType())); + deviceProfile.setTransportType(deviceProfileUpdateMsg.hasTransportType() + ? DeviceTransportType.valueOf(deviceProfileUpdateMsg.getTransportType()) : DeviceTransportType.DEFAULT); + deviceProfile.setImage(deviceProfileUpdateMsg.hasImage() + ? new String(deviceProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); + deviceProfile.setProvisionType(deviceProfileUpdateMsg.hasProvisionType() + ? DeviceProfileProvisionType.valueOf(deviceProfileUpdateMsg.getProvisionType()) : DeviceProfileProvisionType.DISABLED); + deviceProfile.setProvisionDeviceKey(deviceProfileUpdateMsg.hasProvisionDeviceKey() + ? deviceProfileUpdateMsg.getProvisionDeviceKey() : null); + deviceProfile.setDefaultQueueName(deviceProfileUpdateMsg.getDefaultQueueName()); + + Optional profileDataOpt = + dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + deviceProfile.setProfileData(profileDataOpt.orElse(null)); + + UUID defaultRuleChainUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()); + deviceProfile.setDefaultRuleChainId(defaultRuleChainUUID != null ? new RuleChainId(defaultRuleChainUUID) : null); + + UUID defaultDashboardUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultDashboardIdMSB(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()); + deviceProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); + + String defaultQueueName = StringUtils.isNotBlank(deviceProfileUpdateMsg.getDefaultQueueName()) + ? deviceProfileUpdateMsg.getDefaultQueueName() : null; + deviceProfile.setDefaultQueueName(defaultQueueName); + + UUID firmwareUUID = safeGetUUID(deviceProfileUpdateMsg.getFirmwareIdMSB(), deviceProfileUpdateMsg.getFirmwareIdLSB()); + deviceProfile.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); + + UUID softwareUUID = safeGetUUID(deviceProfileUpdateMsg.getSoftwareIdMSB(), deviceProfileUpdateMsg.getSoftwareIdLSB()); + deviceProfile.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); + + + deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); + if (created) { + deviceProfile.setId(deviceProfileId); + } + deviceProfileService.saveDeviceProfile(deviceProfile, false); + } finally { + deviceCreationLock.unlock(); + } + return created; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index d083c5b16f..dfa5705840 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -35,12 +35,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; @@ -114,15 +110,6 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } } - private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { - EntityRelation relation = new EntityRelation(); - relation.setFrom(edgeId); - relation.setTo(entityId); - relation.setTypeGroup(RelationTypeGroup.COMMON); - relation.setType(EntityRelation.EDGE_TYPE); - relationService.saveRelation(tenantId, relation); - } - private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) { try { Device device = deviceService.findDeviceById(tenantId, deviceId); @@ -137,7 +124,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { @Override public void onFailure(Throwable t) { - log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); + log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); } }); } catch (JsonProcessingException | IllegalArgumentException e) { @@ -145,21 +132,6 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } } - private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { - TbMsgMetaData metaData = getTbMsgMetaData(edge); - if (customerId != null && !customerId.isNullUid()) { - metaData.putValue("customerId", customerId.toString()); - } - return metaData; - } - - private TbMsgMetaData getTbMsgMetaData(Edge edge) { - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("edgeId", edge.getId().toString()); - metaData.putValue("edgeName", edge.getName()); - return metaData; - } - public ListenableFuture processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) { log.trace("[{}] processDeviceRpcCallFromEdge [{}]", tenantId, deviceRpcCallMsg); if (deviceRpcCallMsg.hasResponseMsg()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java index c888ec2925..edaffd6f74 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java @@ -15,22 +15,92 @@ */ package org.thingsboard.server.service.edge.rpc.processor.device; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class DeviceProfileEdgeProcessor extends BaseEdgeProcessor { +public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { + + + public ListenableFuture processDeviceProfileMsgFromEdge(TenantId tenantId, Edge edge, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { + log.trace("[{}] executing processDeviceProfileMsgFromEdge [{}] from edge [{}]", tenantId, deviceProfileUpdateMsg, edge.getName()); + DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (deviceProfileUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(deviceProfileUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + log.warn("Failed to process DeviceProfileUpdateMsg from Edge [{}]", deviceProfileUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, Edge edge) { + boolean created = super.saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), deviceProfileId); + pushDeviceProfileCreatedEventToRuleEngine(tenantId, edge, deviceProfileId); + } + } + + private void pushDeviceProfileCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceProfileId deviceProfileId) { + try { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(deviceProfile); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceProfileId, getTbMsgMetaData(edge), + TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceProfileId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", deviceProfile); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", deviceProfile, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device profile action to rule engine: {}", deviceProfileId, DataConstants.ENTITY_CREATED, e); + } + } public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java new file mode 100644 index 0000000000..509b2e5196 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.entityview; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.EdgeEntityType; +import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; + +@Slf4j +public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { + + protected Pair saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, CustomerId customerId) { + boolean created = false; + boolean entityViewNameUpdated = false; + EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); + String entityViewName = entityViewUpdateMsg.getName(); + if (entityView == null) { + created = true; + entityView = new EntityView(); + entityView.setTenantId(tenantId); + entityView.setCreatedTime(Uuids.unixTimestamp(entityViewId.getId())); + } + EntityView entityViewByName = entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName); + if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) { + entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15); + log.warn("Entity view with name {} already exists. Renaming entity view name to {}", + entityViewUpdateMsg.getName(), entityViewName); + entityViewNameUpdated = true; + } + entityView.setName(entityViewName); + entityView.setType(entityViewUpdateMsg.getType()); + entityView.setCustomerId(customerId); + entityView.setAdditionalInfo(entityViewUpdateMsg.hasAdditionalInfo() ? + JacksonUtil.toJsonNode(entityViewUpdateMsg.getAdditionalInfo()) : null); + + UUID entityIdUUID = safeGetUUID(entityViewUpdateMsg.getEntityIdMSB(), entityViewUpdateMsg.getEntityIdLSB()); + if (EdgeEntityType.DEVICE.equals(entityViewUpdateMsg.getEntityType())) { + entityView.setEntityId(entityIdUUID != null ? new DeviceId(entityIdUUID) : null); + } else if (EdgeEntityType.ASSET.equals(entityViewUpdateMsg.getEntityType())) { + entityView.setEntityId(entityIdUUID != null ? new AssetId(entityIdUUID) : null); + } + + entityViewValidator.validate(entityView, EntityView::getTenantId); + if (created) { + entityView.setId(entityViewId); + } + entityViewService.saveEntityView(entityView, false); + return Pair.of(created, entityViewNameUpdated); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index 0964a434ba..e78b6b4ff5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -15,22 +15,111 @@ */ package org.thingsboard.server.service.edge.rpc.processor.entityview; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Component @Slf4j @TbCoreComponent -public class EntityViewEdgeProcessor extends BaseEdgeProcessor { +public class EntityViewEdgeProcessor extends BaseEntityViewProcessor { + + public ListenableFuture processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg) { + log.trace("[{}] executing processEntityViewMsgFromEdge [{}] from edge [{}]", tenantId, entityViewUpdateMsg, edge.getName()); + EntityViewId entityViewId = new EntityViewId(new UUID(entityViewUpdateMsg.getIdMSB(), entityViewUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (entityViewUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + EntityView entityViewToDelete = entityViewService.findEntityViewById(tenantId, entityViewId); + if (entityViewToDelete != null) { + entityViewService.unassignEntityViewFromEdge(tenantId, entityViewId, edge.getId()); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed entity views violated {}", tenantId, entityViewUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, Edge edge) { + CustomerId customerId = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB()); + Pair resultPair = super.saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, customerId); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), entityViewId); + pushAssetCreatedEventToRuleEngine(tenantId, edge, entityViewId); + entityViewService.assignEntityViewToEdge(tenantId, entityViewId, edge.getId()); + } + Boolean assetNameUpdated = resultPair.getSecond(); + if (assetNameUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ENTITY_VIEW, EdgeEventActionType.UPDATED, entityViewId, null); + } + } + + private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, EntityViewId entityViewId) { + try { + EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(entityView); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, entityViewId, entityView.getCustomerId(), + getActionTbMsgMetaData(edge, entityView.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, entityViewId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", entityView); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", entityView, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push entity view action to rule engine: {}", entityViewId, DataConstants.ENTITY_CREATED, e); + } + } public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index e776c040c0..2dc246d9a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 34515f827d..2e2b3e96be 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -756,6 +756,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); } catch (Exception e) { } + try { + conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"); + } catch (Exception e) { + } conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;"); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index eb5818db59..16d4c44d63 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; @@ -243,7 +244,17 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception { if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { throw new AlreadySentException(); + } else { + ctx.getStats().reportProcessed(deliveryMethod, recipient.getId()); } + + if (recipient instanceof User) { + UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), ((User) recipient).getId(), false); + if (!settings.isEnabled(ctx.getNotificationType(), deliveryMethod)) { + throw new RuntimeException("User disabled " + deliveryMethod.getName() + " notifications of this type"); + } + } + NotificationChannel notificationChannel = channels.get(deliveryMethod); DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); @@ -257,7 +268,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple Notification notification = Notification.builder() .requestId(request.getId()) .recipientId(recipient.getId()) - .type(ctx.getNotificationTemplate().getNotificationType()) + .type(ctx.getNotificationType()) .subject(processedTemplate.getSubject()) .text(processedTemplate.getBody()) .additionalConfig(processedTemplate.getAdditionalConfig()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java index 9782fb473f..e91a3e80fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; @@ -48,6 +49,8 @@ public class NotificationProcessingContext { private final Set deliveryMethods; @Getter private final NotificationTemplate notificationTemplate; + @Getter + private final NotificationType notificationType; private final Map templates; @Getter @@ -61,6 +64,7 @@ public class NotificationProcessingContext { this.deliveryMethods = deliveryMethods; this.settings = settings; this.notificationTemplate = template; + this.notificationType = template.getNotificationType(); this.templates = new EnumMap<>(NotificationDeliveryMethod.class); this.stats = new NotificationRequestStats(); init(); diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 8b09565ef9..cdc5afe361 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -228,7 +228,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService= deviceState.getLastActivityTime()) { + deviceState.setLastInactivityAlarmTime(0L); + save(deviceId, INACTIVITY_ALARM_TIME, 0L); + } } } } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java index e8639e0db7..faf0896b81 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java @@ -15,19 +15,29 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -154,5 +164,115 @@ public class AssetEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); } + @Test + public void testSendAssetToCloud() throws Exception { + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); + assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetUpdateMsgBuilder.setName("Asset Edge 2"); + assetUpdateMsgBuilder.setType("test"); + assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); + uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + Asset asset = doGet("/api/asset/" + uuid, Asset.class); + Assert.assertNotNull(asset); + Assert.assertEquals("Asset Edge 2", asset.getName()); + } + + @Test + public void testSendAssetToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + String assetOnCloudName = StringUtils.randomAlphanumeric(15); + Asset assetOnCloud = saveAsset(assetOnCloudName); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); + assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetUpdateMsgBuilder.setName(assetOnCloudName); + assetUpdateMsgBuilder.setType("test"); + assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); + uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(assetUpdateMsgOpt.isPresent()); + AssetUpdateMsg latestAssetUpdateMsg = assetUpdateMsgOpt.get(); + Assert.assertNotEquals(assetOnCloudName, latestAssetUpdateMsg.getName()); + + UUID newAssetId = new UUID(latestAssetUpdateMsg.getIdMSB(), latestAssetUpdateMsg.getIdLSB()); + + Assert.assertNotEquals(assetOnCloud.getUuidId(), newAssetId); + + Asset asset = doGet("/api/asset/" + newAssetId, Asset.class); + Assert.assertNotNull(asset); + Assert.assertNotEquals(assetOnCloudName, asset.getName()); + } + + @Test + public void testSendDeleteAssetOnEdgeToCloud() throws Exception { + Asset savedAsset = saveAssetOnCloudAndVerifyDeliveryToEdge(); + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + AssetUpdateMsg.Builder assetDeleteMsgBuilder = AssetUpdateMsg.newBuilder(); + assetDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + assetDeleteMsgBuilder.setIdMSB(savedAsset.getUuidId().getMostSignificantBits()); + assetDeleteMsgBuilder.setIdLSB(savedAsset.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(assetDeleteMsgBuilder); + + upLinkMsgBuilder.addAssetUpdateMsg(assetDeleteMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + AssetInfo assetInfo = doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class); + Assert.assertNotNull(assetInfo); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/assets?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeAssets.contains(assetInfo)); + } + + private Asset saveAssetOnCloudAndVerifyDeliveryToEdge() throws Exception { + // create asset and assign to edge + Asset savedAsset = saveAsset(StringUtils.randomAlphanumeric(15)); + edgeImitator.expectMessageAmount(1); // asset message + doPost("/api/edge/" + edge.getUuidId() + + "/asset/" + savedAsset.getUuidId(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(assetUpdateMsgOpt.isPresent()); + AssetUpdateMsg assetUpdateMsg = assetUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(savedAsset.getUuidId().getMostSignificantBits(), assetUpdateMsg.getIdMSB()); + Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB()); + return savedAsset; + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java index fd36c11db8..f77975f0a9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java @@ -15,17 +15,22 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import java.nio.charset.StandardCharsets; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -77,4 +82,56 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { unAssignFromEdgeAndDeleteRuleChain(buildingsRuleChainId); } + + @Test + public void testSendAssetProfileToCloud() throws Exception { + RuleChainId ruleChainId = createEdgeRuleChainAndAssignToEdge("Asset Profile Rule Chain"); + DashboardId dashboardId = createDashboardAndAssignToEdge("Asset Profile Dashboard"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AssetProfileUpdateMsg.Builder assetProfileUpdateMsgBuilder = AssetProfileUpdateMsg.newBuilder(); + assetProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setName("Asset Profile On Edge"); + assetProfileUpdateMsgBuilder.setDefault(false); + assetProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits()); + assetProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits()); + assetProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetProfileUpdateMsgBuilder); + uplinkMsgBuilder.addAssetProfileUpdateMsg(assetProfileUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + AssetProfile assetProfile = doGet("/api/assetProfile/" + uuid, AssetProfile.class); + Assert.assertNotNull(assetProfile); + Assert.assertEquals("Asset Profile On Edge", assetProfile.getName()); + + // delete profile + edgeImitator.expectMessageAmount(1); + doDelete("/api/assetProfile/" + assetProfile.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg); + AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); + Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); + Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); + + // cleanup + unAssignFromEdgeAndDeleteDashboard(dashboardId); + unAssignFromEdgeAndDeleteRuleChain(ruleChainId); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 94743c4a6b..fd7dfbaafa 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import org.junit.Assert; @@ -22,13 +23,22 @@ import org.junit.Test; 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.DashboardInfo; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -149,4 +159,74 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB()); } + @Test + public void testSendDashboardToCloud() throws Exception { + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); + dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + dashboardUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + dashboardUpdateMsgBuilder.setTitle("Edge Test Dashboard"); + dashboardUpdateMsgBuilder.setConfiguration(""); + dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(dashboardUpdateMsgBuilder); + uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + Dashboard dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); + Assert.assertNotNull(dashboard); + Assert.assertEquals("Edge Test Dashboard", dashboard.getName()); + } + + @Test + public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception { + Dashboard savedDashboard = saveDashboardOnCloudAndVerifyDeliveryToEdge(); + + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + DashboardUpdateMsg.Builder dashboardDeleteMsgBuilder = DashboardUpdateMsg.newBuilder(); + dashboardDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + dashboardDeleteMsgBuilder.setIdMSB(savedDashboard.getUuidId().getMostSignificantBits()); + dashboardDeleteMsgBuilder.setIdLSB(savedDashboard.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(dashboardDeleteMsgBuilder); + + upLinkMsgBuilder.addDashboardUpdateMsg(dashboardDeleteMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + DashboardInfo dashboardInfo = doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class); + Assert.assertNotNull(dashboardInfo); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/dashboards?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeAssets.contains(dashboardInfo)); + } + + private Dashboard saveDashboardOnCloudAndVerifyDeliveryToEdge() throws Exception { + // create dashboard and assign to edge + Dashboard dashboard = new Dashboard(); + dashboard.setTitle(StringUtils.randomAlphanumeric(15)); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + edgeImitator.expectMessageAmount(1); // dashboard message + doPost("/api/edge/" + edge.getUuidId() + + "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class); + Assert.assertTrue(dashboardUpdateMsgOpt.isPresent()); + DashboardUpdateMsg entityViewUpdateMsg = dashboardUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); + return savedDashboard; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index 9d4cc0ca15..4e8664aa97 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -15,19 +15,26 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; @@ -46,12 +53,15 @@ import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQu import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -260,6 +270,67 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { removeDeviceProfileAndDoBasicAssert(deviceProfile); } + @Test + public void testSendDeviceProfileToCloud() throws Exception { + RuleChainId ruleChainId = createEdgeRuleChainAndAssignToEdge("Device Profile Rule Chain"); + DashboardId dashboardId = createDashboardAndAssignToEdge("Device Profile Dashboard"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceProfileUpdateMsg.Builder deviceProfileUpdateMsgBuilder = DeviceProfileUpdateMsg.newBuilder(); + deviceProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setName("Device Profile On Edge"); + deviceProfileUpdateMsgBuilder.setDefault(false); + deviceProfileUpdateMsgBuilder.setType(DeviceProfileType.DEFAULT.name()); + deviceProfileUpdateMsgBuilder.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(createProfileData()))); + deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits()); + deviceProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits()); + deviceProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(deviceProfileUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceProfileUpdateMsg(deviceProfileUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + AssetProfile assetProfile = doGet("/api/deviceProfile/" + uuid, AssetProfile.class); + Assert.assertNotNull(assetProfile); + Assert.assertEquals("Device Profile On Edge", assetProfile.getName()); + + // delete profile + edgeImitator.expectMessageAmount(1); + doDelete("/api/deviceProfile/" + assetProfile.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); + Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + // cleanup + unAssignFromEdgeAndDeleteDashboard(dashboardId); + unAssignFromEdgeAndDeleteRuleChain(ruleChainId); + } + + private DeviceProfileData createProfileData() { + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration()); + deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration("Device Secret")); + return deviceProfileData; + } private DeviceProfile createDeviceProfileAndDoBasicAssert(String deviceProfileName, DeviceProfileTransportConfiguration deviceProfileTransportConfiguration) throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile(deviceProfileName, deviceProfileTransportConfiguration); diff --git a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java index 75386dbb17..470fd8e576 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.edge; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Assert; @@ -22,15 +24,24 @@ import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.EdgeEntityType; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -43,11 +54,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { // create entity view and assign to edge edgeImitator.expectMessageAmount(1); Device device = findDeviceByName("Edge Device 1"); - EntityView entityView = new EntityView(); - entityView.setName("Edge EntityView 1"); - entityView.setType("test"); - entityView.setEntityId(device.getId()); - EntityView savedEntityView = doPost("/api/entityView", entityView, EntityView.class); + EntityView savedEntityView = saveEntityView("Edge EntityView 1", device.getId()); doPost("/api/edge/" + edge.getUuidId() + "/entityView/" + savedEntityView.getUuidId(), EntityView.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -102,11 +109,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { // create entity view #2 and assign to edge edgeImitator.expectMessageAmount(1); - entityView = new EntityView(); - entityView.setName("Edge EntityView 2"); - entityView.setType("test"); - entityView.setEntityId(device.getId()); - savedEntityView = doPost("/api/entityView", entityView, EntityView.class); + savedEntityView = saveEntityView("Edge EntityView 2", device.getId()); doPost("/api/edge/" + edge.getUuidId() + "/entityView/" + savedEntityView.getUuidId(), EntityView.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -158,6 +161,115 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { } + @Test + public void testSendEntityViewToCloud() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder(); + entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setName("Edge EntityView 2"); + entityViewUpdateMsgBuilder.setType("test"); + entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE); + entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder); + uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + EntityView entityView = doGet("/api/entityView/" + uuid, EntityView.class); + Assert.assertNotNull(entityView); + Assert.assertEquals("Edge EntityView 2", entityView.getName()); + } + + @Test + public void testSendEntityViewToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + String entityViewOnCloudName = StringUtils.randomAlphanumeric(15); + EntityView entityViewOnCloud = saveEntityView(entityViewOnCloudName, device.getId()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder(); + entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setName(entityViewOnCloudName); + entityViewUpdateMsgBuilder.setType("test"); + entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE); + entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder); + uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class); + Assert.assertTrue(entityViewUpdateMsgOpt.isPresent()); + EntityViewUpdateMsg latestEntityViewUpdateMsg = entityViewUpdateMsgOpt.get(); + Assert.assertNotEquals(entityViewOnCloudName, latestEntityViewUpdateMsg.getName()); + + UUID newEntityViewId = new UUID(latestEntityViewUpdateMsg.getIdMSB(), latestEntityViewUpdateMsg.getIdLSB()); + + Assert.assertNotEquals(entityViewOnCloud.getId().getId(), newEntityViewId); + + EntityView entityView = doGet("/api/entityView/" + newEntityViewId, EntityView.class); + Assert.assertNotNull(entityView); + Assert.assertNotEquals(entityViewOnCloudName, entityView.getName()); + } + + @Test + public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + EntityView savedEntityView = saveEntityViewOnCloudAndVerifyDeliveryToEdge(device); + + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + EntityViewUpdateMsg.Builder entityViewDeleteMsgBuilder = EntityViewUpdateMsg.newBuilder(); + entityViewDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + entityViewDeleteMsgBuilder.setIdMSB(savedEntityView.getUuidId().getMostSignificantBits()); + entityViewDeleteMsgBuilder.setIdLSB(savedEntityView.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(entityViewDeleteMsgBuilder); + + upLinkMsgBuilder.addEntityViewUpdateMsg(entityViewDeleteMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + EntityViewInfo entityViewInfo = doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class); + Assert.assertNotNull(entityViewInfo); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/entityViews?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeAssets.contains(entityViewInfo)); + } + private void verifyEntityViewUpdateMsg(EntityView entityView, Device device) throws InvalidProtocolBufferException { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); @@ -173,6 +285,28 @@ public class EntityViewEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(entityViewUpdateMsg); } + private EntityView saveEntityViewOnCloudAndVerifyDeliveryToEdge(Device device) throws Exception { + // create entity view and assign to edge + EntityView savedEntityView = saveEntityView(StringUtils.randomAlphanumeric(15), device.getId()); + edgeImitator.expectMessageAmount(1); // entity view message + doPost("/api/edge/" + edge.getUuidId() + + "/entityView/" + savedEntityView.getUuidId(), EntityView.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class); + Assert.assertTrue(entityViewUpdateMsgOpt.isPresent()); + EntityViewUpdateMsg entityViewUpdateMsg = entityViewUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(savedEntityView.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB()); + Assert.assertEquals(savedEntityView.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB()); + return savedEntityView; + } + private EntityView saveEntityView(String name, DeviceId deviceId) { + EntityView entityView = new EntityView(); + entityView.setName(name); + entityView.setType("test"); + entityView.setEntityId(deviceId); + return doPost("/api/entityView", entityView, EntityView.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 14db3d7d5d..1a7e9e1420 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.notification; +import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.assertj.core.data.Offset; import org.java_websocket.client.WebSocketClient; @@ -27,6 +28,7 @@ import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; @@ -40,6 +42,7 @@ import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; @@ -484,6 +487,66 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); } + @Test + public void testUserNotificationSettings() throws Exception { + var entityActionNotificationPref = new UserNotificationSettings.NotificationPref(); + entityActionNotificationPref.setEnabled(true); + entityActionNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var entitiesLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + entitiesLimitNotificationPref.setEnabled(true); + entitiesLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.SMS, true, + NotificationDeliveryMethod.WEB, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var apiUsageLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + apiUsageLimitNotificationPref.setEnabled(false); + apiUsageLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + UserNotificationSettings settings = new UserNotificationSettings(Map.of( + NotificationType.ENTITY_ACTION, entityActionNotificationPref, + NotificationType.ENTITIES_LIMIT, entitiesLimitNotificationPref, + NotificationType.API_USAGE_LIMIT, apiUsageLimitNotificationPref + )); + doPost("/api/notification/settings/user", settings, UserNotificationSettings.class); + + var entityActionNotificationTemplate = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Entity action", "Entity action", NotificationDeliveryMethod.WEB); + var entitiesLimitNotificationTemplate = createNotificationTemplate(NotificationType.ENTITIES_LIMIT, "Entities limit", "Entities limit", NotificationDeliveryMethod.WEB); + var apiUsageLimitNotificationTemplate = createNotificationTemplate(NotificationType.API_USAGE_LIMIT, "API usage limit", "API usage limit", NotificationDeliveryMethod.WEB); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(tenantId) + .templateId(entityActionNotificationTemplate.getId()) + .originatorEntityId(tenantAdminUserId) + .targets(List.of(target.getUuidId())) + .ruleId(new NotificationRuleId(UUID.randomUUID())) // to trigger user settings check + .build(); + NotificationRequestStats stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getErrors()).isEmpty(); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB).get()).isOne(); + + notificationRequest.setTemplateId(entitiesLimitNotificationTemplate.getId()); + stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled"); + + notificationRequest.setTemplateId(apiUsageLimitNotificationTemplate.getId()); + stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled"); + } + @Test public void testSlackNotifications() throws Exception { NotificationSettings settings = new NotificationSettings(); @@ -603,6 +666,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams); } + private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception { + SettableFuture future = SettableFuture.create(); + notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set); + return future.get(30, TimeUnit.SECONDS); + } + private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications); assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java similarity index 60% rename from application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java rename to application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java index a49f8dc8cb..9b4ced8567 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java @@ -19,14 +19,20 @@ import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserSettingsService; @Service @Primary -public class MockNotificationSettingsService extends DefaultNotificationSettingsService { +public class TestNotificationSettingsService extends DefaultNotificationSettingsService { - public MockNotificationSettingsService(AdminSettingsService adminSettingsService) { - super(adminSettingsService, null, null, null); + public TestNotificationSettingsService(AdminSettingsService adminSettingsService, + NotificationTargetService notificationTargetService, + NotificationTemplateService notificationTemplateService, + UserSettingsService userSettingsService) { + super(adminSettingsService, notificationTargetService, notificationTemplateService, null, userSettingsService); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 52f2ec5c9d..ca30f9e5ed 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -22,19 +22,30 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.sql.query.EntityQueryRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -62,14 +73,32 @@ public class DefaultDeviceStateServiceTest { PartitionService partitionService; @Mock DeviceStateData deviceStateDataMock; + @Mock + EntityQueryRepository entityQueryRepository; + TenantId tenantId = new TenantId(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"); + TopicPartitionInfo tpi; DefaultDeviceStateService service; + TelemetrySubscriptionService telemetrySubscriptionService; + @Before public void setUp() { - service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null, mock(NotificationRuleProcessor.class))); + service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, null, mock(NotificationRuleProcessor.class))); + telemetrySubscriptionService = Mockito.mock(TelemetrySubscriptionService.class); + ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService); + ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60); + ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60); + ReflectionTestUtils.setField(service, "initFetchPackSize", 10); + + tpi = TopicPartitionInfo.builder().myPartition(true).build(); + Mockito.when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi); + Mockito.when(entityQueryRepository.findEntityDataByQueryInternal(Mockito.any())).thenReturn(new PageData<>()); + var deviceIdInfo = new DeviceIdInfo(tenantId.getId(), null, deviceId.getId()); + Mockito.when(deviceService.findDeviceIdInfos(Mockito.any())) + .thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false)); } @Test @@ -125,4 +154,188 @@ public class DefaultDeviceStateServiceTest { Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout()); } + private void initStateService(long timeout) throws InterruptedException { + service.stop(); + Mockito.reset(service, telemetrySubscriptionService); + ReflectionTestUtils.setField(service, "defaultInactivityTimeoutMs", timeout); + service.init(); + PartitionChangeEvent event = new PartitionChangeEvent(this, new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi)); + service.onApplicationEvent(event); + Thread.sleep(100); + } + + @Test + public void increaseInactivityForInactiveDeviceTest() throws Exception { + final long defaultTimeout = 1; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + long increase = 100; + long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase; + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + activityVerify(true); + Thread.sleep(increase); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(newTimeout + 5); + service.checkStates(); + activityVerify(false); + } + + @Test + public void increaseInactivityForActiveDeviceTest() throws Exception { + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + + Mockito.reset(telemetrySubscriptionService); + + long increase = 100; + long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase; + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + Thread.sleep(defaultTimeout + increase); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(newTimeout); + service.checkStates(); + activityVerify(false); + } + + @Test + public void increaseSmallInactivityForInactiveDeviceTest() throws Exception { + final long defaultTimeout = 1; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + + Mockito.reset(telemetrySubscriptionService); + + long newTimeout = 1; + Thread.sleep(newTimeout); + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + } + + @Test + public void decreaseInactivityForActiveDeviceTest() throws Exception { + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + + Mockito.reset(telemetrySubscriptionService); + + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + + long newTimeout = 1; + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + activityVerify(false); + Mockito.reset(telemetrySubscriptionService); + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, defaultTimeout); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + } + + @Test + public void decreaseInactivityForInactiveDeviceTest() throws Exception { + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + DeviceState deviceState = DeviceState.builder().build(); + DeviceStateData deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(new TbMsgMetaData()) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); + activityVerify(true); + Thread.sleep(defaultTimeout); + service.checkStates(); + activityVerify(false); + Mockito.reset(telemetrySubscriptionService); + + long newTimeout = 1; + + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); + Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); + } + + private void activityVerify(boolean isActive) { + Mockito.verify(telemetrySubscriptionService, Mockito.times(1)).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.eq(isActive), Mockito.any()); + } + } \ No newline at end of file diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 99055e0e5f..d96af8a910 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -68,3 +68,5 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 + +server.log_controller_error_stack_trace=false diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index f89153feee..1ec6fb3fb5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -31,6 +31,8 @@ public interface AssetProfileService extends EntityDaoService { AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId); + AssetProfile saveAssetProfile(AssetProfile assetProfile, boolean doValidate); + AssetProfile saveAssetProfile(AssetProfile assetProfile); void deleteAssetProfile(TenantId tenantId, AssetProfileId assetProfileId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index 8b825e0837..722eeff365 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -41,6 +41,8 @@ public interface AssetService extends EntityDaoService { Asset findAssetByTenantIdAndName(TenantId tenantId, String name); + Asset saveAsset(Asset asset, boolean doValidate); + Asset saveAsset(Asset asset); Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index c9974bba69..434088180c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -40,6 +40,8 @@ public interface DashboardService extends EntityDaoService { ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId); + Dashboard saveDashboard(Dashboard dashboard, boolean doValidate); + Dashboard saveDashboard(Dashboard dashboard); Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index e765cc5030..f94b709c97 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -31,6 +31,8 @@ public interface DeviceProfileService extends EntityDaoService { DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId); + DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate); + DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile); void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index 31b2004a38..ea9dc31b45 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -38,6 +38,8 @@ public interface EntityViewService extends EntityDaoService { EntityView saveEntityView(EntityView entityView); + EntityView saveEntityView(EntityView entityView, boolean doValidate); + EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId); EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java index a5433915b3..9fca174b3a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java @@ -16,7 +16,9 @@ package org.thingsboard.server.dao.notification; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; public interface NotificationSettingsService { @@ -24,6 +26,10 @@ public interface NotificationSettingsService { NotificationSettings findNotificationSettings(TenantId tenantId); + UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings); + + UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format); + void createDefaultNotificationConfigs(TenantId tenantId); void updateDefaultNotificationConfigs(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index 206b203682..23149c79c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -16,11 +16,10 @@ package org.thingsboard.server.common.data.msg; import lombok.Getter; +import org.thingsboard.server.common.data.StringUtils; -import java.util.Arrays; import java.util.EnumSet; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; public enum TbMsgType { @@ -39,10 +38,10 @@ public enum TbMsgType { ENTITY_UNASSIGNED("Entity Unassigned"), ATTRIBUTES_UPDATED("Attributes Updated"), ATTRIBUTES_DELETED("Attributes Deleted"), - ALARM(null), + ALARM, ALARM_ACK("Alarm Acknowledged"), ALARM_CLEAR("Alarm Cleared"), - ALARM_DELETE(null), + ALARM_DELETE, ALARM_ASSIGNED("Alarm Assigned"), ALARM_UNASSIGNED("Alarm Unassigned"), COMMENT_CREATED("Comment Created"), @@ -50,8 +49,8 @@ public enum TbMsgType { RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"), ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"), ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"), - ENTITY_ASSIGNED_TO_EDGE(null), - ENTITY_UNASSIGNED_FROM_EDGE(null), + ENTITY_ASSIGNED_TO_EDGE, + ENTITY_UNASSIGNED_FROM_EDGE, TIMESERIES_UPDATED("Timeseries Updated"), TIMESERIES_DELETED("Timeseries Deleted"), RPC_QUEUED("RPC Queued"), @@ -65,9 +64,9 @@ public enum TbMsgType { RELATION_ADD_OR_UPDATE("Relation Added or Updated"), RELATION_DELETED("Relation Deleted"), RELATIONS_DELETED("All Relations Deleted"), - PROVISION_SUCCESS(null), - PROVISION_FAILURE(null), - SEND_EMAIL(null), + PROVISION_SUCCESS, + PROVISION_FAILURE, + SEND_EMAIL, // tellSelfOnly types GENERATOR_NODE_SELF_MSG(null, true), @@ -76,12 +75,15 @@ public enum TbMsgType { DEVICE_UPDATE_SELF_MSG(null, true), DEDUPLICATION_TIMEOUT_SELF_MSG(null, true), DELAY_TIMEOUT_SELF_MSG(null, true), - MSG_COUNT_SELF_MSG(null, true); + MSG_COUNT_SELF_MSG(null, true), + + // Custom or N/A type: + NA; public static final List NODE_CONNECTIONS = EnumSet.allOf(TbMsgType.class).stream() .filter(tbMsgType -> !tbMsgType.isTellSelfOnly()) .map(TbMsgType::getRuleNodeConnection) - .filter(Objects::nonNull) + .filter(connection -> !TbNodeConnectionType.OTHER.equals(connection)) .collect(Collectors.toUnmodifiableList()); @Getter @@ -91,25 +93,16 @@ public enum TbMsgType { private final boolean tellSelfOnly; TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) { - this.ruleNodeConnection = ruleNodeConnection; + this.ruleNodeConnection = StringUtils.isNotEmpty(ruleNodeConnection) ? ruleNodeConnection : TbNodeConnectionType.OTHER; this.tellSelfOnly = tellSelfOnly; } TbMsgType(String ruleNodeConnection) { - this.ruleNodeConnection = ruleNodeConnection; - this.tellSelfOnly = false; + this(ruleNodeConnection, false); } - public static String getRuleNodeConnectionOrElseOther(String msgType) { - if (msgType == null) { - return TbNodeConnectionType.OTHER; - } else { - return Arrays.stream(TbMsgType.values()) - .filter(type -> type.name().equals(msgType)) - .findFirst() - .map(TbMsgType::getRuleNodeConnection) - .orElse(TbNodeConnectionType.OTHER); - } + TbMsgType() { + this(null, false); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java index 619e1ad38f..691ecf8bc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java @@ -54,7 +54,6 @@ public class NotificationRequestStats { public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet(); - processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId()); } public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) { @@ -68,6 +67,10 @@ public class NotificationRequestStats { errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage); } + public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) { + processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipientId); + } + public boolean contains(NotificationDeliveryMethod deliveryMethod, Object recipientId) { Set processedRecipients = this.processedRecipients.get(deliveryMethod); return processedRecipients != null && processedRecipients.contains(recipientId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java new file mode 100644 index 0000000000..0ad7cc6d78 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.settings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Data +public class UserNotificationSettings { + + @NotNull + @Valid + private final Map prefs; + + public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap()); + + public static final Set deliveryMethods = NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods(); + + @JsonCreator + public UserNotificationSettings(@JsonProperty("prefs") Map prefs) { + this.prefs = prefs; + } + + public boolean isEnabled(NotificationType notificationType, NotificationDeliveryMethod deliveryMethod) { + NotificationPref pref = prefs.get(notificationType); + if (pref == null) { + return true; + } + if (!pref.isEnabled()) { + return false; + } + return pref.getEnabledDeliveryMethods().getOrDefault(deliveryMethod, true); + } + + @Data + public static class NotificationPref { + private boolean enabled; + @NotNull + private Map enabledDeliveryMethods; + + public static NotificationPref createDefault() { + NotificationPref pref = new NotificationPref(); + pref.setEnabled(true); + pref.setEnabledDeliveryMethods(deliveryMethods.stream().collect(Collectors.toMap(v -> v, v -> true))); + return pref; + } + + @JsonIgnore + @AssertTrue(message = "Only email, Web and SMS delivery methods are allowed") + public boolean isValid() { + return enabledDeliveryMethods.entrySet().stream() + .allMatch(entry -> deliveryMethods.contains(entry.getKey()) && entry.getValue() != null); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java index 5c540995aa..62a6377519 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java @@ -36,4 +36,6 @@ public class SortOrder { ASC, DESC } + public static final SortOrder BY_CREATED_TIME_DESC = new SortOrder("createdTime", Direction.DESC); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java index b19dbbbaee..cd627821ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java @@ -19,7 +19,7 @@ import lombok.Getter; public enum UserSettingsType { - GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED; + GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED, NOTIFICATIONS; @Getter private final boolean reserved; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index a37eb31d72..870d5a2804 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -22,6 +22,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; +import static org.thingsboard.server.common.data.msg.TbMsgType.NA; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; @@ -51,37 +52,36 @@ class TbMsgTypeTest { DEVICE_UPDATE_SELF_MSG, DEDUPLICATION_TIMEOUT_SELF_MSG, DELAY_TIMEOUT_SELF_MSG, - MSG_COUNT_SELF_MSG + MSG_COUNT_SELF_MSG, + NA ); // backward-compatibility tests - + @Test void getRuleNodeConnectionsTest() { var tbMsgTypes = TbMsgType.values(); for (var type : tbMsgTypes) { if (typesWithNullRuleNodeConnection.contains(type)) { - assertThat(type.getRuleNodeConnection()).isNull(); + assertThat(type.getRuleNodeConnection()).isEqualTo(TbNodeConnectionType.OTHER); } else { - assertThat(type.getRuleNodeConnection()).isNotNull(); + assertThat(type.getRuleNodeConnection()).isNotEqualTo(TbNodeConnectionType.OTHER); } } } @Test void getRuleNodeConnectionOrElseOtherTest() { - assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(null)) - .isEqualTo(TbNodeConnectionType.OTHER); var tbMsgTypes = TbMsgType.values(); for (var type : tbMsgTypes) { if (typesWithNullRuleNodeConnection.contains(type)) { - assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())) + assertThat(type.getRuleNodeConnection()) .isEqualTo(TbNodeConnectionType.OTHER); } else { - assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())).isNotNull() + assertThat(type.getRuleNodeConnection()).isNotNull() .isNotEqualTo(TbNodeConnectionType.OTHER); } } } - + } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 4e8548aa33..dcd543a9a6 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -221,17 +221,11 @@ public class EdgeGrpcClient implements EdgeRpcClient { } @Override - public void sendSyncRequestMsg(boolean syncRequired) { - sendSyncRequestMsg(syncRequired, true); - } - - @Override - public void sendSyncRequestMsg(boolean syncRequired, boolean fullSync) { + public void sendSyncRequestMsg(boolean fullSyncRequired) { uplinkMsgLock.lock(); try { SyncRequestMsg syncRequestMsg = SyncRequestMsg.newBuilder() - .setSyncRequired(syncRequired) - .setFullSync(fullSync) + .setFullSync(fullSyncRequired) .build(); this.inputStream.onNext(RequestMsg.newBuilder() .setMsgType(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE) diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java index 44d00e22a8..9f2a303f5b 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -34,9 +34,7 @@ public interface EdgeRpcClient { void disconnect(boolean onError) throws InterruptedException; - void sendSyncRequestMsg(boolean syncRequired); - - void sendSyncRequestMsg(boolean syncRequired, boolean fullSync); + void sendSyncRequestMsg(boolean fullSyncRequired); void sendUplinkMsg(UplinkMsg uplinkMsg); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index afcf4056c4..94418566c2 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -85,7 +85,7 @@ message ConnectResponseMsg { } message SyncRequestMsg { - bool syncRequired = 1; + bool syncRequired = 1; // deprecated optional bool fullSync = 2; } @@ -529,6 +529,11 @@ message UplinkMsg { repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13; // deprecated repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14; repeated EntityViewsRequestMsg entityViewsRequestMsg = 15; + repeated AssetUpdateMsg assetUpdateMsg = 16; + repeated DashboardUpdateMsg dashboardUpdateMsg = 17; + repeated EntityViewUpdateMsg entityViewUpdateMsg = 18; + repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 19; + repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20; } message UplinkResponseMsg { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index a987a4a253..63c1e27385 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -52,6 +52,7 @@ public final class TbMsg implements Serializable { private final UUID id; private final long ts; private final String type; + private final TbMsgType internalType; private final EntityId originator; private final CustomerId customerId; private final TbMsgMetaData metaData; @@ -97,7 +98,7 @@ public final class TbMsg implements Serializable { */ @Deprecated(since = "3.5.2") public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } @@ -108,7 +109,7 @@ public final class TbMsg implements Serializable { @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } @@ -117,7 +118,7 @@ public final class TbMsg implements Serializable { } public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } @@ -126,7 +127,7 @@ public final class TbMsg implements Serializable { } public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } @@ -170,13 +171,13 @@ public final class TbMsg implements Serializable { */ @Deprecated(since = "3.5.2") public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId, metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); } @@ -205,12 +206,12 @@ public final class TbMsg implements Serializable { } public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); } @@ -222,13 +223,13 @@ public final class TbMsg implements Serializable { @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); } @@ -250,72 +251,77 @@ public final class TbMsg implements Serializable { */ @Deprecated(since = "3.5.2") public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); } public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { - return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); } public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type.name(), originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); } public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgData(TbMsg tbMsg, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) { - return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { - return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } //used for enqueueForTellNext public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), + return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); } - private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, + private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, + RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { + this(queueName, id, ts, internalType, internalType.name(), originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, ctx, callback); + } + + private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { this.id = id; this.queueName = queueName; @@ -325,6 +331,7 @@ public final class TbMsg implements Serializable { this.ts = System.currentTimeMillis(); } this.type = type; + this.internalType = internalType != null ? internalType : getInternalType(type); this.originator = originator; if (customerId == null || customerId.isNullUid()) { if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) { @@ -410,7 +417,7 @@ public final class TbMsg implements Serializable { } TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; - return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, + return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); @@ -422,17 +429,17 @@ public final class TbMsg implements Serializable { } public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); } public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); } public TbMsg copyWithNewCtx() { - return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY); } @@ -468,8 +475,16 @@ public final class TbMsg implements Serializable { return ts; } + private TbMsgType getInternalType(String type) { + try { + return TbMsgType.valueOf(type); + } catch (IllegalArgumentException e) { + return TbMsgType.NA; + } + } + public boolean isTypeOf(TbMsgType tbMsgType) { - return tbMsgType != null && tbMsgType.name().equals(this.type); + return internalType.equals(tbMsgType); } public boolean isTypeOneOf(TbMsgType... types) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index b3bc89c34d..9240bbc223 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -111,10 +111,24 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService prefs = new EnumMap<>(NotificationType.class); + if (settings != null) { + prefs.putAll(settings.getPrefs()); + } + NotificationPref defaultPref = NotificationPref.createDefault(); + for (NotificationType notificationType : NotificationType.values()) { + NotificationPref pref = prefs.get(notificationType); + if (pref == null) { + prefs.put(notificationType, defaultPref); + } else { + var enabledDeliveryMethods = new LinkedHashMap<>(pref.getEnabledDeliveryMethods()); + // in case a new delivery method was added to the platform + UserNotificationSettings.deliveryMethods.forEach(deliveryMethod -> { + enabledDeliveryMethods.putIfAbsent(deliveryMethod, true); + }); + pref.setEnabledDeliveryMethods(enabledDeliveryMethods); + } + } + return new UserNotificationSettings(prefs); + } + @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure @Override public void createDefaultNotificationConfigs(TenantId tenantId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java index 5fa156725d..3d24c6221f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java @@ -81,6 +81,9 @@ public class JpaNotificationDao extends JpaAbstractDao 'READ'; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java index 2121e0c5fa..d5b06b4537 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -50,7 +49,7 @@ public class TbMsgTypeSwitchNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.tellNext(msg, TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType())); + ctx.tellNext(msg, msg.getInternalType().getRuleNodeConnection()); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java index c4fc8cd76d..603c23cd05 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java @@ -81,9 +81,10 @@ class TbMsgTypeSwitchNodeTest { var msg = resultMsgs.get(i); assertThat(msg).isNotNull(); assertThat(msg.getType()).isNotNull(); + assertThat(msg.getType()).isEqualTo(msg.getInternalType().name()); assertThat(msg).isSameAs(tbMsgList.get(i)); assertThat(resultNodeConnections.get(i)) - .isEqualTo(TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType())); + .isEqualTo(msg.getInternalType().getRuleNodeConnection()); } } diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index f1af3b745a..8a838b4130 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -385,10 +385,10 @@ export class AuthService { } else if (authPayload.authUser) { authPayload.authUser.authority = Authority.ANONYMOUS; } - if (authPayload.authUser.isPublic) { + if (authPayload.authUser?.isPublic) { authPayload.forceFullscreen = true; } - if (authPayload.authUser.isPublic) { + if (authPayload.authUser?.isPublic) { this.loadSystemParams().subscribe( (sysParams) => { authPayload = {...authPayload, ...sysParams}; @@ -399,10 +399,10 @@ export class AuthService { loadUserSubject.error(err); } ); - } else if (authPayload.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { + } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) { loadUserSubject.next(authPayload); loadUserSubject.complete(); - } else if (authPayload.authUser.userId) { + } else if (authPayload.authUser?.userId) { this.userService.getUser(authPayload.authUser.userId).subscribe( (user) => { authPayload.userDetails = user; diff --git a/ui-ngx/src/app/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index ec64621aeb..20e10c91e2 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/ui-ngx/src/app/core/http/notification.service.ts @@ -31,6 +31,7 @@ import { NotificationTarget, NotificationTemplate, NotificationType, + NotificationUserSettings, SlackChanelType, SlackConversation } from '@shared/models/notification.models'; @@ -174,4 +175,12 @@ export class NotificationService { } return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } + + public getNotificationUserSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/notification/settings/user`, defaultHttpOptionsFromConfig(config)); + } + + public saveNotificationUserSettings(settings: NotificationUserSettings, config?: RequestConfig): Observable { + return this.http.post('/api/notification/settings/user', settings, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 2a2f664c3a..8d48c5f038 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -130,7 +130,7 @@ export function isLiteralObject(value: any) { return (!!value) && (value.constructor === Object); } -export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { +export const formatValue = (value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined => { if (isDefinedAndNotNull(value) && isNumeric(value) && (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { let formatted: string | number = Number(value); @@ -150,6 +150,16 @@ export function formatValue(value: any, dec?: number, units?: string, showZeroDe } } +export const formatNumberValue = (value: any, dec?: number): number | undefined => { + if (isDefinedAndNotNull(value) && isNumeric(value)) { + let formatted: string | number = Number(value); + if (isDefinedAndNotNull(dec)) { + formatted = formatted.toFixed(dec); + } + return Number(formatted); + } +} + export function objectValues(obj: any): any[] { return Object.keys(obj).map(e => obj[e]); } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 795d46984c..7c4168a786 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -40,6 +40,15 @@ import { import { ValueCardBasicConfigComponent } from '@home/components/widget/config/basic/cards/value-card-basic-config.component'; +import { + AggregatedValueCardBasicConfigComponent +} from '@home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component'; +import { + AggregatedDataKeyRowComponent +} from '@home/components/widget/config/basic/cards/aggregated-data-key-row.component'; +import { + AggregatedDataKeysPanelComponent +} from '@home/components/widget/config/basic/cards/aggregated-data-keys-panel.component'; @NgModule({ declarations: [ @@ -50,6 +59,9 @@ import { FlotBasicConfigComponent, AlarmsTableBasicConfigComponent, ValueCardBasicConfigComponent, + AggregatedValueCardBasicConfigComponent, + AggregatedDataKeyRowComponent, + AggregatedDataKeysPanelComponent, DataKeyRowComponent, DataKeysPanelComponent ], @@ -66,6 +78,9 @@ import { FlotBasicConfigComponent, AlarmsTableBasicConfigComponent, ValueCardBasicConfigComponent, + AggregatedValueCardBasicConfigComponent, + AggregatedDataKeyRowComponent, + AggregatedDataKeysPanelComponent, DataKeyRowComponent, DataKeysPanelComponent ] @@ -79,5 +94,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type +
+ + + + {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} + + + + + + +
+
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ + + f() + + + + + + + + {{ (modelValue?.aggregationType || aggregationTypes.NONE) }} + ({{ 'datakey.latest-value' | translate }}) + + + ({{ 'datakey.delta' | translate }}:{{ (modelValue?.comparisonResultType === comparisonResultTypes.DELTA_PERCENT ? 'datakey.percent' : 'datakey.absolute') | translate }}) + ({{ 'datakey.delta-calculation-result-previous-value' | translate }}) + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss new file mode 100644 index 0000000000..e07b9fa9ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../../../scss/constants'; + +.tb-aggregated-data-key-row { + .mat-mdc-form-field.tb-inline-field.tb-aggregation-field { + .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { + padding-left: 8px; + padding-right: 0; + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 6px; + .mdc-evolution-chip-set .mdc-evolution-chip { + margin: 0; + } + input.mat-mdc-chip-input { + height: 32px; + margin-left: 0; + } + } + } + .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + .tb-attribute-chip { + .tb-chip-labels { + background: transparent; + } + } + } + } + + .tb-position-field { + width: 132px; + min-width: 132px; + } + + .tb-aggregation-field { + flex: 1; + min-width: 150px; + } + + .tb-units-field, .tb-decimals-field, .tb-font-field, .tb-color-field { + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } + + .tb-units-field { + width: 80px; + min-width: 80px; + } + + .tb-decimals-field { + width: 60px; + min-width: 60px; + } + + .tb-font-field { + width: 40px; + min-width: 40px; + } + + .tb-color-field { + width: 40px; + min-width: 40px; + } + + .tb-units-field, .tb-decimals-field { + display: none; + @media #{$mat-gt-sm} { + display: block; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts new file mode 100644 index 0000000000..411a6a03b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts @@ -0,0 +1,233 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { + ComparisonResultType, + DataKey, + DataKeyConfigMode, + DatasourceType, Widget, + widgetType +} from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone, formatValue } from '@core/utils'; +import { + AggregatedValueCardKeyPosition, + aggregatedValueCardKeyPositionTranslations, + AggregatedValueCardKeySettings +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; + +@Component({ + selector: 'tb-aggregated-data-key-row', + templateUrl: './aggregated-data-key-row.component.html', + styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AggregatedDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { + + aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = + Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); + + aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; + + dataKeyTypes = DataKeyType; + + aggregationTypes = AggregationType; + + comparisonResultTypes = ComparisonResultType; + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + keyName: string; + + @Output() + keyRemoved = new EventEmitter(); + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + public translate: TranslateService, + public truncate: TruncatePipe, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keyRowFormGroup = this.fb.group({ + position: [null, []], + units: [null, []], + decimals: [null, []], + font: [null, []], + color: [null, []] + }); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['keyName'].includes(propName)) { + if (change.currentValue) { + this.modelValue.name = change.currentValue; + setTimeout(() => { + this.updateModel(); + }, 0); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value || {} as DataKey; + const settings: AggregatedValueCardKeySettings = (this.modelValue.settings || {}); + this.keyRowFormGroup.patchValue( + { + position: settings.position || AggregatedValueCardKeyPosition.center, + units: value?.units, + decimals: value?.decimals, + font: settings.font, + color: settings.color + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + dataKeyHasPostprocessing(): boolean { + return !!this.modelValue?.postFuncBody; + } + + editKey() { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(this.modelValue), + dataKeyConfigMode: DataKeyConfigMode.general, + dataKeySettingsSchema: null, + dataKeySettingsDirective: null, + dashboard: null, + aliasController: null, + widget: this.widget, + widgetType: widgetType.latest, + deviceId: null, + entityAliasId: null, + showPostProcessing: true, + callbacks: this.callbacks, + hideDataKeyName: true, + hideDataKeyLabel: true, + hideDataKeyColor: true + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.modelValue = updatedDataKey; + this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); + this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); + this.updateModel(); + } + }); + } + + private updateModel() { + const value = this.keyRowFormGroup.value; + this.modelValue.settings = this.modelValue.settings || {}; + this.modelValue.settings.position = value.position; + this.modelValue.settings.font = value.font; + this.modelValue.settings.color = value.color; + this.modelValue.units = value.units; + this.modelValue.decimals = value.decimals; + this.propagateChange(this.modelValue); + } + + private _valuePreviewFn(): string { + const units: string = this.keyRowFormGroup.get('units').value; + const decimals: number = this.keyRowFormGroup.get('decimals').value; + return formatValue(22, decimals, units, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html new file mode 100644 index 0000000000..5ffbc2a018 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html @@ -0,0 +1,50 @@ + +
+
{{ 'widgets.aggregated-value-card.values' | translate }}
+
+
+
widgets.aggregated-value-card.position
+
widgets.aggregated-value-card.aggregation
+
widget-config.units-short
+
widget-config.decimals-short
+
widgets.aggregated-value-card.font
+
widgets.aggregated-value-card.color
+
+
+
+
+ + +
+
+
+
+ +
+
+ + {{ 'widgets.aggregated-value-card.no-values' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss new file mode 100644 index 0000000000..9280b98ff6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../../../scss/constants'; + +.tb-aggregated-data-keys-panel { + .tb-form-table-header-cell { + &.tb-position-header { + width: 132px; + min-width: 132px; + } + + &.tb-aggregation-header { + flex: 1; + min-width: 150px; + } + + &.tb-units-header { + width: 80px; + min-width: 80px; + } + + &.tb-decimals-header { + width: 60px; + min-width: 60px; + } + + &.tb-font-header { + width: 40px; + min-width: 40px; + } + + &.tb-color-header { + width: 40px; + min-width: 40px; + } + + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + + &.tb-units-header, &.tb-decimals-header { + display: none; + @media #{$mat-gt-sm} { + display: block; + } + } + } + .tb-form-table-body { + tb-aggregated-data-key-row { + overflow: hidden; + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts new file mode 100644 index 0000000000..d8b7f07f92 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; + +@Component({ + selector: 'tb-aggregated-data-keys-panel', + templateUrl: './aggregated-data-keys-panel.component.html', + styleUrls: ['./aggregated-data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AggregatedDataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges { + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + keyName: string; + + dataKeyType: DataKeyType; + + keysListFormGroup: UntypedFormGroup; + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get noKeys(): boolean { + const keys: DataKey[] = this.keysListFormGroup.get('keys').value; + return keys.length === 0; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private utils: UtilsService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + (val) => this.propagateChange(this.keysListFormGroup.get('keys').value) + ); + this.updateParams(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['datasourceType'].includes(propName)) { + this.updateParams(); + } + } + } + } + + private updateParams() { + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + } else { + this.dataKeyType = DataKeyType.timeseries; + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + (this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); + } + + addKey() { + const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null); + dataKey.decimals = 0; + dataKey.settings = {...aggregatedValueCardDefaultKeySettings}; + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const keyControl = this.fb.control(dataKey, []); + keysArray.push(keyControl); + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [])); + }); + } + return this.fb.array(keysControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html new file mode 100644 index 0000000000..4c2742173b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html @@ -0,0 +1,136 @@ + + + + + + +
+
widget-config.appearance
+
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+ + {{ 'widgets.aggregated-value-card.subtitle' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.aggregated-value-card.chart' | translate }} + + + +
+
+ + +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts new file mode 100644 index 0000000000..2770539245 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts @@ -0,0 +1,293 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Injector } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { isUndefined } from '@core/utils'; +import { + cssSizeToStrSize, + DateFormatProcessor, + DateFormatSettings, getDataKey, + resolveCssSize +} from '@shared/models/widget-settings.models'; +import { + aggregatedValueCardDefaultSettings, + AggregatedValueCardWidgetSettings, + createDefaultAggregatedValueLatestDataKeys +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { + AggregationType, + HistoryWindowType, + HOUR, + QuickTimeInterval, + TimewindowType +} from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-aggregated-value-card-basic-config', + templateUrl: './aggregated-value-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.aggregatedValueCardWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + public get keyName(): string { + const dataKey = getDataKey(this.aggregatedValueCardWidgetConfigForm.get('datasources').value); + if (dataKey) { + return dataKey.name; + } else { + return null; + } + } + + aggregatedValueCardWidgetConfigForm: UntypedFormGroup; + + datePreviewFn = this._datePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private cd: ChangeDetectorRef, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.aggregatedValueCardWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [ + { name: 'watermeter', label: 'Watermeter', type: DataKeyType.timeseries } + ], + createDefaultAggregatedValueLatestDataKeys('watermeter', 'm³') + ); + configData.config.useDashboardTimewindow = false; + configData.config.displayTimewindow = true; + configData.config.timewindow = { + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.INTERVAL, + quickInterval: QuickTimeInterval.CURRENT_MONTH_SO_FAR, + }, + aggregation: { + type: AggregationType.AVG, + interval: 12 * HOUR, + limit: 5000 + } + }; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: AggregatedValueCardWidgetSettings = {...aggregatedValueCardDefaultSettings, ...(configData.config.settings || {})}; + const dataKey = getDataKey(configData.config.datasources); + const keyName = dataKey?.name; + const iconSize = resolveCssSize(configData.config.iconSize); + this.aggregatedValueCardWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + showSubtitle: [settings.showSubtitle, []], + subtitle: [settings.subtitle, []], + subtitleFont: [settings.subtitleFont, []], + subtitleColor: [settings.subtitleColor, []], + + showDate: [settings.showDate, []], + dateFormat: [settings.dateFormat, []], + dateFont: [settings.dateFont, []], + dateColor: [settings.dateColor, []], + + showChart: [settings.showChart, []], + chartColor: [settings.chartColor, []], + + values: [this.getValues(configData.config.datasources, keyName), []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.showSubtitle = config.showSubtitle; + this.widgetConfig.config.settings.subtitle = config.subtitle; + this.widgetConfig.config.settings.subtitleFont = config.subtitleFont; + this.widgetConfig.config.settings.subtitleColor = config.subtitleColor; + + this.widgetConfig.config.settings.showDate = config.showDate; + this.widgetConfig.config.settings.dateFormat = config.dateFormat; + this.widgetConfig.config.settings.dateFont = config.dateFont; + this.widgetConfig.config.settings.dateColor = config.dateColor; + + this.widgetConfig.config.settings.showChart = config.showChart; + this.widgetConfig.config.settings.chartColor = config.chartColor; + + this.setValues(config.values, this.widgetConfig.config.datasources); + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon', 'showSubtitle', 'showDate', 'showChart']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.aggregatedValueCardWidgetConfigForm.get('showIcon').value; + const showSubtitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showSubtitle').value; + const showDate: boolean = this.aggregatedValueCardWidgetConfigForm.get('showDate').value; + const showChart: boolean = this.aggregatedValueCardWidgetConfigForm.get('showChart').value; + + if (showTitle) { + this.aggregatedValueCardWidgetConfigForm.get('title').enable(); + this.aggregatedValueCardWidgetConfigForm.get('titleFont').enable(); + this.aggregatedValueCardWidgetConfigForm.get('titleColor').enable(); + this.aggregatedValueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.aggregatedValueCardWidgetConfigForm.get('iconSize').enable(); + this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').enable(); + this.aggregatedValueCardWidgetConfigForm.get('icon').enable(); + this.aggregatedValueCardWidgetConfigForm.get('iconColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.aggregatedValueCardWidgetConfigForm.get('title').disable(); + this.aggregatedValueCardWidgetConfigForm.get('titleFont').disable(); + this.aggregatedValueCardWidgetConfigForm.get('titleColor').disable(); + this.aggregatedValueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); + this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); + } + + if (showSubtitle) { + this.aggregatedValueCardWidgetConfigForm.get('subtitle').enable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').enable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('subtitle').disable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').disable(); + this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').disable(); + } + + if (showDate) { + this.aggregatedValueCardWidgetConfigForm.get('dateFormat').enable(); + this.aggregatedValueCardWidgetConfigForm.get('dateFont').enable(); + this.aggregatedValueCardWidgetConfigForm.get('dateColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('dateFormat').disable(); + this.aggregatedValueCardWidgetConfigForm.get('dateFont').disable(); + this.aggregatedValueCardWidgetConfigForm.get('dateColor').disable(); + } + + if (showChart) { + this.aggregatedValueCardWidgetConfigForm.get('chartColor').enable(); + } else { + this.aggregatedValueCardWidgetConfigForm.get('chartColor').disable(); + } + } + + private getValues(datasources: Datasource[], keyName: string): DataKey[] { + if (datasources && datasources.length) { + return (datasources[0].latestDataKeys || []).filter(k => k.name === keyName); + } + return []; + } + + private setValues(values: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].latestDataKeys = values; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _datePreviewFn(): string { + const dateFormat: DateFormatSettings = this.aggregatedValueCardWidgetConfigForm.get('dateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index 05275e0a70..63f936195a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
{{ 'datakey.timeseries' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html index 4c2c9a834a..78f96d8264 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -32,7 +32,7 @@ [cdkDropListDisabled]="!dragEnabled" (cdkDropListDropped)="keyDrop($event)">
datakey.general
- + {{ 'entity.key' | translate }}
+
+ + {{ 'timewindow.displayTypePrefix' | translate }} + +
timewindow.preview
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts index e0e4ee5615..85e7e5a4be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts @@ -63,7 +63,8 @@ export class TimewindowStylePanelComponent extends PageComponent implements OnIn icon: [computedTimewindowStyle.icon, []], iconPosition: [computedTimewindowStyle.iconPosition, []], font: [computedTimewindowStyle.font, []], - color: [computedTimewindowStyle.color, []] + color: [computedTimewindowStyle.color, []], + displayTypePrefix: [computedTimewindowStyle.displayTypePrefix, []] } ); this.updatePreviewStyle(this.timewindowStyle); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index 9bda9345dc..fb5dd252b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -119,7 +119,7 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement return this.configForm().valid; } - protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) { + protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[], latestKeys?: DataKey[]) { let datasources = configData.config.datasources; if (!datasources || !datasources.length) { datasources = [ @@ -135,23 +135,58 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement dataKeys = []; datasources[0].dataKeys = dataKeys; } + let latestDataKeys = datasources[0].latestDataKeys; + if (!latestDataKeys) { + latestDataKeys = []; + datasources[0].latestDataKeys = latestDataKeys; + } if (keys && keys.length) { dataKeys.length = 0; keys.forEach(key => { - const dataKey = - this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); - if (key.label) { - dataKey.label = key.label; - } - if (key.units) { - dataKey.units = key.units; - } - if (isDefinedAndNotNull(key.decimals)) { - dataKey.decimals = key.decimals; - } + const dataKey = this.constructDataKey(configData, key); dataKeys.push(dataKey); }); } + if (latestKeys && latestKeys.length) { + latestDataKeys.length = 0; + latestKeys.forEach(key => { + const dataKey = this.constructDataKey(configData, key); + latestDataKeys.push(dataKey); + }); + } + } + + protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey): DataKey { + const dataKey = + this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); + if (key.label) { + dataKey.label = key.label; + } + if (key.units) { + dataKey.units = key.units; + } + if (isDefinedAndNotNull(key.decimals)) { + dataKey.decimals = key.decimals; + } + if (isDefinedAndNotNull(key.settings)) { + dataKey.settings = key.settings; + } + if (isDefinedAndNotNull(key.aggregationType)) { + dataKey.aggregationType = key.aggregationType; + } + if (isDefinedAndNotNull(key.comparisonEnabled)) { + dataKey.comparisonEnabled = key.comparisonEnabled; + } + if (isDefinedAndNotNull(key.timeForComparison)) { + dataKey.timeForComparison = key.timeForComparison; + } + if (isDefinedAndNotNull(key.comparisonCustomIntervalValue)) { + dataKey.comparisonCustomIntervalValue = key.comparisonCustomIntervalValue; + } + if (isDefinedAndNotNull(key.comparisonResultType)) { + dataKey.comparisonResultType = key.comparisonResultType; + } + return dataKey; } protected abstract configForm(): UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html new file mode 100644 index 0000000000..b20431fa64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html @@ -0,0 +1,90 @@ + +
+
+
+ + +
+ + + +
+ + +
{{ subtitle$ | async }}
+
+ +
+
+
+ + + + + + +
+
+ + + +
+
+ + + + + + +
+
+
+
+ +
+
+
{{tickMax$ | async}}
+
{{tickMin$ | async}}
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ + arrow_upward + arrow_downward +
+
+ {{ value.value }} + {{ value.units }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss new file mode 100644 index 0000000000..6dee828c36 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-aggregated-value-card-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 20px 24px 24px; + > div:not(.tb-value-card-overlay) { + z-index: 1; + } + .tb-aggregated-value-card-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + > div.tb-aggregated-value-card-title-panel { + display: flex; + flex-direction: column; + .tb-aggregated-value-card-subtitle { + margin-left: 28px; + } + } + .tb-aggregated-value-card-values, .tb-aggregated-value-card-chart { + flex: 1; + min-height: 0; + overflow: hidden; + } + .tb-aggregated-value-card-values-container { + width: 100%; + height: 100%; + padding: 8px 0; + display: grid; + grid-template-columns: minmax(0, 1fr) fit-content(100%) minmax(0, 1fr); + .tb-aggregated-value-card-values-section { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + &.left { + align-items: flex-start; + } + &.center { + align-items: center; + } + &.right { + align-items: flex-end; + } + } + } + .tb-aggregated-value-card-chart { + display: flex; + gap: 8px; + flex-direction: row; + .tb-aggregated-value-card-chart-ticks { + height: 100%; + display: flex; + flex-direction: column; + place-content: flex-end space-between; + align-items: flex-end; + font-size: 11px; + line-height: 16px; + font-weight: 400; + color: rgba(0, 0, 0, 0.38); + } + .tb-aggregated-value-card-chart-container { + position: relative; + flex: 1; + margin-top: 8px; + margin-bottom: 8px; + .tb-aggregated-value-card-chart-element { + width: 100%; + height: 100%; + } + .tb-aggregated-value-card-chart-boundary { + position: absolute; + width: 6px; + height: 6px; + &.top { + top: 0; + border-top: 2px solid rgba(0,0,0,0.38); + } + &.left { + left: 0; + border-left: 2px solid rgba(0,0,0,0.38); + } + &.right { + right: 0; + border-right: 2px solid rgba(0,0,0,0.38); + } + &.bottom { + bottom: 0; + border-bottom: 2px solid rgba(0,0,0,0.38); + } + } + } + } + .tb-aggregated-value-card-value { + white-space: nowrap; + min-height: 0; + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + .value-arrow-container { + display: flex; + } + .value-text { + line-height: 1; + } + .value-arrow { + font-size: 1.1em; + height: 1.1em; + line-height: 1.1em; + min-width: 1em; + width: 1em; + } + .units { + font-size: 85%; + padding-left: 0.2em; + &.small { + font-size: 50%; + } + } + } + } +} + +:host ::ng-deep { + .tb-aggregated-value-card-panel { + > div.tb-aggregated-value-card-title-panel { + .tb-widget-title { + padding: 0; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts new file mode 100644 index 0000000000..fca9f31084 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts @@ -0,0 +1,261 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + TemplateRef, + ViewChild +} from '@angular/core'; +import { + aggregatedValueCardDefaultSettings, + AggregatedValueCardKeyPosition, + AggregatedValueCardValue, + AggregatedValueCardWidgetSettings, + computeAggregatedCardValue, + getTsValueByLatestDataKey +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { + backgroundStyle, + ColorProcessor, + ComponentStyle, + DateFormatProcessor, getDataKey, + getLatestSingleTsValue, + overlayStyle, + textStyle +} from '@shared/models/widget-settings.models'; +import { DatePipe } from '@angular/common'; +import { TbFlot } from '@home/components/widget/lib/flot-widget'; +import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; +import { DataKey } from '@shared/models/widget.models'; +import { formatNumberValue, formatValue, isDefined, isNumeric } from '@core/utils'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-aggregated-value-card-widget', + templateUrl: './aggregated-value-card-widget.component.html', + styleUrls: ['./aggregated-value-card-widget.component.scss'] +}) +export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit { + + @ViewChild('chartElement', {static: false}) chartElement: ElementRef; + + aggregatedValueCardKeyPosition = AggregatedValueCardKeyPosition; + + settings: AggregatedValueCardWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + showSubtitle = true; + subtitle$: Observable; + subtitleStyle: ComponentStyle = {}; + subtitleColor: ColorProcessor; + + showValues = false; + + values: {[key: string]: AggregatedValueCardValue} = {}; + + showChart = true; + chartColor: ColorProcessor; + + showDate = true; + dateFormat: DateFormatProcessor; + dateStyle: ComponentStyle = {}; + dateColor: ColorProcessor; + + backgroundStyle: ComponentStyle = {}; + overlayStyle: ComponentStyle = {}; + + private flot: TbFlot; + private flotDataKey: DataKey; + + private lastUpdateTs: number; + + tickMin$: Observable; + tickMax$: Observable; + + constructor(private date: DatePipe, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.aggregatedValueCardWidget = this; + this.settings = {...aggregatedValueCardDefaultSettings, ...this.ctx.settings}; + this.showSubtitle = this.settings.showSubtitle; + const subtitle = this.settings.subtitle; + this.subtitle$ = this.ctx.registerLabelPattern(subtitle, this.subtitle$); + this.subtitleStyle = textStyle(this.settings.subtitleFont, '0.25px'); + this.subtitleColor = ColorProcessor.fromSettings(this.settings.subtitleColor); + + const dataKey = getDataKey(this.ctx.defaultSubscription.datasources); + if (dataKey?.name && this.ctx.defaultSubscription.firstDatasource?.latestDataKeys?.length) { + const dataKeys = this.ctx.defaultSubscription.firstDatasource?.latestDataKeys; + for (const position of Object.keys(AggregatedValueCardKeyPosition)) { + const value = computeAggregatedCardValue(dataKeys, dataKey?.name, AggregatedValueCardKeyPosition[position]); + if (value) { + this.values[position] = value; + } + } + this.showValues = !!Object.keys(this.values).length; + } + + this.showChart = this.settings.showChart; + this.chartColor = ColorProcessor.fromSettings(this.settings.chartColor); + if (this.showChart) { + if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { + this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; + this.flotDataKey.settings = { + fillLines: false, + showLines: true, + lineWidth: 2 + } as TbFlotKeySettings; + this.flotDataKey.color = this.chartColor.color; + } + } + + this.showDate = this.settings.showDate; + this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat); + this.dateStyle = textStyle(this.settings.dateFont, '0.25px'); + this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor); + + this.backgroundStyle = backgroundStyle(this.settings.background); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + } + + ngAfterViewInit(): void { + if (this.showChart) { + const settings = { + shadowSize: 0, + enableSelection: false, + smoothLines: false, + grid: { + tickColor: 'rgba(0,0,0,0.12)', + horizontalLines: true, + verticalLines: false, + outlineWidth: 0, + minBorderMargin: 0, + margin: 0 + }, + yaxis: { + showLabels: false, + tickGenerator: 'return [(axis.max + axis.min) / 2];' + }, + xaxis: { + showLabels: false + } + } as TbFlotSettings; + this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings); + this.tickMin$ = this.flot.yMin$.pipe( + map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals), + (this.flotDataKey?.units || this.ctx.units)) + )); + this.tickMax$ = this.flot.yMax$.pipe( + map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals), + (this.flotDataKey?.units || this.ctx.units)) + )); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onDataUpdated() { + const tsValue = getLatestSingleTsValue(this.ctx.data); + let ts; + let value; + if (tsValue) { + ts = tsValue[0]; + value = tsValue[1]; + } + this.subtitleColor.update(value); + this.dateColor.update(value); + + if (this.showChart) { + this.chartColor.update(value); + this.flot.updateSeriesColor(this.chartColor.color); + this.flot.update(); + } + + this.updateLastUpdateTs(ts); + this.cd.detectChanges(); + } + + public onLatestDataUpdated() { + if (this.showValues) { + for (const aggValue of Object.values(this.values)) { + const tsValue = getTsValueByLatestDataKey(this.ctx.latestData, aggValue.key); + let ts; + let value; + if (tsValue) { + ts = tsValue[0]; + value = tsValue[1]; + aggValue.value = formatValue(value, (aggValue.key.decimals || this.ctx.decimals), null, false); + } else { + aggValue.value = 'N/A'; + } + const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals)); + aggValue.color.update(numeric); + if (aggValue.showArrow && isDefined(numeric)) { + aggValue.upArrow = numeric > 0; + aggValue.downArrow = numeric < 0; + } else { + aggValue.upArrow = aggValue.downArrow = false; + } + this.updateLastUpdateTs(ts); + } + this.cd.detectChanges(); + } + } + + public onResize() { + if (this.showChart) { + this.flot.resize(); + } + } + + public onEditModeChanged() { + if (this.showChart) { + this.flot.checkMouseEvents(); + } + } + + public onDestroy() { + if (this.showChart) { + this.flot.destroy(); + } + } + + private updateLastUpdateTs(ts: number) { + if (ts && (!this.lastUpdateTs || ts > this.lastUpdateTs)) { + this.lastUpdateTs = ts; + this.dateFormat.update(ts); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts new file mode 100644 index 0000000000..273ca1edaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts @@ -0,0 +1,232 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + BackgroundSettings, + BackgroundType, + ColorProcessor, + ColorSettings, + ColorType, + ComponentStyle, + constantColor, + DateFormatSettings, + Font, + iconStyle, + lastUpdateAgoDateFormat, + textStyle +} from '@shared/models/widget-settings.models'; +import { ComparisonResultType, DataKey, DatasourceData } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; + +export interface AggregatedValueCardWidgetSettings { + showSubtitle: boolean; + subtitle: string; + subtitleFont: Font; + subtitleColor: ColorSettings; + showDate: boolean; + dateFormat: DateFormatSettings; + dateFont: Font; + dateColor: ColorSettings; + showChart: boolean; + chartColor: ColorSettings; + background: BackgroundSettings; +} + +export enum AggregatedValueCardKeyPosition { + center = 'center', + rightTop = 'rightTop', + rightBottom = 'rightBottom', + leftTop = 'leftTop', + leftBottom = 'leftBottom' +} + +export const aggregatedValueCardKeyPositionTranslations = new Map( + [ + [AggregatedValueCardKeyPosition.center, 'widgets.aggregated-value-card.position-center'], + [AggregatedValueCardKeyPosition.rightTop, 'widgets.aggregated-value-card.position-right-top'], + [AggregatedValueCardKeyPosition.rightBottom, 'widgets.aggregated-value-card.position-right-bottom'], + [AggregatedValueCardKeyPosition.leftTop, 'widgets.aggregated-value-card.position-left-top'], + [AggregatedValueCardKeyPosition.leftBottom, 'widgets.aggregated-value-card.position-left-bottom'] + ] +); + +export interface AggregatedValueCardKeySettings { + position: AggregatedValueCardKeyPosition; + font: Font; + color: ColorSettings; + showArrow: boolean; +} + +export interface AggregatedValueCardValue { + key: DataKey; + value: string; + units: string; + style: ComponentStyle; + color: ColorProcessor; + center: boolean; + showArrow: boolean; + upArrow: boolean; + downArrow: boolean; +} + +export const computeAggregatedCardValue = (dataKeys: DataKey[], keyName: string, position: AggregatedValueCardKeyPosition): AggregatedValueCardValue => { + const key = dataKeys.find(dataKey => ( dataKey.name === keyName && (dataKey.settings?.position === position || + (!dataKey.settings?.position && position === AggregatedValueCardKeyPosition.center)) )); + if (key) { + const settings: AggregatedValueCardKeySettings = key.settings; + return { + key, + value: '', + units: key.units, + style: textStyle(settings.font, '0.25px'), + color: ColorProcessor.fromSettings(settings.color), + center: position === AggregatedValueCardKeyPosition.center, + showArrow: settings.showArrow, + upArrow: false, + downArrow: false + }; + } +}; + +export const getTsValueByLatestDataKey = (latestData: Array, dataKey: DataKey): [number, any] => { + if (latestData?.length) { + const dsData = latestData.find(data => data.dataKey === dataKey); + if (dsData?.data?.length) { + return dsData.data[0]; + } + } + return null; +}; + +export const aggregatedValueCardDefaultSettings: AggregatedValueCardWidgetSettings = { + showSubtitle: true, + subtitle: '${entityName}', + subtitleFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + subtitleColor: constantColor('rgba(0, 0, 0, 0.38)'), + showDate: true, + dateFormat: lastUpdateAgoDateFormat(), + dateFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + dateColor: constantColor('rgba(0, 0, 0, 0.38)'), + showChart: true, + chartColor: constantColor('rgba(0, 0, 0, 0.87)'), + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; + +export const aggregatedValueCardDefaultKeySettings: AggregatedValueCardKeySettings = { + position: AggregatedValueCardKeyPosition.center, + font: { + family: 'Roboto', + size: 14, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '1' + }, + color: constantColor('rgba(0, 0, 0, 0.87)'), + showArrow: false +}; + +export const createDefaultAggregatedValueLatestDataKeys = (keyName: string, units): DataKey[] => [ + { + name: keyName, label: keyName, type: DataKeyType.timeseries, units, decimals: 0, + aggregationType: AggregationType.NONE, + settings: { + position: AggregatedValueCardKeyPosition.center, + font: { + family: 'Roboto', + size: 52, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '1' + }, + color: constantColor('rgba(0, 0, 0, 0.87)'), + showArrow: false + } as AggregatedValueCardKeySettings + }, + { + name: keyName, label: 'Delta percent ' + keyName, type: DataKeyType.timeseries, units: '%', decimals: 0, + aggregationType: AggregationType.AVG, + comparisonEnabled: true, + timeForComparison: 'previousInterval', + comparisonResultType: ComparisonResultType.DELTA_PERCENT, + settings: { + position: AggregatedValueCardKeyPosition.rightTop, + font: { + family: 'Roboto', + size: 14, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '1' + }, + color: { + color: 'rgba(0, 0, 0, 0.87)', + type: ColorType.range, + rangeList: [ + {to: 0, color: '#198038'}, + {from: 0, to: 0, color: 'rgba(0, 0, 0, 0.87)'}, + {from: 0, color: '#D12730'} + ], + colorFunction: '' + }, + showArrow: true + } as AggregatedValueCardKeySettings + }, + { + name: keyName, label: 'Delta absolute ' + keyName, type: DataKeyType.timeseries, units, decimals: 1, + aggregationType: AggregationType.AVG, + comparisonEnabled: true, + timeForComparison: 'previousInterval', + comparisonResultType: ComparisonResultType.DELTA_ABSOLUTE, + settings: { + position: AggregatedValueCardKeyPosition.rightBottom, + font: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + color: constantColor('rgba(0, 0, 0, 0.38)'), + showArrow: false + } as AggregatedValueCardKeySettings + } + ]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html index fee7dc5259..377e0a31f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html @@ -66,7 +66,7 @@
{{ label$ | async }}
-
{{ dateFormat.formatted }}
+
{{ valueText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts index 64267a5637..907878368d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts @@ -83,7 +83,7 @@ export function loadNodeCtxFunction any>(functionB } export function materialIconHtml(materialIcon: string): string { - return '' + materialIcon + ''; + return '' + materialIcon + ''; } export function iconUrlHtml(iconUrl: string): string { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 844d7540ff..7faa9e9de3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -132,6 +132,7 @@ export interface TbFlotYAxisSettings { ticksFormatter: string; tickDecimals: number; tickSize: number; + tickGenerator: string; } export interface TbFlotBaseSettings { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index 1d20068e20..e38bb3b098 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -18,7 +18,8 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { createLabelFromDatasource, - deepClone, formattedDataFormDatasourceData, + deepClone, + formattedDataFormDatasourceData, insertVariable, isDefined, isDefinedAndNotNull, @@ -59,6 +60,7 @@ import { AggregationType } from '@shared/models/time/time.models'; import { CancelAnimationFrame } from '@core/services/raf.service'; import { UtilsService } from '@core/services/utils.service'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { BehaviorSubject } from 'rxjs'; import Timeout = NodeJS.Timeout; const moment = moment_; @@ -130,9 +132,15 @@ export class TbFlot { private pieAnimationLastTime: number; private pieAnimationCaf: CancelAnimationFrame; - constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery) { + private yMinSubject = new BehaviorSubject(-1); + private yMaxSubject = new BehaviorSubject(1); + + yMin$ = this.yMinSubject.asObservable(); + yMax$ = this.yMaxSubject.asObservable(); + + constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery, settings?: TbFlotSettings) { this.chartType = this.chartType || 'line'; - this.settings = ctx.settings as TbFlotSettings; + this.settings = settings || (ctx.settings as TbFlotSettings); this.utils = this.ctx.$injector.get(UtilsService); this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true; this.selectionMode = this.enableSelection ? 'x' : null; @@ -209,6 +217,12 @@ export class TbFlot { } else { this.yaxis.tickSize = null; } + if (this.settings.yaxis.tickGenerator?.length) { + try { + this.yaxis.ticks = new Function('axis', + this.settings.yaxis.tickGenerator); + } catch (e) {} + } if (isNumber(this.settings.yaxis.tickDecimals)) { this.yaxis.tickDecimals = this.settings.yaxis.tickDecimals; } else { @@ -717,6 +731,15 @@ export class TbFlot { } } + public updateSeriesColor(color: string) { + if (this.subscription?.data?.length) { + const series = this.subscription.data[0] as TbFlotSeries; + series.dataKey.color = color; + series.color = color; + series.highlightColor = tinycolor(color).setAlpha(.75).toRgbString(); + } + } + private latestDataByDataIndex(index: number): FormattedData { if (this.latestData[index]) { return this.latestData[index]; @@ -802,6 +825,8 @@ export class TbFlot { clearTimeout(this.resizeTimeoutHandle); this.resizeTimeoutHandle = null; } + this.yMinSubject.complete(); + this.yMaxSubject.complete(); } private createPlot() { @@ -818,6 +843,7 @@ export class TbFlot { } else { this.plot = $.plot(this.$element, this.subscription.data, this.options) as JQueryPlot; } + this.updateYMinMax(); } else { this.createPlotTimeoutHandle = setTimeout(this.createPlot.bind(this), 30); } @@ -830,6 +856,20 @@ export class TbFlot { this.plot.setupGrid(); } this.plot.draw(); + this.updateYMinMax(); + } + + private updateYMinMax() { + if (this.plot?.getYAxes().length) { + const min = this.plot?.getYAxes()[0].min; + const max = this.plot?.getYAxes()[0].max; + if (this.yMinSubject.value !== min) { + this.yMinSubject.next(min); + } + if (this.yMaxSubject.value !== max) { + this.yMaxSubject.next(max); + } + } } private redrawPlot() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html new file mode 100644 index 0000000000..32d48347ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html @@ -0,0 +1,48 @@ + + +
+
widgets.aggregated-value-card.value-appearance
+
+
{{ 'widgets.aggregated-value-card.position' | translate }}
+ + + + {{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} + + + +
+
+
{{ 'widgets.aggregated-value-card.font' | translate }}
+ + +
+
+
{{ 'widgets.aggregated-value-card.color' | translate }}
+ + +
+
+ + {{ 'widgets.aggregated-value-card.display-up-down-arrow' | translate }} + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts new file mode 100644 index 0000000000..e527c3b3d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts @@ -0,0 +1,64 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + aggregatedValueCardDefaultKeySettings, + AggregatedValueCardKeyPosition, + aggregatedValueCardKeyPositionTranslations +} from '@home/components/widget/lib/cards/aggregated-value-card.models'; +import { constantColor } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-aggregated-value-card-key-settings', + templateUrl: './aggregated-value-card-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AggregatedValueCardKeySettingsComponent extends WidgetSettingsComponent { + + aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = + Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); + + aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; + + aggregatedValueCardKeySettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.aggregatedValueCardKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...aggregatedValueCardDefaultKeySettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.aggregatedValueCardKeySettingsForm = this.fb.group({ + position: [settings.position, []], + font: [settings.font, []], + color: [settings.color, []], + showArrow: [settings.showArrow, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 46454937f6..9241f60dda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -74,7 +74,8 @@ export const flotDefaultSettings = (chartType: ChartType): Partial - mdi:function-variant + mdi:function-variant
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 6d8ad1eefe..f92203459e 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 @@ -99,7 +99,7 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor { } private updateColorStyle() { - if (!this.disabled) { + if (!this.disabled && this.modelValue) { let colors: string[] = [this.modelValue.color]; if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) { const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 1e4010faaa..0f467ba917 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -267,6 +267,9 @@ import { ValueCardWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/value-card-widget-settings.component'; import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module'; +import { + AggregatedValueCardKeySettingsComponent +} from '@home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component'; @NgModule({ declarations: [ @@ -366,7 +369,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings TripAnimationWidgetSettingsComponent, DocLinksWidgetSettingsComponent, QuickLinksWidgetSettingsComponent, - ValueCardWidgetSettingsComponent + ValueCardWidgetSettingsComponent, + AggregatedValueCardKeySettingsComponent ], imports: [ CommonModule, @@ -471,7 +475,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings TripAnimationWidgetSettingsComponent, DocLinksWidgetSettingsComponent, QuickLinksWidgetSettingsComponent, - ValueCardWidgetSettingsComponent + ValueCardWidgetSettingsComponent, + AggregatedValueCardKeySettingsComponent ] }) export class WidgetSettingsModule { @@ -541,5 +546,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type +
+
+
+ + + {{notificationTemplateTypeTranslateMap.get(notificationSettingsFormGroup.get('name').value)?.name | translate}} + + +
+
+
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss new file mode 100644 index 0000000000..ee37181a09 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .notification-type { + font-size: 14px; + + &-disabled { + color: rgba(0, 0, 0, 0.38) + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts new file mode 100644 index 0000000000..76e9babf00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts @@ -0,0 +1,126 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { + NotificationDeliveryMethod, + NotificationTemplateTypeTranslateMap, + NotificationUserSetting +} from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-notification-setting-form', + templateUrl: './notification-setting-form.component.html', + styleUrls: ['./notification-setting-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationSettingFormComponent), + multi: true + } + ] +}) +export class NotificationSettingFormComponent implements ControlValueAccessor, OnInit, OnDestroy { + + @Input() + disabled: boolean; + + @Input() + deliveryMethods: NotificationDeliveryMethod[] = []; + + @Input() + allowDeliveryMethods: NotificationDeliveryMethod[] = []; + + notificationSettingsFormGroup: UntypedFormGroup; + + notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; + + private propagateChange = null; + + private valueChange$: Subscription = null; + + constructor(private utils: UtilsService, + private fb: UntypedFormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + const deliveryMethod = {}; + this.deliveryMethods.forEach(value => { + deliveryMethod[value] = true; + }); + this.notificationSettingsFormGroup = this.fb.group( + { + name: [''], + enabled: [true], + enabledDeliveryMethods: this.fb.group({ + ...deliveryMethod + }) + }); + this.valueChange$ = this.notificationSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + ngOnDestroy() { + if (this.valueChange$) { + this.valueChange$.unsubscribe(); + this.valueChange$ = null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.notificationSettingsFormGroup.disable({emitEvent: false}); + } else { + this.notificationSettingsFormGroup.enable({emitEvent: false}); + } + } + + toggleEnabled() { + this.notificationSettingsFormGroup.get('enabled').patchValue(!this.notificationSettingsFormGroup.get('enabled').value); + } + + getChecked(deliveryMethod: NotificationDeliveryMethod): boolean { + return this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value; + } + + toggleDeliviryMethod(deliveryMethod: NotificationDeliveryMethod) { + this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod) + .patchValue(!this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value); + } + + writeValue(value: NotificationUserSetting): void { + if (isDefinedAndNotNull(value)) { + this.notificationSettingsFormGroup.patchValue(value, {emitEvent: false}); + } + } + + private updateModel() { + this.propagateChange(this.notificationSettingsFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts new file mode 100644 index 0000000000..6a65e554bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts @@ -0,0 +1,41 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Routes } from '@angular/router'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { inject, NgModule } from '@angular/core'; +import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component'; +import { NotificationService } from '@core/http/notification.service'; + +export const notificationUserSettingsRoutes: Routes = [ + { + path: 'notificationSettings', + component: NotificationSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'account.notification-settings', + breadcrumb: { + label: 'account.notification-settings', + icon: 'settings' + } + }, + resolve: { + userSettings: () => inject(NotificationService).getNotificationUserSettings() + } + } +]; diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html new file mode 100644 index 0000000000..c531f10d78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -0,0 +1,84 @@ + +
+ + +
+
+ notification.settings.notification-settings +
+
+ +
+
+
+ + +
+ +
+
+
+
+
+ + notification.settings.type + +
+
+
+ + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + +
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss new file mode 100644 index 0000000000..a91c57ed65 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../../scss/constants"; + +:host { + .mat-mdc-card.settings-card { + margin: 8px; + @media #{$mat-gt-sm} { + width: 60%; + } + .mat-headline-5 { + margin: 0; + } + .notification-form { + height: 100%; + min-height: min-content; + max-height: min-content; + } + .notification-section { + height: 100%; + border: 1px solid rgba(0, 0, 0, 0.12); + overflow: scroll; + &-block { + min-width: 470px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts new file mode 100644 index 0000000000..c28e030eec --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts @@ -0,0 +1,188 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { + NotificationDeliveryMethod, + NotificationDeliveryMethodTranslateMap, + NotificationUserSettings +} from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { DialogService } from '@core/services/dialog.service'; + +@Component({ + selector: 'tb-notification-settings', + templateUrl: './notification-settings.component.html', + styleUrls: ['./notification-settings.component.scss'] +}) +export class NotificationSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + notificationSettings: UntypedFormGroup; + + notificationDeliveryMethods: NotificationDeliveryMethod[]; + notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + + allowNotificationDeliveryMethods: Array; + + constructor(protected store: Store, + private route: ActivatedRoute, + private translate: TranslateService, + private dialogService: DialogService, + private notificationService: NotificationService, + private fb: UntypedFormBuilder,) { + super(store); + } + + ngOnInit() { + this.notificationDeliveryMethods = this.getNotificationDeliveryMethods(); + + this.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => { + this.allowNotificationDeliveryMethods = allowMethods; + }); + + this.buildNotificationSettingsForm(); + this.patchNotificationSettings(this.route.snapshot.data.userSettings); + } + + private getNotificationDeliveryMethods(): NotificationDeliveryMethod[] { + const deliveryMethods = new Set([ + NotificationDeliveryMethod.SLACK + ]); + return Object.values(NotificationDeliveryMethod).filter(type => !deliveryMethods.has(type)); + } + + private buildNotificationSettingsForm() { + this.notificationSettings = this.fb.group({ + prefs: this.fb.array([]) + }); + } + + private patchNotificationSettings(settings: NotificationUserSettings) { + const notificationSettingsControls: Array = []; + let preparedSettings; + if (settings.prefs) { + preparedSettings = this.prepareNotificationSettings(settings.prefs); + preparedSettings.forEach((setting) => { + setting.enabledDeliveryMethods = Object.assign( + this.notificationDeliveryMethods.reduce((a, v) => ({ ...a, [v]: true}), {}), + setting.enabledDeliveryMethods + ); + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + } + + private prepareNotificationSettings(prefs: any) { + return Object.entries(prefs).map((value: any) => { + value[1].name = value[0]; + return value[1]; + }); + } + + resetSettings() { + this.dialogService.confirm( + this.translate.instant('notification.settings.reset-all-title'), + this.translate.instant('notification.settings.reset-all-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe( + result => { + if (result) { + const settings = this.prepareNotificationSettings(this.route.snapshot.data.userSettings.prefs); + const notificationSettingsControls: Array = []; + this.notificationSettings.reset({}); + if (settings) { + settings.forEach((setting) => { + setting.enabled = true; + setting.enabledDeliveryMethods = this.notificationDeliveryMethods.reduce((a, v) => ({ ...a, [v]: true}), {}); + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + this.save(); + } + } + ); + } + + getChecked = (method: NotificationDeliveryMethod = null): boolean => { + const type = this.notificationSettings.get('prefs').value; + if (isDefinedAndNotNull(method)) { + return isDefinedAndNotNull(type) && type.every(resource => resource.enabledDeliveryMethods[method]); + } + return isDefinedAndNotNull(type) && type.every(resource => resource.enabled); + }; + + getSomeChecked = () => { + const type = this.notificationSettings.get('prefs').value; + return isDefinedAndNotNull(type) && type.some(resource => resource.enabled); + }; + + getIndeterminate = (deliveryMethod: NotificationDeliveryMethod = null): boolean => { + const type = this.notificationSettings.get('prefs').value; + if (isDefinedAndNotNull(type)) { + const checkedResource = isDefinedAndNotNull(deliveryMethod) ? + type.filter(resource => resource.enabledDeliveryMethods[deliveryMethod]) : + type.filter(resource => resource.enabled); + return checkedResource.length !== 0 && checkedResource.length !== type.length; + } + return false; + }; + + changeInstanceTypeCheckBox = (value: boolean, deliveryMethod: NotificationDeliveryMethod = null): void => { + const type = deepClone(this.notificationSettings.get('prefs').value); + if (isDefinedAndNotNull(deliveryMethod)) { + type.forEach(notificationType => notificationType.enabledDeliveryMethods[deliveryMethod] = value); + } else { + type.forEach(notificationType => notificationType.enabled = value); + } + this.notificationSettings.get('prefs').patchValue(type); + this.notificationSettings.markAsDirty(); + }; + + get notificationSettingsFormArray(): UntypedFormArray { + return this.notificationSettings.get('prefs') as UntypedFormArray; + } + + save(): void { + const settings = {prefs: {}}; + this.notificationSettings.getRawValue().prefs.forEach(value => { + const key = value.name; + delete value.name; + settings.prefs[key] = value; + }); + this.notificationService.saveNotificationUserSettings(settings).subscribe( + (userSettings) => { + this.notificationSettings.get('prefs').reset({}); + this.patchNotificationSettings(userSettings); + } + ); + } + + confirmForm(): UntypedFormGroup { + return this.notificationSettings; + } +} diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index ba8b4ba114..6205667245 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -89,6 +89,11 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan return this.historyOnlyValue; } + get displayTypePrefix(): boolean { + return isDefinedAndNotNull(this.computedTimewindowStyle?.displayTypePrefix) + ? this.computedTimewindowStyle?.displayTypePrefix : true; + } + @HostBinding('class.no-margin') @Input() @coerceBoolean() @@ -198,6 +203,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan if (!change.firstChange && change.currentValue !== change.previousValue) { if (propName === 'timewindowStyle') { this.updateTimewindowStyle(); + this.updateDisplayValue(); } } } @@ -316,7 +322,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan updateDisplayValue() { if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { - this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - '; + this.innerValue.displayValue = this.displayTypePrefix ? (this.translate.instant('timewindow.realtime') + ' - ') : ''; if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) { this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval)); } else { @@ -324,7 +330,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); } } else { - this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ? + this.innerValue.displayValue = this.displayTypePrefix && (!this.historyOnly || this.alwaysDisplayTypePrefix) ? (this.translate.instant('timewindow.history') + ' - ') : ''; if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 047ec7b38d..7de754dadf 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -612,3 +612,12 @@ export const TriggerTypeTranslationMap = new Map([ [TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'], [TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'], ]); + +export interface NotificationUserSettings { + prefs: {[key: string]: NotificationUserSetting}; +} + +export interface NotificationUserSetting { + enabled: boolean; + enabledDeliveryMethods: {[key: string]: boolean}; +} 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 18782cdd93..fb15622754 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts @@ -17,7 +17,7 @@ import { isDefinedAndNotNull, isNumber, isNumeric, parseFunction } from '@core/utils'; import { DataKey, Datasource, DatasourceData } from '@shared/models/widget.models'; import { Injector } from '@angular/core'; -import { DatePipe, formatDate } from '@angular/common'; +import { DatePipe } from '@angular/common'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; import { TranslateService } from '@ngx-translate/core'; @@ -97,13 +97,15 @@ export interface TimewindowStyle { iconPosition: 'left' | 'right'; font?: Font; color?: string; + displayTypePrefix?: boolean; } export const defaultTimewindowStyle: TimewindowStyle = { showIcon: true, icon: 'query_builder', iconSize: '24px', - iconPosition: 'left' + iconPosition: 'left', + displayTypePrefix: true }; export const constantColor = (color: string): ColorSettings => ({ @@ -117,18 +119,44 @@ export const constantColor = (color: string): ColorSettings => ({ 'return \'blue\';' }); +export const cssSizeToStrSize = (size?: number, unit?: cssUnit): string => (isDefinedAndNotNull(size) ? size + '' : '0') + (unit || 'px'); + +export const resolveCssSize = (strSize?: string): [number, cssUnit] => { + if (!strSize || !strSize.trim().length) { + return [0, 'px']; + } + let resolvedUnit: cssUnit; + let resolvedSize = strSize; + for (const unit of cssUnits) { + if (strSize.endsWith(unit)) { + resolvedUnit = unit; + break; + } + } + if (resolvedUnit) { + resolvedSize = strSize.substring(0, strSize.length - resolvedUnit.length); + } + resolvedUnit = resolvedUnit || 'px'; + let numericSize = 0; + if (isNumeric(resolvedSize)) { + numericSize = Number(resolvedSize); + } + return [numericSize, resolvedUnit]; +}; + type ValueColorFunction = (value: any) => string; export abstract class ColorProcessor { static fromSettings(color: ColorSettings): ColorProcessor { - switch (color.type) { + const settings = color || constantColor('rgba(0, 0, 0, 0.87)'); + switch (settings.type) { case ColorType.constant: - return new ConstantColorProcessor(color); + return new ConstantColorProcessor(settings); case ColorType.range: - return new RangeColorProcessor(color); + return new RangeColorProcessor(settings); case ColorType.function: - return new FunctionColorProcessor(color); + return new FunctionColorProcessor(settings); } } @@ -164,13 +192,19 @@ class RangeColorProcessor extends ColorProcessor { if (this.settings.rangeList?.length && isDefinedAndNotNull(value) && isNumeric(value)) { const num = Number(value); for (const range of this.settings.rangeList) { - if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) { + if (this.constantRange(range) && range.from === num) { + return range.color; + } else if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) { return range.color; } } } return this.settings.color; } + + private constantRange(range: ColorRange): boolean { + return isNumber(range.from) && isNumber(range.to) && range.from === range.to; + } } class FunctionColorProcessor extends ColorProcessor { @@ -242,7 +276,7 @@ export abstract class DateFormatProcessor { } } - formatted = ''; + formatted = ' '; protected constructor(protected $injector: Injector, protected settings: DateFormatSettings) { @@ -412,3 +446,13 @@ export const getSingleTsValue = (data: Array): [number, any] => } return null; }; + +export const getLatestSingleTsValue = (data: Array): [number, any] => { + if (data.length) { + const dsData = data[0]; + if (dsData.data.length) { + return dsData.data[dsData.data.length - 1]; + } + } + return 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 a4d8c70363..562cc91926 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -11,7 +11,8 @@ "permission-denied-text": "You don't have permission to perform this operation!" }, "account": { - "account": "Account" + "account": "Account", + "notification-settings": "Notification settings" }, "action": { "activate": "Activate", @@ -1226,7 +1227,11 @@ "delta-calculation-result-delta-absolute": "Delta (absolute)", "delta-calculation-result-delta-percent": "Delta (percent)", "source": "Source", - "latest": "Latest" + "latest": "Latest", + "latest-value": "Latest value", + "delta": "delta", + "percent": "percent", + "absolute": "absolute" }, "datasource": { "type": "Datasource type", @@ -3154,7 +3159,17 @@ "webhook-url": "Webhook URL", "webhook-url-required": "Webhook URL is required", "channel-name": "Channel name", - "channel-name-required": "Channel name is required" + "channel-name-required": "Channel name is required", + "settings": { + "notification-settings": "Notification settings", + "reset-all": "Reset all settings", + "reset-all-title": "Are you sure you want to reset form?", + "reset-all-text": "After the confirmation, the settings form will reset to the default value and save.", + "type": "Type", + "enable-all": "Enable all", + "disable-all": "Disable all", + "delivery-not-configured": "Delivery method is not configured" + } }, "ota-update": { "add": "Add package", @@ -3938,6 +3953,7 @@ "icon-position-right": "Right", "font": "Font", "color": "Color", + "displayTypePrefix": "Display Realtime/History prefix", "preview": "Preview" }, "unit": { @@ -5745,6 +5761,25 @@ "date": "Date", "value-card-style": "Value card style" }, + "aggregated-value-card": { + "subtitle": "Subtitle", + "chart": "Chart", + "values": "Values", + "value-appearance": "Value appearance", + "position": "Position", + "position-center": "Center", + "position-right-top": "Right top", + "position-right-bottom": "Right bottom", + "position-left-top": "Left top", + "position-left-bottom": "Left bottom", + "font": "Font", + "color": "Color", + "display-up-down-arrow": "Display Up/Down arrow", + "add-value": "Add value", + "remove-value": "Remove value", + "no-values": "No values configured", + "aggregation": "Aggregation" + }, "table": { "common-table-settings": "Common Table Settings", "enable-search": "Enable search", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 8bf8aef2ab..c5115add1b 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -122,18 +122,6 @@ font: inherit; } } - .mat-slide { - margin: 0; - &.margin { - margin: 8px 0; - } - .mdc-form-field>label { - font-weight: 400; - font-size: 16px; - line-height: 24px; - margin-left: 12px; - } - } } .tb-form-panel-title { @@ -200,6 +188,21 @@ } } + .tb-form-panel, .tb-form-row { + .mat-slide { + margin: 0; + &.margin { + margin: 8px 0; + } + .mdc-form-field>label { + font-weight: 400; + font-size: 16px; + line-height: 24px; + margin-left: 12px; + } + } + } + .tb-form-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field { &.mat-form-field-appearance-fill { .mdc-text-field--filled:not(.mdc-text-field--disabled) { @@ -397,12 +400,18 @@ } } - .tb-form-table-row { - &.tb-draggable { - gap: 0; - padding-left: 0; - background: #fff; + .tb-draggable-form-table-row { + background: #fff; + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + .tb-form-table-row { + padding-right: 0; } + } + + .tb-form-table-row { &-cell { color: rgba(0, 0, 0, 0.87);