From 0f85e164462821f22854310fe53360854b2d31a2 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Wed, 24 May 2017 10:39:33 +0300 Subject: [PATCH] Asset and Relations management, Dashboard states and layouts management --- .../server/actors/ActorSystemContext.java | 4 + .../plugin/PluginProcessingContext.java | 199 +++-- .../plugin/SharedPluginProcessingContext.java | 17 +- .../actors/plugin/ValidationCallback.java | 2 +- .../actors/service/DefaultActorService.java | 2 +- .../actors/session/ASyncMsgProcessor.java | 2 +- .../AbstractSessionActorMsgProcessor.java | 4 +- .../server/controller/AssetController.java | 234 ++++++ .../server/controller/BaseController.java | 104 ++- .../controller/DashboardController.java | 32 + .../server/controller/DeviceController.java | 29 +- .../controller/EntityRelationController.java | 194 +++++ .../server/controller/EventController.java | 24 +- .../routing/ClusterRoutingService.java | 6 +- .../ConsistentClusterRoutingService.java | 15 +- .../src/main/resources/thingsboard.yml | 4 +- .../actors/DefaultActorServiceTest.java | 4 +- .../server/common/data/Device.java | 18 + .../server/common/data/EntityType.java | 2 +- .../server/common/data/alarm/Alarm.java | 40 + .../server/common/data/alarm/AlarmId.java | 45 + .../server/common/data/alarm/AlarmQuery.java | 32 + .../common/data/alarm/AlarmSeverity.java | 25 + .../server/common/data/alarm/AlarmStatus.java | 25 + .../server/common/data/asset/Asset.java | 166 ++++ .../server/common/data/id/AssetId.java | 43 + .../server/common/data/id/DashboardId.java | 16 +- .../server/common/data/id/EntityId.java | 5 + .../common/data/id/EntityIdDeserializer.java | 43 + .../common/data/id/EntityIdFactory.java | 56 ++ .../common/data/id/EntityIdSerializer.java | 37 + .../server/common/data/id/UserId.java | 21 +- .../common/data/relation/EntityRelation.java | 103 +++ .../server/dao/alarm/AlarmDao.java | 22 + .../server/dao/alarm/AlarmService.java | 41 + .../server/dao/asset/AssetDao.java | 90 ++ .../server/dao/asset/AssetDaoImpl.java | 104 +++ .../server/dao/asset/AssetSearchQuery.java | 50 ++ .../server/dao/asset/AssetService.java | 59 ++ .../server/dao/asset/AssetTypeFilter.java | 32 + .../server/dao/asset/BaseAssetService.java | 292 +++++++ .../server/dao/customer/CustomerService.java | 17 +- .../dao/customer/CustomerServiceImpl.java | 22 +- .../dao/dashboard/DashboardService.java | 4 +- .../dao/dashboard/DashboardServiceImpl.java | 14 +- .../server/dao/device/DeviceSearchQuery.java | 46 ++ .../server/dao/device/DeviceService.java | 3 + .../server/dao/device/DeviceServiceImpl.java | 46 +- .../server/dao/entity/BaseEntityService.java | 37 + .../server/dao/model/AssetEntity.java | 235 ++++++ .../server/dao/model/DeviceEntity.java | 23 +- .../server/dao/model/EventEntity.java | 20 +- .../server/dao/model/ModelConstants.java | 27 + .../server/dao/plugin/BasePluginService.java | 18 +- .../server/dao/plugin/PluginService.java | 3 + .../server/dao/relation/BaseRelationDao.java | 279 +++++++ .../dao/relation/BaseRelationService.java | 257 ++++++ .../dao/relation/EntityRelationsQuery.java | 31 + .../dao/relation/EntitySearchDirection.java | 25 + .../server/dao/relation/EntityTypeFilter.java | 35 + .../server/dao/relation/RelationDao.java | 47 ++ .../server/dao/relation/RelationService.java | 49 ++ .../relation/RelationsSearchParameters.java | 48 ++ .../server/dao/rule/BaseRuleService.java | 15 +- .../server/dao/rule/RuleService.java | 3 + .../server/dao/service/Validator.java | 14 + .../server/dao/tenant/TenantService.java | 15 +- .../server/dao/tenant/TenantServiceImpl.java | 34 +- .../dao/timeseries/BaseTimeseriesDao.java | 77 +- .../dao/timeseries/BaseTimeseriesService.java | 44 +- .../server/dao/timeseries/TimeseriesDao.java | 13 +- .../dao/timeseries/TimeseriesService.java | 11 +- .../server/dao/user/UserServiceImpl.java | 6 +- dao/src/main/resources/schema.cql | 51 ++ .../dao/service/AbstractServiceTest.java | 4 + .../dao/service/RelationServiceImplTest.java | 283 +++++++ .../dao/timeseries/TimeseriesServiceTest.java | 26 +- .../api/exception/UnauthorizedException.java | 5 + .../extensions/api/plugins/PluginContext.java | 26 +- extensions-core/pom.xml | 5 + .../plugin/telemetry/SubscriptionManager.java | 78 +- .../plugin/telemetry/cmd/GetHistoryCmd.java | 3 +- .../plugin/telemetry/cmd/SubscriptionCmd.java | 5 +- .../telemetry/handlers/TelemetryFeature.java | 29 + .../handlers/TelemetryRestMsgHandler.java | 287 ++++--- .../handlers/TelemetryRpcMsgHandler.java | 110 ++- .../TelemetryWebsocketMsgHandler.java | 58 +- .../plugin/telemetry/sub/Subscription.java | 5 +- .../telemetry/sub/SubscriptionState.java | 11 +- .../src/main/proto/telemetry.proto | 24 +- .../mqtt/session/GatewaySessionCtx.java | 7 +- ui/src/app/api/asset.service.js | 261 ++++++ ui/src/app/api/attribute.service.js | 282 +++++++ ui/src/app/api/dashboard.service.js | 35 +- ui/src/app/api/datasource.service.js | 25 +- ui/src/app/api/device.service.js | 382 +-------- ui/src/app/api/entity-relation.service.js | 136 +++ ui/src/app/api/entity.service.js | 777 ++++++++++++++++++ ui/src/app/api/subscription.js | 123 +-- ui/src/app/api/user.service.js | 15 +- ui/src/app/app.config.js | 2 +- ui/src/app/app.js | 10 + ui/src/app/asset/add-asset.tpl.html | 45 + .../add-assets-to-customer.controller.js | 123 +++ .../app/asset/add-assets-to-customer.tpl.html | 77 ++ ui/src/app/asset/asset-card.tpl.html | 19 + ui/src/app/asset/asset-fieldset.tpl.html | 71 ++ ui/src/app/asset/asset.controller.js | 506 ++++++++++++ ui/src/app/asset/asset.directive.js | 71 ++ ui/src/app/asset/asset.routes.js | 68 ++ ui/src/app/asset/assets.tpl.html | 58 ++ .../asset/assign-to-customer.controller.js | 123 +++ ui/src/app/asset/assign-to-customer.tpl.html | 76 ++ ui/src/app/asset/index.js | 43 + ui/src/app/common/dashboard-utils.service.js | 437 ++++++++++ ui/src/app/common/types.constant.js | 22 +- ui/src/app/common/utils.service.js | 162 +--- .../dashboard-autocomplete.directive.js | 13 +- ui/src/app/components/dashboard.directive.js | 191 ++++- ui/src/app/components/dashboard.tpl.html | 7 +- .../datakey-config-dialog.controller.js | 12 +- .../components/datakey-config-dialog.tpl.html | 4 +- .../components/datakey-config.directive.js | 8 +- .../components/datasource-entity.directive.js | 249 ++++++ ui/src/app/components/datasource-entity.scss | 44 + .../app/components/datasource-entity.tpl.html | 137 +++ .../components/datasource-func.directive.js | 4 +- ui/src/app/components/datasource.directive.js | 12 +- ui/src/app/components/datasource.scss | 2 +- ui/src/app/components/datasource.tpl.html | 14 +- .../entity-alias-select.directive.js | 151 ++++ .../app/components/entity-alias-select.scss | 29 + .../components/entity-alias-select.tpl.html | 53 ++ .../related-entity-autocomplete.directive.js | 128 +++ .../related-entity-autocomplete.scss | 30 + .../related-entity-autocomplete.tpl.html | 43 + .../app/components/widget-config.directive.js | 217 ++--- ui/src/app/components/widget-config.tpl.html | 15 +- ui/src/app/components/widget.controller.js | 26 +- .../app/customer/customer-fieldset.tpl.html | 11 + ui/src/app/customer/customer.controller.js | 26 +- ui/src/app/customer/customer.directive.js | 7 +- ui/src/app/customer/customers.tpl.html | 44 +- ui/src/app/dashboard/add-widget.controller.js | 53 +- ui/src/app/dashboard/add-widget.tpl.html | 6 +- .../dashboard-settings.controller.js | 70 +- .../app/dashboard/dashboard-settings.tpl.html | 215 ++--- ui/src/app/dashboard/dashboard.controller.js | 743 +++++++++++------ ui/src/app/dashboard/dashboard.routes.js | 6 +- ui/src/app/dashboard/dashboard.scss | 36 +- ui/src/app/dashboard/dashboard.tpl.html | 241 +++--- ui/src/app/dashboard/edit-widget.directive.js | 49 +- ui/src/app/dashboard/edit-widget.tpl.html | 6 +- ui/src/app/dashboard/index.js | 18 +- .../layouts/dashboard-layout.directive.js | 277 +++++++ .../layouts/dashboard-layout.tpl.html | 69 ++ ui/src/app/dashboard/layouts/index.js | 25 + .../manage-dashboard-layouts.controller.js | 83 ++ .../layouts/manage-dashboard-layouts.tpl.html | 65 ++ .../select-target-layout.controller.js | 33 + .../layouts/select-target-layout.tpl.html | 48 ++ .../dashboard-state-dialog.controller.js | 82 ++ .../states/dashboard-state-dialog.tpl.html | 72 ++ .../states/default-state-controller.js | 181 ++++ .../states/default-state-controller.tpl.html | 22 + .../states/entity-state-controller.js | 243 ++++++ .../states/entity-state-controller.scss | 33 + .../states/entity-state-controller.tpl.html | 33 + ui/src/app/dashboard/states/index.js | 29 + .../manage-dashboard-states.controller.js | 198 +++++ .../states/manage-dashboard-states.scss | 34 + .../states/manage-dashboard-states.tpl.html | 127 +++ .../states/select-target-state.controller.js | 35 + .../states/select-target-state.tpl.html | 50 ++ .../states/states-component.directive.js | 117 +++ .../states/states-controller.service.js | 60 ++ ui/src/app/device/devices.tpl.html | 12 +- ui/src/app/device/index.js | 6 - .../aliases-entity-select-button.tpl.html | 32 + .../aliases-entity-select-panel.controller.js | 31 + .../aliases-entity-select-panel.tpl.html | 33 + .../entity/aliases-entity-select.directive.js | 154 ++++ ui/src/app/entity/aliases-entity-select.scss | 46 ++ .../add-attribute-dialog.controller.js | 50 ++ .../attribute/add-attribute-dialog.tpl.html | 95 +++ ...d-widget-to-dashboard-dialog.controller.js | 159 ++++ .../add-widget-to-dashboard-dialog.tpl.html | 81 ++ .../attribute/attribute-table.directive.js | 433 ++++++++++ .../app/entity/attribute/attribute-table.scss | 50 ++ .../entity/attribute/attribute-table.tpl.html | 210 +++++ .../edit-attribute-value.controller.js | 71 ++ .../attribute/edit-attribute-value.tpl.html | 72 ++ .../app/entity/entity-aliases.controller.js | 221 +++++ ui/src/app/entity/entity-aliases.scss | 28 + ui/src/app/entity/entity-aliases.tpl.html | 106 +++ ui/src/app/entity/entity-filter.directive.js | 234 ++++++ ui/src/app/entity/entity-filter.scss | 45 + ui/src/app/entity/entity-filter.tpl.html | 67 ++ .../entity/entity-type-select.directive.js | 120 +++ ui/src/app/entity/entity-type-select.scss | 18 + ui/src/app/entity/entity-type-select.tpl.html | 25 + ui/src/app/entity/index.js | 35 + ui/src/app/global-interceptor.service.js | 2 +- ui/src/app/help/help-links.constant.js | 1 + .../import-export/import-export.service.js | 200 +++-- ui/src/app/layout/index.js | 4 + ui/src/app/locale/locale.constant.js | 157 +++- ui/src/app/locale/translate-handler.js | 26 + ui/src/app/plugin/plugin-fieldset.tpl.html | 10 + ui/src/app/plugin/plugin.directive.js | 6 +- ui/src/app/plugin/plugins.tpl.html | 17 + ui/src/app/rule/rule-fieldset.tpl.html | 10 + ui/src/app/rule/rule.directive.js | 7 +- ui/src/app/rule/rules.tpl.html | 17 + ui/src/app/services/item-buffer.service.js | 272 +++--- ui/src/app/services/menu.service.js | 32 + ui/src/app/tenant/tenant-fieldset.tpl.html | 10 + ui/src/app/tenant/tenant.controller.js | 4 +- ui/src/app/tenant/tenant.directive.js | 7 +- ui/src/app/tenant/tenants.tpl.html | 40 +- ui/src/app/user/user-fieldset.tpl.html | 13 +- ui/src/app/user/user.controller.js | 12 +- ui/src/app/user/user.directive.js | 4 + .../app/widget/widget-library.controller.js | 6 +- ui/src/scss/main.scss | 18 +- 225 files changed, 14890 insertions(+), 1982 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/AssetController.java create mode 100644 application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java create mode 100644 extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java create mode 100644 ui/src/app/api/asset.service.js create mode 100644 ui/src/app/api/attribute.service.js create mode 100644 ui/src/app/api/entity-relation.service.js create mode 100644 ui/src/app/api/entity.service.js create mode 100644 ui/src/app/asset/add-asset.tpl.html create mode 100644 ui/src/app/asset/add-assets-to-customer.controller.js create mode 100644 ui/src/app/asset/add-assets-to-customer.tpl.html create mode 100644 ui/src/app/asset/asset-card.tpl.html create mode 100644 ui/src/app/asset/asset-fieldset.tpl.html create mode 100644 ui/src/app/asset/asset.controller.js create mode 100644 ui/src/app/asset/asset.directive.js create mode 100644 ui/src/app/asset/asset.routes.js create mode 100644 ui/src/app/asset/assets.tpl.html create mode 100644 ui/src/app/asset/assign-to-customer.controller.js create mode 100644 ui/src/app/asset/assign-to-customer.tpl.html create mode 100644 ui/src/app/asset/index.js create mode 100644 ui/src/app/common/dashboard-utils.service.js create mode 100644 ui/src/app/components/datasource-entity.directive.js create mode 100644 ui/src/app/components/datasource-entity.scss create mode 100644 ui/src/app/components/datasource-entity.tpl.html create mode 100644 ui/src/app/components/entity-alias-select.directive.js create mode 100644 ui/src/app/components/entity-alias-select.scss create mode 100644 ui/src/app/components/entity-alias-select.tpl.html create mode 100644 ui/src/app/components/related-entity-autocomplete.directive.js create mode 100644 ui/src/app/components/related-entity-autocomplete.scss create mode 100644 ui/src/app/components/related-entity-autocomplete.tpl.html create mode 100644 ui/src/app/dashboard/layouts/dashboard-layout.directive.js create mode 100644 ui/src/app/dashboard/layouts/dashboard-layout.tpl.html create mode 100644 ui/src/app/dashboard/layouts/index.js create mode 100644 ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js create mode 100644 ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html create mode 100644 ui/src/app/dashboard/layouts/select-target-layout.controller.js create mode 100644 ui/src/app/dashboard/layouts/select-target-layout.tpl.html create mode 100644 ui/src/app/dashboard/states/dashboard-state-dialog.controller.js create mode 100644 ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html create mode 100644 ui/src/app/dashboard/states/default-state-controller.js create mode 100644 ui/src/app/dashboard/states/default-state-controller.tpl.html create mode 100644 ui/src/app/dashboard/states/entity-state-controller.js create mode 100644 ui/src/app/dashboard/states/entity-state-controller.scss create mode 100644 ui/src/app/dashboard/states/entity-state-controller.tpl.html create mode 100644 ui/src/app/dashboard/states/index.js create mode 100644 ui/src/app/dashboard/states/manage-dashboard-states.controller.js create mode 100644 ui/src/app/dashboard/states/manage-dashboard-states.scss create mode 100644 ui/src/app/dashboard/states/manage-dashboard-states.tpl.html create mode 100644 ui/src/app/dashboard/states/select-target-state.controller.js create mode 100644 ui/src/app/dashboard/states/select-target-state.tpl.html create mode 100644 ui/src/app/dashboard/states/states-component.directive.js create mode 100644 ui/src/app/dashboard/states/states-controller.service.js create mode 100644 ui/src/app/entity/aliases-entity-select-button.tpl.html create mode 100644 ui/src/app/entity/aliases-entity-select-panel.controller.js create mode 100644 ui/src/app/entity/aliases-entity-select-panel.tpl.html create mode 100644 ui/src/app/entity/aliases-entity-select.directive.js create mode 100644 ui/src/app/entity/aliases-entity-select.scss create mode 100644 ui/src/app/entity/attribute/add-attribute-dialog.controller.js create mode 100644 ui/src/app/entity/attribute/add-attribute-dialog.tpl.html create mode 100644 ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js create mode 100644 ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html create mode 100644 ui/src/app/entity/attribute/attribute-table.directive.js create mode 100644 ui/src/app/entity/attribute/attribute-table.scss create mode 100644 ui/src/app/entity/attribute/attribute-table.tpl.html create mode 100644 ui/src/app/entity/attribute/edit-attribute-value.controller.js create mode 100644 ui/src/app/entity/attribute/edit-attribute-value.tpl.html create mode 100644 ui/src/app/entity/entity-aliases.controller.js create mode 100644 ui/src/app/entity/entity-aliases.scss create mode 100644 ui/src/app/entity/entity-aliases.tpl.html create mode 100644 ui/src/app/entity/entity-filter.directive.js create mode 100644 ui/src/app/entity/entity-filter.scss create mode 100644 ui/src/app/entity/entity-filter.tpl.html create mode 100644 ui/src/app/entity/entity-type-select.directive.js create mode 100644 ui/src/app/entity/entity-type-select.scss create mode 100644 ui/src/app/entity/entity-type-select.tpl.html create mode 100644 ui/src/app/entity/index.js create mode 100644 ui/src/app/locale/translate-handler.js diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 7cc2c9f84f..ae9d9677c7 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; @@ -80,6 +81,9 @@ public class ActorSystemContext { @Autowired @Getter private DeviceService deviceService; + @Autowired + @Getter private AssetService assetService; + @Autowired @Getter private TenantService tenantService; diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java index 876b52535b..5dbc5f4244 100644 --- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java @@ -17,7 +17,6 @@ package org.thingsboard.server.actors.plugin; import java.io.IOException; import java.util.*; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -30,15 +29,19 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvQuery; -import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.plugin.PluginMetaData; +import org.thingsboard.server.common.data.rule.RuleMetaData; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext; @@ -53,7 +56,6 @@ import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRe import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; import akka.actor.ActorRef; -import org.w3c.dom.Attr; import javax.annotation.Nullable; @@ -91,103 +93,107 @@ public final class PluginProcessingContext implements PluginContext { } @Override - public void saveAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List attributes, final PluginCallback callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes); + public void saveAttributes(final TenantId tenantId, final EntityId entityId, final String scope, final List attributes, final PluginCallback callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> rsListFuture = pluginCtx.attributesService.save(entityId, scope, attributes); Futures.addCallback(rsListFuture, getListCallback(callback, v -> { - onDeviceAttributesChanged(tenantId, deviceId, scope, attributes); + if (entityId.getEntityType() == EntityType.DEVICE) { + onDeviceAttributesChanged(tenantId, new DeviceId(entityId.getId()), scope, attributes); + } return null; }), executor); })); } @Override - public void removeAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List keys, final PluginCallback callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> future = pluginCtx.attributesService.removeAll(deviceId, scope, keys); + public void removeAttributes(final TenantId tenantId, final EntityId entityId, final String scope, final List keys, final PluginCallback callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> future = pluginCtx.attributesService.removeAll(entityId, scope, keys); Futures.addCallback(future, getCallback(callback, v -> null), executor); - onDeviceAttributesDeleted(tenantId, deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet())); + if (entityId.getEntityType() == EntityType.DEVICE) { + onDeviceAttributesDeleted(tenantId, new DeviceId(entityId.getId()), keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet())); + } })); } @Override - public void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey); + public void loadAttribute(EntityId entityId, String attributeType, String attributeKey, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> future = pluginCtx.attributesService.find(entityId, attributeType, attributeKey); Futures.addCallback(future, getCallback(callback, v -> v), executor); })); } @Override - public void loadAttributes(DeviceId deviceId, String attributeType, Collection attributeKeys, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys); + public void loadAttributes(EntityId entityId, String attributeType, Collection attributeKeys, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> future = pluginCtx.attributesService.find(entityId, attributeType, attributeKeys); Futures.addCallback(future, getCallback(callback, v -> v), executor); })); } @Override - public void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> future = pluginCtx.attributesService.findAll(deviceId, attributeType); + public void loadAttributes(EntityId entityId, String attributeType, PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> future = pluginCtx.attributesService.findAll(entityId, attributeType); Futures.addCallback(future, getCallback(callback, v -> v), executor); })); } @Override - public void loadAttributes(final DeviceId deviceId, final Collection attributeTypes, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { + public void loadAttributes(final EntityId entityId, final Collection attributeTypes, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { List>> futures = new ArrayList<>(); - attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.findAll(deviceId, attributeType))); + attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.findAll(entityId, attributeType))); convertFuturesAndAddCallback(callback, futures); })); } @Override - public void loadAttributes(final DeviceId deviceId, final Collection attributeTypes, final Collection attributeKeys, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { + public void loadAttributes(final EntityId entityId, final Collection attributeTypes, final Collection attributeKeys, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { List>> futures = new ArrayList<>(); - attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys))); + attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.find(entityId, attributeType, attributeKeys))); convertFuturesAndAddCallback(callback, futures); })); } @Override - public void saveTsData(final DeviceId deviceId, final TsKvEntry entry, final PluginCallback callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry); + public void saveTsData(final EntityId entityId, final TsKvEntry entry, final PluginCallback callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> rsListFuture = pluginCtx.tsService.save(entityId, entry); Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor); })); } @Override - public void saveTsData(final DeviceId deviceId, final List entries, final PluginCallback callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries); + public void saveTsData(final EntityId entityId, final List entries, final PluginCallback callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> rsListFuture = pluginCtx.tsService.save(entityId, entries); Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor); })); } @Override - public void loadTimeseries(final DeviceId deviceId, final List queries, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> future = pluginCtx.tsService.findAll(DataConstants.DEVICE, deviceId, queries); + public void loadTimeseries(final EntityId entityId, final List queries, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> future = pluginCtx.tsService.findAll(entityId, queries); Futures.addCallback(future, getCallback(callback, v -> v), executor); })); } @Override - public void loadLatestTimeseries(final DeviceId deviceId, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId); + public void loadLatestTimeseries(final EntityId entityId, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ResultSetFuture future = pluginCtx.tsService.findAllLatest(entityId); Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor); })); } @Override - public void loadLatestTimeseries(final DeviceId deviceId, final Collection keys, final PluginCallback> callback) { - validate(deviceId, new ValidationCallback(callback, ctx -> { - ListenableFuture> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys); + public void loadLatestTimeseries(final EntityId entityId, final Collection keys, final PluginCallback> callback) { + validate(entityId, new ValidationCallback(callback, ctx -> { + ListenableFuture> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys); Futures.addCallback(rsListFuture, getListCallback(callback, rsList -> { List result = new ArrayList<>(); @@ -270,24 +276,101 @@ public final class PluginProcessingContext implements PluginContext { validate(deviceId, new ValidationCallback(callback, ctx -> callback.onSuccess(ctx, null))); } - private void validate(DeviceId deviceId, ValidationCallback callback) { + private void validate(EntityId entityId, ValidationCallback callback) { if (securityCtx.isPresent()) { final PluginApiCallSecurityContext ctx = securityCtx.get(); - if (ctx.isTenantAdmin() || ctx.isCustomerUser()) { - ListenableFuture deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(deviceId); - Futures.addCallback(deviceFuture, getCallback(callback, device -> { - if (device == null) { - return Boolean.FALSE; - } else { - if (!device.getTenantId().equals(ctx.getTenantId())) { - return Boolean.FALSE; - } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) { - return Boolean.FALSE; + if (ctx.isTenantAdmin() || ctx.isCustomerUser() || ctx.isSystemAdmin()) { + switch (entityId.getEntityType()) { + case DEVICE: + if (ctx.isSystemAdmin()) { + callback.onSuccess(this, Boolean.FALSE); } else { - return Boolean.TRUE; + ListenableFuture deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId())); + Futures.addCallback(deviceFuture, getCallback(callback, device -> { + if (device == null) { + return Boolean.FALSE; + } else { + if (!device.getTenantId().equals(ctx.getTenantId())) { + return Boolean.FALSE; + } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) { + return Boolean.FALSE; + } else { + return Boolean.TRUE; + } + } + })); } - } - })); + return; + case ASSET: + if (ctx.isSystemAdmin()) { + callback.onSuccess(this, Boolean.FALSE); + } else { + ListenableFuture assetFuture = pluginCtx.assetService.findAssetByIdAsync(new AssetId(entityId.getId())); + Futures.addCallback(assetFuture, getCallback(callback, asset -> { + if (asset == null) { + return Boolean.FALSE; + } else { + if (!asset.getTenantId().equals(ctx.getTenantId())) { + return Boolean.FALSE; + } else if (ctx.isCustomerUser() && !asset.getCustomerId().equals(ctx.getCustomerId())) { + return Boolean.FALSE; + } else { + return Boolean.TRUE; + } + } + })); + } + return; + case RULE: + if (ctx.isCustomerUser()) { + callback.onSuccess(this, Boolean.FALSE); + } else { + ListenableFuture ruleFuture = pluginCtx.ruleService.findRuleByIdAsync(new RuleId(entityId.getId())); + Futures.addCallback(ruleFuture, getCallback(callback, rule -> rule != null && rule.getTenantId().equals(ctx.getTenantId()))); + } + return; + case PLUGIN: + if (ctx.isCustomerUser()) { + callback.onSuccess(this, Boolean.FALSE); + } else { + ListenableFuture pluginFuture = pluginCtx.pluginService.findPluginByIdAsync(new PluginId(entityId.getId())); + Futures.addCallback(pluginFuture, getCallback(callback, plugin -> plugin != null && plugin.getTenantId().equals(ctx.getTenantId()))); + } + return; + case CUSTOMER: + if (ctx.isSystemAdmin()) { + callback.onSuccess(this, Boolean.FALSE); + } else { + ListenableFuture customerFuture = pluginCtx.customerService.findCustomerByIdAsync(new CustomerId(entityId.getId())); + Futures.addCallback(customerFuture, getCallback(callback, customer -> { + if (customer == null) { + return Boolean.FALSE; + } else { + if (!customer.getTenantId().equals(ctx.getTenantId())) { + return Boolean.FALSE; + } else if (ctx.isCustomerUser() && !customer.getId().equals(ctx.getCustomerId())) { + return Boolean.FALSE; + } else { + return Boolean.TRUE; + } + } + })); + } + return; + case TENANT: + if (ctx.isCustomerUser()) { + callback.onSuccess(this, Boolean.FALSE); + } else if (ctx.isSystemAdmin()) { + callback.onSuccess(this, Boolean.TRUE); + } else { + ListenableFuture tenantFuture = pluginCtx.tenantService.findTenantByIdAsync(new TenantId(entityId.getId())); + Futures.addCallback(tenantFuture, getCallback(callback, tenant -> tenant != null && tenant.getId().equals(ctx.getTenantId()))); + } + return; + default: + //TODO: add support of other entities + throw new IllegalStateException("Not Implemented!"); + } } else { callback.onSuccess(this, Boolean.FALSE); } @@ -297,8 +380,8 @@ public final class PluginProcessingContext implements PluginContext { } @Override - public Optional resolve(DeviceId deviceId) { - return pluginCtx.routingService.resolve(deviceId); + public Optional resolve(EntityId entityId) { + return pluginCtx.routingService.resolveById(entityId); } @Override diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java index b09b72e74b..43ddce6105 100644 --- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java @@ -25,8 +25,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; import org.thingsboard.server.common.data.id.PluginId; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.plugin.PluginService; +import org.thingsboard.server.dao.rule.RuleService; +import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg; @@ -46,7 +51,12 @@ public final class SharedPluginProcessingContext { final ActorRef currentActor; final ActorSystemContext systemContext; final PluginWebSocketMsgEndpoint msgEndpoint; + final AssetService assetService; final DeviceService deviceService; + final RuleService ruleService; + final PluginService pluginService; + final CustomerService customerService; + final TenantService tenantService; final TimeseriesService tsService; final AttributesService attributesService; final ClusterRpcService rpcService; @@ -65,9 +75,14 @@ public final class SharedPluginProcessingContext { this.msgEndpoint = sysContext.getWsMsgEndpoint(); this.tsService = sysContext.getTsService(); this.attributesService = sysContext.getAttributesService(); + this.assetService = sysContext.getAssetService(); this.deviceService = sysContext.getDeviceService(); this.rpcService = sysContext.getRpcService(); this.routingService = sysContext.getRoutingService(); + this.ruleService = sysContext.getRuleService(); + this.pluginService = sysContext.getPluginService(); + this.customerService = sysContext.getCustomerService(); + this.tenantService = sysContext.getTenantService(); } public PluginId getPluginId() { @@ -89,7 +104,7 @@ public final class SharedPluginProcessingContext { } private void forward(DeviceId deviceId, T msg, BiConsumer rpcFunction) { - Optional instance = routingService.resolve(deviceId); + Optional instance = routingService.resolveById(deviceId); if (instance.isPresent()) { log.trace("[{}] Forwarding msg {} to remote device actor!", pluginId, msg); rpcFunction.accept(instance.get(), msg); diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java b/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java index 707afa50dd..c8d79b41b2 100644 --- a/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java +++ b/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java @@ -38,7 +38,7 @@ public class ValidationCallback implements PluginCallback { if (value) { action.accept(ctx); } else { - onFailure(ctx, new UnauthorizedException()); + onFailure(ctx, new UnauthorizedException("Permission denied.")); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java index 54160bc0a8..65d2c69321 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java @@ -230,7 +230,7 @@ public class DefaultActorService implements ActorService { @Override public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) { DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId); - Optional address = actorContext.getRoutingService().resolve(deviceId); + Optional address = actorContext.getRoutingService().resolveById(deviceId); if (address.isPresent()) { rpcService.tell(address.get(), msg); } else { diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java index f806687056..b451d773f5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java @@ -116,7 +116,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor { @Override public void processClusterEvent(ActorContext context, ClusterEventMsg msg) { if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) { - Optional newTargetServer = systemContext.getRoutingService().resolve(getDeviceId()); + Optional newTargetServer = systemContext.getRoutingService().resolveById(getDeviceId()); if (!newTargetServer.equals(currentTargetServer)) { firstMsg = true; currentTargetServer = newTargetServer; diff --git a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java index b8d13ec4c5..483c034d72 100644 --- a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java @@ -81,13 +81,13 @@ abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgP } protected Optional forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) { - Optional address = systemContext.getRoutingService().resolve(toForward.getDeviceId()); + Optional address = systemContext.getRoutingService().resolveById(toForward.getDeviceId()); forwardToAppActor(ctx, toForward, address); return address; } protected Optional forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional oldAddress) { - Optional newAddress = systemContext.getRoutingService().resolve(toForward.getDeviceId()); + Optional newAddress = systemContext.getRoutingService().resolveById(toForward.getDeviceId()); if (!newAddress.equals(oldAddress)) { if (newAddress.isPresent()) { systemContext.getRpcService().tell(newAddress.get(), diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java new file mode 100644 index 0000000000..24497d461f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -0,0 +1,234 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.asset.Asset; +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.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.asset.AssetSearchQuery; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.exception.ThingsboardException; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api") +public class AssetController extends BaseController { + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.GET) + @ResponseBody + public Asset getAssetById(@PathVariable("assetId") String strAssetId) throws ThingsboardException { + checkParameter("assetId", strAssetId); + try { + AssetId assetId = new AssetId(toUUID(strAssetId)); + return checkAssetId(assetId); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/asset", method = RequestMethod.POST) + @ResponseBody + public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException { + try { + asset.setTenantId(getCurrentUser().getTenantId()); + return checkNotNull(assetService.saveAsset(asset)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteAsset(@PathVariable("assetId") String strAssetId) throws ThingsboardException { + checkParameter("assetId", strAssetId); + try { + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId); + assetService.deleteAsset(assetId); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/{customerId}/asset/{assetId}", method = RequestMethod.POST) + @ResponseBody + public Asset assignAssetToCustomer(@PathVariable("customerId") String strCustomerId, + @PathVariable("assetId") String strAssetId) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + checkParameter("assetId", strAssetId); + try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId); + + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId); + + return checkNotNull(assetService.assignAssetToCustomer(assetId, customerId)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/asset/{assetId}", method = RequestMethod.DELETE) + @ResponseBody + public Asset unassignAssetFromCustomer(@PathVariable("assetId") String strAssetId) throws ThingsboardException { + checkParameter("assetId", strAssetId); + try { + AssetId assetId = new AssetId(toUUID(strAssetId)); + Asset asset = checkAssetId(assetId); + if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + throw new IncorrectParameterException("Asset isn't assigned to any customer!"); + } + return checkNotNull(assetService.unassignAssetFromCustomer(assetId)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/asset/{assetId}", method = RequestMethod.POST) + @ResponseBody + public Asset assignAssetToPublicCustomer(@PathVariable("assetId") String strAssetId) throws ThingsboardException { + checkParameter("assetId", strAssetId); + try { + AssetId assetId = new AssetId(toUUID(strAssetId)); + Asset asset = checkAssetId(assetId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId()); + return checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/assets", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TextPageData getTenantAssets( + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) + @ResponseBody + public Asset getTenantAsset( + @RequestParam String assetName) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/assets", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TextPageData getCustomerAssets( + @PathVariable("customerId") String strCustomerId, + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) + @ResponseBody + public List getAssetsByIds( + @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException { + checkArrayParameter("assetIds", strAssetIds); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + List assetIds = new ArrayList<>(); + for (String strAssetId : strAssetIds) { + assetIds.add(new AssetId(toUUID(strAssetId))); + } + ListenableFuture> assets; + if (customerId == null || customerId.isNullUid()) { + assets = assetService.findAssetsByTenantIdAndIdsAsync(tenantId, assetIds); + } else { + assets = assetService.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, assetIds); + } + return checkNotNull(assets.get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/assets", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getAssetTypes()); + checkEntityId(query.getParameters().getEntityId()); + try { + List assets = checkNotNull(assetService.findAssetsByQuery(query).get()); + assets = assets.stream().filter(asset -> { + try { + checkAsset(asset); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return assets; + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 1b6696fd68..d4adebeccc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -24,10 +24,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ExceptionHandler; import org.thingsboard.server.actors.service.ActorService; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TimePageLink; @@ -38,6 +36,7 @@ import org.thingsboard.server.common.data.rule.RuleMetaData; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -46,6 +45,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.plugin.PluginService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; @@ -80,6 +80,9 @@ public abstract class BaseController { @Autowired protected DeviceService deviceService; + @Autowired + protected AssetService assetService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -104,6 +107,9 @@ public abstract class BaseController { @Autowired protected ActorService actorService; + @Autowired + protected RelationService relationService; + @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { @@ -253,6 +259,43 @@ public abstract class BaseController { } } + protected void checkEntityId(EntityId entityId) throws ThingsboardException { + try { + checkNotNull(entityId); + validateId(entityId.getId(), "Incorrect entityId " + entityId); + switch (entityId.getEntityType()) { + case DEVICE: + checkDevice(deviceService.findDeviceById(new DeviceId(entityId.getId()))); + return; + case CUSTOMER: + checkCustomerId(new CustomerId(entityId.getId())); + return; + case TENANT: + checkTenantId(new TenantId(entityId.getId())); + return; + case PLUGIN: + checkPlugin(new PluginId(entityId.getId())); + return; + case RULE: + checkRule(new RuleId(entityId.getId())); + return; + case ASSET: + checkAsset(assetService.findAssetById(new AssetId(entityId.getId()))); + return; + case DASHBOARD: + checkDashboardId(new DashboardId(entityId.getId())); + return; + case USER: + checkUserId(new UserId(entityId.getId())); + return; + default: + throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); + } + } catch (Exception e) { + throw handleException(e, false); + } + } + Device checkDeviceId(DeviceId deviceId) throws ThingsboardException { try { validateId(deviceId, "Incorrect deviceId " + deviceId); @@ -264,7 +307,7 @@ public abstract class BaseController { } } - private void checkDevice(Device device) throws ThingsboardException { + protected void checkDevice(Device device) throws ThingsboardException { checkNotNull(device); checkTenantId(device.getTenantId()); if (device.getCustomerId() != null && !device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { @@ -272,6 +315,25 @@ public abstract class BaseController { } } + Asset checkAssetId(AssetId assetId) throws ThingsboardException { + try { + validateId(assetId, "Incorrect assetId " + assetId); + Asset asset = assetService.findAssetById(assetId); + checkAsset(asset); + return asset; + } catch (Exception e) { + throw handleException(e, false); + } + } + + protected void checkAsset(Asset asset) throws ThingsboardException { + checkNotNull(asset); + checkTenantId(asset.getTenantId()); + if (asset.getCustomerId() != null && !asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + checkCustomerId(asset.getCustomerId()); + } + } + WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, boolean modify) throws ThingsboardException { try { validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); @@ -318,14 +380,26 @@ public abstract class BaseController { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); Dashboard dashboard = dashboardService.findDashboardById(dashboardId); - checkDashboard(dashboard); + checkDashboard(dashboard, true); return dashboard; } catch (Exception e) { throw handleException(e, false); } } - private void checkDashboard(Dashboard dashboard) throws ThingsboardException { + DashboardInfo checkDashboardInfoId(DashboardId dashboardId) throws ThingsboardException { + try { + validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(dashboardId); + SecurityUser authUser = getCurrentUser(); + checkDashboard(dashboardInfo, authUser.getAuthority() != Authority.SYS_ADMIN); + return dashboardInfo; + } catch (Exception e) { + throw handleException(e, false); + } + } + + private void checkDashboard(DashboardInfo dashboard, boolean checkCustomerId) throws ThingsboardException { checkNotNull(dashboard); checkTenantId(dashboard.getTenantId()); SecurityUser authUser = getCurrentUser(); @@ -335,7 +409,8 @@ public abstract class BaseController { ThingsboardErrorCode.PERMISSION_DENIED); } } - if (dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + if (checkCustomerId && + dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { checkCustomerId(dashboard.getCustomerId()); } } @@ -387,6 +462,16 @@ public abstract class BaseController { return plugin; } + protected PluginMetaData checkPlugin(PluginId pluginId) throws ThingsboardException { + checkNotNull(pluginId); + return checkPlugin(pluginService.findPluginById(pluginId)); + } + + protected RuleMetaData checkRule(RuleId ruleId) throws ThingsboardException { + checkNotNull(ruleId); + return checkRule(ruleService.findRuleById(ruleId)); + } + protected RuleMetaData checkRule(RuleMetaData rule) throws ThingsboardException { checkNotNull(rule); SecurityUser authUser = getCurrentUser(); @@ -412,7 +497,8 @@ public abstract class BaseController { if (request.getHeader("x-forwarded-port") != null) { try { serverPort = request.getIntHeader("x-forwarded-port"); - } catch (NumberFormatException e) {} + } catch (NumberFormatException e) { + } } String baseUrl = String.format("%s://%s:%d", diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 3812610d20..2a6416ce98 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -41,6 +41,19 @@ public class DashboardController extends BaseController { return System.currentTimeMillis(); } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET) + @ResponseBody + public DashboardInfo getDashboardInfoById(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException { + checkParameter("dashboardId", strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + return checkDashboardInfoId(dashboardId); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) @ResponseBody @@ -132,6 +145,25 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = { "limit" }, method = RequestMethod.GET) + @ResponseBody + public TextPageData getTenantDashboards( + @PathVariable("tenantId") String strTenantId, + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + try { + TenantId tenantId = new TenantId(toUUID(strTenantId)); + checkTenantId(tenantId); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index bebab8b880..7cd381c57a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -24,19 +24,18 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.device.DeviceSearchQuery; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.exception.ThingsboardException; -import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.ArrayList; import java.util.List; -import java.util.UUID; +import java.util.stream.Collectors; @RestController @RequestMapping("/api") @@ -238,4 +237,28 @@ public class DeviceController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/devices", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getDeviceTypes()); + checkEntityId(query.getParameters().getEntityId()); + try { + List devices = checkNotNull(deviceService.findDevicesByQuery(query).get()); + devices = devices.stream().filter(device -> { + try { + checkDevice(device); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return devices; + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java new file mode 100644 index 0000000000..0c1fd8bf73 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -0,0 +1,194 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.exception.ThingsboardErrorCode; +import org.thingsboard.server.exception.ThingsboardException; + +import java.util.List; + + +@RestController +@RequestMapping("/api") +public class EntityRelationController extends BaseController { + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relation", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException { + try { + checkNotNull(relation); + checkEntityId(relation.getFrom()); + checkEntityId(relation.getTo()); + relationService.saveRelation(relation).get(); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"}) + @ResponseStatus(value = HttpStatus.OK) + public void deleteRelation(@RequestParam("fromId") String strFromId, + @RequestParam("fromType") String strFromType, @RequestParam("relationType") String strRelationType, + @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException { + checkParameter("fromId", strFromId); + checkParameter("fromType", strFromType); + checkParameter("relationType", strRelationType); + checkParameter("toId", strToId); + checkParameter("toType", strToType); + EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); + EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId); + checkEntityId(fromId); + checkEntityId(toId); + try { + Boolean found = relationService.deleteRelation(fromId, toId, strRelationType).get(); + if (!found) { + throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"}) + @ResponseStatus(value = HttpStatus.OK) + public void deleteRelations(@RequestParam("entityId") String strId, + @RequestParam("entityType") String strType) throws ThingsboardException { + checkParameter("entityId", strId); + checkParameter("entityType", strType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId); + checkEntityId(entityId); + try { + relationService.deleteEntityRelations(entityId).get(); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"}) + @ResponseStatus(value = HttpStatus.OK) + public void checkRelation(@RequestParam("fromId") String strFromId, + @RequestParam("fromType") String strFromType, @RequestParam("relationType") String strRelationType, + @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException { + try { + checkParameter("fromId", strFromId); + checkParameter("fromType", strFromType); + checkParameter("relationType", strRelationType); + checkParameter("toId", strToId); + checkParameter("toType", strToType); + EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); + EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId); + checkEntityId(fromId); + checkEntityId(toId); + Boolean found = relationService.checkRelation(fromId, toId, strRelationType).get(); + if (!found) { + throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"}) + @ResponseBody + public List findByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType) throws ThingsboardException { + checkParameter("fromId", strFromId); + checkParameter("fromType", strFromType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); + checkEntityId(entityId); + try { + return checkNotNull(relationService.findByFrom(entityId).get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"}) + @ResponseBody + public List findByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType + , @RequestParam("relationType") String strRelationType) throws ThingsboardException { + checkParameter("fromId", strFromId); + checkParameter("fromType", strFromType); + checkParameter("relationType", strRelationType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); + checkEntityId(entityId); + try { + return checkNotNull(relationService.findByFromAndType(entityId, strRelationType).get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"}) + @ResponseBody + public List findByTo(@RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException { + checkParameter("toId", strToId); + checkParameter("toType", strToType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId); + checkEntityId(entityId); + try { + return checkNotNull(relationService.findByTo(entityId).get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"}) + @ResponseBody + public List findByTo(@RequestParam("toId") String strToId, @RequestParam("toType") String strToType + , @RequestParam("relationType") String strRelationType) throws ThingsboardException { + checkParameter("toId", strToId); + checkParameter("toType", strToType); + checkParameter("relationType", strRelationType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId); + checkEntityId(entityId); + try { + return checkNotNull(relationService.findByToAndType(entityId, strRelationType).get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getFilters()); + checkEntityId(query.getParameters().getEntityId()); + try { + return checkNotNull(relationService.findByQuery(query).get()); + } catch (Exception e) { + throw handleException(e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index 1bf6aa984c..dc35324bdd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -18,7 +18,6 @@ package org.thingsboard.server.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.page.TimePageData; @@ -59,7 +58,7 @@ public class EventController extends BaseController { ThingsboardErrorCode.PERMISSION_DENIED); } TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(eventService.findEvents(tenantId, getEntityId(strEntityType, strEntityId), eventType, pageLink)); + return checkNotNull(eventService.findEvents(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), eventType, pageLink)); } catch (Exception e) { throw handleException(e); } @@ -88,29 +87,10 @@ public class EventController extends BaseController { ThingsboardErrorCode.PERMISSION_DENIED); } TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); - return checkNotNull(eventService.findEvents(tenantId, getEntityId(strEntityType, strEntityId), pageLink)); + return checkNotNull(eventService.findEvents(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink)); } catch (Exception e) { throw handleException(e); } } - - private EntityId getEntityId(String strEntityType, String strEntityId) throws ThingsboardException { - EntityId entityId; - EntityType entityType = EntityType.valueOf(strEntityType); - switch (entityType) { - case RULE: - entityId = new RuleId(toUUID(strEntityId)); - break; - case PLUGIN: - entityId = new PluginId(toUUID(strEntityId)); - break; - case DEVICE: - entityId = new DeviceId(toUUID(strEntityId)); - break; - default: - throw new ThingsboardException("EntityType ['" + entityType + "'] is incorrect!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - return entityId; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java index 089ade8f5d..c0efbfc1d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.service.cluster.routing; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.service.cluster.discovery.ServerInstance; import java.util.Optional; +import java.util.UUID; /** * @author Andrew Shvayka @@ -28,6 +30,8 @@ public interface ClusterRoutingService { ServerAddress getCurrentServer(); - Optional resolve(UUIDBased entityId); + Optional resolveByUuid(UUID uuid); + + Optional resolveById(EntityId entityId); } diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java index b57e15eadf..c29833b910 100644 --- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.service.cluster.discovery.DiscoveryService; @@ -31,6 +32,7 @@ import org.thingsboard.server.utils.MiscUtils; import javax.annotation.PostConstruct; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; @@ -77,13 +79,18 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D } @Override - public Optional resolve(UUIDBased entityId) { - Assert.notNull(entityId); + public Optional resolveById(EntityId entityId) { + return resolveByUuid(entityId.getId()); + } + + @Override + public Optional resolveByUuid(UUID uuid) { + Assert.notNull(uuid); if (circle.isEmpty()) { return Optional.empty(); } - Long hash = hashFunction.newHasher().putLong(entityId.getId().getMostSignificantBits()) - .putLong(entityId.getId().getLeastSignificantBits()).hash().asLong(); + Long hash = hashFunction.newHasher().putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()).hash().asLong(); if (!circle.containsKey(hash)) { ConcurrentNavigableMap tailMap = circle.tailMap(hash); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e64e9cd95c..69b1cd78a2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -60,8 +60,8 @@ plugins: # JWT Token parameters security.jwt: - tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins) - refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour) + tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000000}" # Number of seconds (15 mins) + refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:36000000}" # Seconds (1 hour) tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" diff --git a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java index 2940a62a93..857e0835e3 100644 --- a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java @@ -145,7 +145,7 @@ public class DefaultActorServiceTest { ReflectionTestUtils.setField(actorContext, "eventService", eventService); - when(routingService.resolve(any())).thenReturn(Optional.empty()); + when(routingService.resolveById((EntityId) any())).thenReturn(Optional.empty()); when(discoveryService.getCurrentServer()).thenReturn(serverInstance); @@ -239,7 +239,7 @@ public class DefaultActorServiceTest { List expected = new ArrayList<>(); expected.add(new BasicTsKvEntry(ts, entry1)); expected.add(new BasicTsKvEntry(ts, entry2)); - verify(tsService, Mockito.timeout(5000)).save(DataConstants.DEVICE, deviceId, expected); + verify(tsService, Mockito.timeout(5000)).save(deviceId, expected); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 6b312836b3..92e8655c72 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -28,6 +28,7 @@ public class Device extends SearchTextBased { private TenantId tenantId; private CustomerId customerId; private String name; + private String type; private JsonNode additionalInfo; public Device() { @@ -43,6 +44,7 @@ public class Device extends SearchTextBased { this.tenantId = device.getTenantId(); this.customerId = device.getCustomerId(); this.name = device.getName(); + this.type = device.getType(); this.additionalInfo = device.getAdditionalInfo(); } @@ -70,6 +72,14 @@ public class Device extends SearchTextBased { this.name = name; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public JsonNode getAdditionalInfo() { return additionalInfo; } @@ -90,6 +100,7 @@ public class Device extends SearchTextBased { result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode()); result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); return result; } @@ -118,6 +129,11 @@ public class Device extends SearchTextBased { return false; } else if (!name.equals(other.name)) return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; if (tenantId == null) { if (other.tenantId != null) return false; @@ -135,6 +151,8 @@ public class Device extends SearchTextBased { builder.append(customerId); builder.append(", name="); builder.append(name); + builder.append(", type="); + builder.append(type); builder.append(", additionalInfo="); builder.append(additionalInfo); builder.append(", createdTime="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 8dac8f5e20..ebf42f22ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, DEVICE, CUSTOMER, RULE, PLUGIN + TENANT, CUSTOMER, USER, RULE, PLUGIN, DASHBOARD, ASSET, DEVICE, ALARM } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java new file mode 100644 index 0000000000..19d671cab2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2017 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.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.EntityId; + +/** + * Created by ashvayka on 11.05.17. + */ +@Data +public class Alarm extends BaseData { + + private long startTs; + private long endTs; + private long ackTs; + private long clearTs; + private String type; + private EntityId originator; + private AlarmSeverity severity; + private AlarmStatus status; + private JsonNode details; + private boolean propagate; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java new file mode 100644 index 0000000000..ad0fd5b98a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2017 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.alarm; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UUIDBased; + +import java.util.UUID; + +public class AlarmId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public AlarmId(@JsonProperty("id") UUID id) { + super(id); + } + + public static AlarmId fromString(String alarmId) { + return new AlarmId(UUID.fromString(alarmId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.ALARM; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java new file mode 100644 index 0000000000..04d23a55c0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2017 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.alarm; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.TimePageLink; + +/** + * Created by ashvayka on 11.05.17. + */ +@Data +public class AlarmQuery { + + private EntityId affectedEntityId; + private TimePageLink pageLink; + private AlarmStatus status; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java new file mode 100644 index 0000000000..ed73027ca6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2017 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.alarm; + +/** + * Created by ashvayka on 11.05.17. + */ +public enum AlarmSeverity { + + CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java new file mode 100644 index 0000000000..54fbc9dc03 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2017 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.alarm; + +/** + * Created by ashvayka on 11.05.17. + */ +public enum AlarmStatus { + + ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java new file mode 100644 index 0000000000..2e20b7745c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -0,0 +1,166 @@ +/** + * Copyright © 2016-2017 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.asset; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.SearchTextBased; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; + +public class Asset extends SearchTextBased { + + private static final long serialVersionUID = 2807343040519543363L; + + private TenantId tenantId; + private CustomerId customerId; + private String name; + private String type; + private JsonNode additionalInfo; + + public Asset() { + super(); + } + + public Asset(AssetId id) { + super(id); + } + + public Asset(Asset asset) { + super(asset); + this.tenantId = asset.getTenantId(); + this.customerId = asset.getCustomerId(); + this.name = asset.getName(); + this.type = asset.getType(); + this.additionalInfo = asset.getAdditionalInfo(); + } + + public TenantId getTenantId() { + return tenantId; + } + + public void setTenantId(TenantId tenantId) { + this.tenantId = tenantId; + } + + public CustomerId getCustomerId() { + return customerId; + } + + public void setCustomerId(CustomerId customerId) { + this.customerId = customerId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public JsonNode getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(JsonNode additionalInfo) { + this.additionalInfo = additionalInfo; + } + + @Override + public String getSearchText() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode()); + result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + Asset other = (Asset) obj; + if (additionalInfo == null) { + if (other.additionalInfo != null) + return false; + } else if (!additionalInfo.equals(other.additionalInfo)) + return false; + if (customerId == null) { + if (other.customerId != null) + return false; + } else if (!customerId.equals(other.customerId)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + if (tenantId == null) { + if (other.tenantId != null) + return false; + } else if (!tenantId.equals(other.tenantId)) + return false; + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Asset [tenantId="); + builder.append(tenantId); + builder.append(", customerId="); + builder.append(customerId); + builder.append(", name="); + builder.append(name); + builder.append(", type="); + builder.append(type); + builder.append(", additionalInfo="); + builder.append(additionalInfo); + builder.append(", createdTime="); + builder.append(createdTime); + builder.append(", id="); + builder.append(id); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java new file mode 100644 index 0000000000..049116e876 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2017 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class AssetId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public AssetId(@JsonProperty("id") UUID id) { + super(id); + } + + public static AssetId fromString(String assetId) { + return new AssetId(UUID.fromString(assetId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.ASSET; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java index 632d5c23fb..a9a3441528 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java @@ -18,12 +18,24 @@ package org.thingsboard.server.common.data.id; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; -public class DashboardId extends UUIDBased { +public class DashboardId extends UUIDBased implements EntityId { @JsonCreator - public DashboardId(@JsonProperty("id") UUID id){ + public DashboardId(@JsonProperty("id") UUID id) { super(id); } + + public static DashboardId fromString(String dashboardId) { + return new DashboardId(UUID.fromString(dashboardId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.DASHBOARD; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index f0caec5e39..17d69ac939 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -23,6 +25,9 @@ import java.util.UUID; /** * @author Andrew Shvayka */ + +@JsonDeserialize(using = EntityIdDeserializer.class) +@JsonSerialize(using = EntityIdSerializer.class) public interface EntityId { UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java new file mode 100644 index 0000000000..f9f34c413e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2017 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.id; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; + +/** + * Created by ashvayka on 11.05.17. + */ +public class EntityIdDeserializer extends JsonDeserializer { + + @Override + public EntityId deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException, JsonProcessingException { + ObjectCodec oc = jsonParser.getCodec(); + ObjectNode node = oc.readTree(jsonParser); + if (node.has("entityType") && node.has("id")) { + return EntityIdFactory.getByTypeAndId(node.get("entityType").asText(), node.get("id").asText()); + } else { + throw new IOException("Missing entityType or id!"); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java new file mode 100644 index 0000000000..21093ce3f5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2017 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.id; + +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +/** + * Created by ashvayka on 25.04.17. + */ +public class EntityIdFactory { + + public static EntityId getByTypeAndId(String type, String uuid) { + return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); + } + + public static EntityId getByTypeAndUuid(String type, UUID uuid) { + return getByTypeAndUuid(EntityType.valueOf(type), uuid); + } + + public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { + switch (type) { + case TENANT: + return new TenantId(uuid); + case CUSTOMER: + return new CustomerId(uuid); + case USER: + return new UserId(uuid); + case RULE: + return new RuleId(uuid); + case PLUGIN: + return new PluginId(uuid); + case DASHBOARD: + return new DashboardId(uuid); + case DEVICE: + return new DeviceId(uuid); + case ASSET: + return new AssetId(uuid); + } + throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java new file mode 100644 index 0000000000..18ffaeaa5b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2017 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.id; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * Created by ashvayka on 11.05.17. + */ +public class EntityIdSerializer extends JsonSerializer { + + @Override + public void serialize(EntityId value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { + gen.writeStartObject(); + gen.writeStringField("entityType", value.getEntityType().name()); + gen.writeStringField("id", value.getId().toString()); + gen.writeEndObject(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java index 51b114012b..31b6642ef7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java @@ -18,12 +18,25 @@ package org.thingsboard.server.common.data.id; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; -public class UserId extends UUIDBased { +public class UserId extends UUIDBased implements EntityId { @JsonCreator - public UserId(@JsonProperty("id") UUID id){ - super(id); - } + public UserId(@JsonProperty("id") UUID id) { + super(id); + } + + public static UserId fromString(String userId) { + return new UserId(UUID.fromString(userId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.USER; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java new file mode 100644 index 0000000000..8fcf2691af --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2017 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.relation; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Objects; + +public class EntityRelation { + + private static final long serialVersionUID = 2807343040519543363L; + + public static final String CONTAINS_TYPE = "Contains"; + public static final String MANAGES_TYPE = "Manages"; + + private EntityId from; + private EntityId to; + private String type; + private JsonNode additionalInfo; + + public EntityRelation() { + super(); + } + + public EntityRelation(EntityId from, EntityId to, String type) { + this(from, to, type, null); + } + + public EntityRelation(EntityId from, EntityId to, String type, JsonNode additionalInfo) { + this.from = from; + this.to = to; + this.type = type; + this.additionalInfo = additionalInfo; + } + + public EntityRelation(EntityRelation device) { + this.from = device.getFrom(); + this.to = device.getTo(); + this.type = device.getType(); + this.additionalInfo = device.getAdditionalInfo(); + } + + public EntityId getFrom() { + return from; + } + + public void setFrom(EntityId from) { + this.from = from; + } + + public EntityId getTo() { + return to; + } + + public void setTo(EntityId to) { + this.to = to; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public JsonNode getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(JsonNode additionalInfo) { + this.additionalInfo = additionalInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EntityRelation relation = (EntityRelation) o; + return Objects.equals(from, relation.from) && + Objects.equals(to, relation.to) && + Objects.equals(type, relation.type); + } + + @Override + public int hashCode() { + return Objects.hash(from, to, type); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java new file mode 100644 index 0000000000..a116598a23 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +/** + * Created by ashvayka on 11.05.17. + */ +public interface AlarmDao { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java new file mode 100644 index 0000000000..35bd247602 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmId; +import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.page.TimePageData; + +import java.util.Optional; + +/** + * Created by ashvayka on 11.05.17. + */ +public interface AlarmService { + + Optional saveIfNotExists(Alarm alarm); + + ListenableFuture updateAlarm(Alarm alarm); + + ListenableFuture ackAlarm(Alarm alarm); + + ListenableFuture clearAlarm(AlarmId alarmId); + + ListenableFuture> findAlarms(AlarmQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java new file mode 100644 index 0000000000..81f95b4821 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.model.AssetEntity; +import org.thingsboard.server.dao.model.DeviceEntity; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * The Interface AssetDao. + * + */ +public interface AssetDao extends Dao { + + /** + * Save or update asset object + * + * @param asset the asset object + * @return saved asset object + */ + AssetEntity save(Asset asset); + + /** + * Find assets by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of asset objects + */ + List findAssetsByTenantId(UUID tenantId, TextPageLink pageLink); + + /** + * Find assets by tenantId and assets Ids. + * + * @param tenantId the tenantId + * @param assetIds the asset Ids + * @return the list of asset objects + */ + ListenableFuture> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List assetIds); + + /** + * Find assets by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of asset objects + */ + List findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); + + /** + * Find assets by tenantId, customerId and assets Ids. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param assetIds the asset Ids + * @return the list of asset objects + */ + ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List assetIds); + + /** + * Find assets by tenantId and asset name. + * + * @param tenantId the tenantId + * @param name the asset name + * @return the optional asset object + */ + Optional findAssetsByTenantIdAndName(UUID tenantId, String name); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java new file mode 100644 index 0000000000..ed08c6ddf3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import com.datastax.driver.core.querybuilder.Select; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.AbstractSearchTextDao; +import org.thingsboard.server.dao.model.AssetEntity; + +import java.util.*; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.*; +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Component +@Slf4j +public class AssetDaoImpl extends AbstractSearchTextDao implements AssetDao { + + @Override + protected Class getColumnFamilyClass() { + return AssetEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return ASSET_COLUMN_FAMILY_NAME; + } + + @Override + public AssetEntity save(Asset asset) { + log.debug("Save asset [{}] ", asset); + return save(new AssetEntity(asset)); + } + + @Override + public List findAssetsByTenantId(UUID tenantId, TextPageLink pageLink) { + log.debug("Try to find assets by tenantId [{}] and pageLink [{}]", tenantId, pageLink); + List assetEntities = findPageWithTextSearch(ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, + Collections.singletonList(eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink); + + log.trace("Found assets [{}] by tenantId [{}] and pageLink [{}]", assetEntities, tenantId, pageLink); + return assetEntities; + } + + @Override + public ListenableFuture> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List assetIds) { + log.debug("Try to find assets by tenantId [{}] and asset Ids [{}]", tenantId, assetIds); + Select select = select().from(getColumnFamilyName()); + Select.Where query = select.where(); + query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId)); + query.and(in(ID_PROPERTY, assetIds)); + return findListByStatementAsync(query); + } + + @Override + public List findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { + log.debug("Try to find assets by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); + List assetEntities = findPageWithTextSearch(ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, + Arrays.asList(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId), + eq(ASSET_TENANT_ID_PROPERTY, tenantId)), + pageLink); + + log.trace("Found assets [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", assetEntities, tenantId, customerId, pageLink); + return assetEntities; + } + + @Override + public ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List assetIds) { + log.debug("Try to find assets by tenantId [{}], customerId [{}] and asset Ids [{}]", tenantId, customerId, assetIds); + Select select = select().from(getColumnFamilyName()); + Select.Where query = select.where(); + query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId)); + query.and(in(ID_PROPERTY, assetIds)); + return findListByStatementAsync(query); + } + + @Override + public Optional findAssetsByTenantIdAndName(UUID tenantId, String assetName) { + Select select = select().from(ASSET_BY_TENANT_AND_NAME_VIEW_NAME); + Select.Where query = select.where(); + query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(ASSET_NAME_PROPERTY, assetName)); + return Optional.ofNullable(findOneByStatement(query)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java new file mode 100644 index 0000000000..f3c69b23bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.dao.relation.EntityTypeFilter; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by ashvayka on 03.05.17. + */ +@Data +public class AssetSearchQuery { + + private RelationsSearchParameters parameters; + @Nullable + private String relationType; + @Nullable + private List assetTypes; + + public EntityRelationsQuery toEntitySearchQuery() { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(parameters); + query.setFilters( + Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(EntityType.ASSET)))); + return query; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java new file mode 100644 index 0000000000..7a61bd8019 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.asset.Asset; +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.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; + +import java.util.List; +import java.util.Optional; + +public interface AssetService { + + Asset findAssetById(AssetId assetId); + + ListenableFuture findAssetByIdAsync(AssetId assetId); + + Optional findAssetByTenantIdAndName(TenantId tenantId, String name); + + Asset saveAsset(Asset device); + + Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId); + + Asset unassignAssetFromCustomer(AssetId assetId); + + void deleteAsset(AssetId assetId); + + TextPageData findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink); + + ListenableFuture> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List assetIds); + + void deleteAssetsByTenantId(TenantId tenantId); + + TextPageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + + ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List assetIds); + + void unassignCustomerAssets(TenantId tenantId, CustomerId customerId); + + ListenableFuture> findAssetsByQuery(AssetSearchQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java new file mode 100644 index 0000000000..221e4df4fc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import lombok.Data; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * Created by ashvayka on 02.05.17. + */ +@Data +public class AssetTypeFilter { + @Nullable + private String relationType; + @Nullable + private List assetTypes; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java new file mode 100644 index 0000000000..c63e5d7aa0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -0,0 +1,292 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + + +import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.BaseEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.model.AssetEntity; +import org.thingsboard.server.dao.model.CustomerEntity; +import org.thingsboard.server.dao.model.TenantEntity; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.dao.relation.EntitySearchDirection; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.tenant.TenantDao; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.DaoUtil.*; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; +import static org.thingsboard.server.dao.service.Validator.*; + +@Service +@Slf4j +public class BaseAssetService extends BaseEntityService implements AssetService { + + @Autowired + private AssetDao assetDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private CustomerDao customerDao; + + @Override + public Asset findAssetById(AssetId assetId) { + log.trace("Executing findAssetById [{}]", assetId); + validateId(assetId, "Incorrect assetId " + assetId); + AssetEntity assetEntity = assetDao.findById(assetId.getId()); + return getData(assetEntity); + } + + @Override + public ListenableFuture findAssetByIdAsync(AssetId assetId) { + log.trace("Executing findAssetById [{}]", assetId); + validateId(assetId, "Incorrect assetId " + assetId); + ListenableFuture assetEntity = assetDao.findByIdAsync(assetId.getId()); + return Futures.transform(assetEntity, (Function) input -> getData(input)); + } + + @Override + public Optional findAssetByTenantIdAndName(TenantId tenantId, String name) { + log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name); + validateId(tenantId, "Incorrect tenantId " + tenantId); + Optional assetEntityOpt = assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name); + if (assetEntityOpt.isPresent()) { + return Optional.of(getData(assetEntityOpt.get())); + } else { + return Optional.empty(); + } + } + + @Override + public Asset saveAsset(Asset asset) { + log.trace("Executing saveAsset [{}]", asset); + assetValidator.validate(asset); + return getData(assetDao.save(asset)); + } + + @Override + public Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId) { + Asset asset = findAssetById(assetId); + asset.setCustomerId(customerId); + return saveAsset(asset); + } + + @Override + public Asset unassignAssetFromCustomer(AssetId assetId) { + Asset asset = findAssetById(assetId); + asset.setCustomerId(null); + return saveAsset(asset); + } + + @Override + public void deleteAsset(AssetId assetId) { + log.trace("Executing deleteAsset [{}]", assetId); + validateId(assetId, "Incorrect assetId " + assetId); + deleteEntityRelations(assetId); + assetDao.removeById(assetId.getId()); + } + + @Override + public TextPageData findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink) { + log.trace("Executing findAssetsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, "Incorrect tenantId " + tenantId); + validatePageLink(pageLink, "Incorrect page link " + pageLink); + List assetEntities = assetDao.findAssetsByTenantId(tenantId.getId(), pageLink); + List assets = convertDataList(assetEntities); + return new TextPageData(assets, pageLink); + } + + @Override + public ListenableFuture> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List assetIds) { + log.trace("Executing findAssetsByTenantIdAndIdsAsync, tenantId [{}], assetIds [{}]", tenantId, assetIds); + validateId(tenantId, "Incorrect tenantId " + tenantId); + validateIds(assetIds, "Incorrect assetIds " + assetIds); + ListenableFuture> assetEntities = assetDao.findAssetsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetIds)); + return Futures.transform(assetEntities, (Function, List>) input -> convertDataList(input)); + } + + @Override + public void deleteAssetsByTenantId(TenantId tenantId) { + log.trace("Executing deleteAssetsByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, "Incorrect tenantId " + tenantId); + tenantAssetsRemover.removeEntitites(tenantId); + } + + @Override + public TextPageData findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) { + log.trace("Executing findAssetsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, "Incorrect tenantId " + tenantId); + validateId(customerId, "Incorrect customerId " + customerId); + validatePageLink(pageLink, "Incorrect page link " + pageLink); + List assetEntities = assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + List assets = convertDataList(assetEntities); + return new TextPageData(assets, pageLink); + } + + @Override + public ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List assetIds) { + log.trace("Executing findAssetsByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], assetIds [{}]", tenantId, customerId, assetIds); + validateId(tenantId, "Incorrect tenantId " + tenantId); + validateId(customerId, "Incorrect customerId " + customerId); + validateIds(assetIds, "Incorrect assetIds " + assetIds); + ListenableFuture> assetEntities = assetDao.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), + customerId.getId(), toUUIDs(assetIds)); + return Futures.transform(assetEntities, (Function, List>) input -> convertDataList(input)); + } + + @Override + public void unassignCustomerAssets(TenantId tenantId, CustomerId customerId) { + log.trace("Executing unassignCustomerAssets, tenantId [{}], customerId [{}]", tenantId, customerId); + validateId(tenantId, "Incorrect tenantId " + tenantId); + validateId(customerId, "Incorrect customerId " + customerId); + new CustomerAssetsUnassigner(tenantId).removeEntitites(customerId); + } + + @Override + public ListenableFuture> findAssetsByQuery(AssetSearchQuery query) { + ListenableFuture> relations = relationService.findByQuery(query.toEntitySearchQuery()); + ListenableFuture> assets = Futures.transform(relations, (AsyncFunction, List>) relations1 -> { + EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); + List> futures = new ArrayList<>(); + for (EntityRelation relation : relations1) { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + if (entityId.getEntityType() == EntityType.ASSET) { + futures.add(findAssetByIdAsync(new AssetId(entityId.getId()))); + } + } + return Futures.successfulAsList(futures); + }); + + assets = Futures.transform(assets, new Function, List>() { + @Nullable + @Override + public List apply(@Nullable List assetList) { + return assetList.stream().filter(asset -> query.getAssetTypes().contains(asset.getType())).collect(Collectors.toList()); + } + }); + + return assets; + } + + private DataValidator assetValidator = + new DataValidator() { + + @Override + protected void validateCreate(Asset asset) { + assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( + d -> { + throw new DataValidationException("Asset with such name already exists!"); + } + ); + } + + @Override + protected void validateUpdate(Asset asset) { + assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent( + d -> { + if (!d.getId().equals(asset.getUuidId())) { + throw new DataValidationException("Asset with such name already exists!"); + } + } + ); + } + + @Override + protected void validateDataImpl(Asset asset) { + if (StringUtils.isEmpty(asset.getName())) { + throw new DataValidationException("Asset name should be specified!"); + } + if (asset.getTenantId() == null) { + throw new DataValidationException("Asset should be assigned to tenant!"); + } else { + TenantEntity tenant = tenantDao.findById(asset.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Asset is referencing to non-existent tenant!"); + } + } + if (asset.getCustomerId() == null) { + asset.setCustomerId(new CustomerId(NULL_UUID)); + } else if (!asset.getCustomerId().getId().equals(NULL_UUID)) { + CustomerEntity customer = customerDao.findById(asset.getCustomerId().getId()); + if (customer == null) { + throw new DataValidationException("Can't assign asset to non-existent customer!"); + } + if (!customer.getTenantId().equals(asset.getTenantId().getId())) { + throw new DataValidationException("Can't assign asset to customer from different tenant!"); + } + } + } + }; + + private PaginatedRemover tenantAssetsRemover = + new PaginatedRemover() { + + @Override + protected List findEntities(TenantId id, TextPageLink pageLink) { + return assetDao.findAssetsByTenantId(id.getId(), pageLink); + } + + @Override + protected void removeEntity(AssetEntity entity) { + deleteAsset(new AssetId(entity.getId())); + } + }; + + class CustomerAssetsUnassigner extends PaginatedRemover { + + private TenantId tenantId; + + CustomerAssetsUnassigner(TenantId tenantId) { + this.tenantId = tenantId; + } + + @Override + protected List findEntities(CustomerId id, TextPageLink pageLink) { + return assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); + } + + @Override + protected void removeEntity(AssetEntity entity) { + unassignAssetFromCustomer(new AssetId(entity.getId())); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index 01f4f99995..e8b42c2c34 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.customer; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -23,16 +24,18 @@ import org.thingsboard.server.common.data.page.TextPageLink; public interface CustomerService { - public Customer findCustomerById(CustomerId customerId); + Customer findCustomerById(CustomerId customerId); + + ListenableFuture findCustomerByIdAsync(CustomerId customerId); - public Customer saveCustomer(Customer customer); + Customer saveCustomer(Customer customer); - public void deleteCustomer(CustomerId customerId); + void deleteCustomer(CustomerId customerId); - public Customer findOrCreatePublicCustomer(TenantId tenantId); - - public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); + Customer findOrCreatePublicCustomer(TenantId tenantId); - public void deleteCustomersByTenantId(TenantId tenantId); + TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); + void deleteCustomersByTenantId(TenantId tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index a80c6e84b2..e9f6836774 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer; import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.getData; +import static org.thingsboard.server.dao.service.Validator.validateId; import java.io.IOException; import java.util.List; @@ -24,31 +25,35 @@ import java.util.Optional; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Function; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.AssetEntity; import org.thingsboard.server.dao.model.CustomerEntity; import org.thingsboard.server.dao.model.TenantEntity; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.user.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.service.Validator; @Service @Slf4j -public class CustomerServiceImpl implements CustomerService { +public class CustomerServiceImpl extends BaseEntityService implements CustomerService { private static final String PUBLIC_CUSTOMER_TITLE = "Public"; @@ -75,6 +80,14 @@ public class CustomerServiceImpl implements CustomerService { return getData(customerEntity); } + @Override + public ListenableFuture findCustomerByIdAsync(CustomerId customerId) { + log.trace("Executing findCustomerByIdAsync [{}]", customerId); + validateId(customerId, "Incorrect customerId " + customerId); + ListenableFuture customerEntity = customerDao.findByIdAsync(customerId.getId()); + return Futures.transform(customerEntity, (Function) input -> getData(input)); + } + @Override public Customer saveCustomer(Customer customer) { log.trace("Executing saveCustomer [{}]", customer); @@ -93,7 +106,8 @@ public class CustomerServiceImpl implements CustomerService { } dashboardService.unassignCustomerDashboards(customer.getTenantId(), customerId); deviceService.unassignCustomerDevices(customer.getTenantId(), customerId); - userService.deleteCustomerUsers(customer.getTenantId(), customerId); + userService.deleteCustomerUsers(customer.getTenantId(), customerId); + deleteEntityRelations(customerId); customerDao.removeById(customerId.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 8c860646bd..b0ebbfd5f0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -26,7 +26,9 @@ import org.thingsboard.server.common.data.page.TextPageLink; public interface DashboardService { public Dashboard findDashboardById(DashboardId dashboardId); - + + public DashboardInfo findDashboardInfoById(DashboardId dashboardId); + public Dashboard saveDashboard(Dashboard dashboard); public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 2e0abfbdfd..cf554f3a62 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -30,20 +30,19 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.*; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.service.Validator; @Service @Slf4j -public class DashboardServiceImpl implements DashboardService { +public class DashboardServiceImpl extends BaseEntityService implements DashboardService { @Autowired private DashboardDao dashboardDao; @@ -65,6 +64,14 @@ public class DashboardServiceImpl implements DashboardService { return getData(dashboardEntity); } + @Override + public DashboardInfo findDashboardInfoById(DashboardId dashboardId) { + log.trace("Executing findDashboardInfoById [{}]", dashboardId); + Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + DashboardInfoEntity dashboardInfoEntity = dashboardInfoDao.findById(dashboardId.getId()); + return getData(dashboardInfoEntity); + } + @Override public Dashboard saveDashboard(Dashboard dashboard) { log.trace("Executing saveDashboard [{}]", dashboard); @@ -91,6 +98,7 @@ public class DashboardServiceImpl implements DashboardService { public void deleteDashboard(DashboardId dashboardId) { log.trace("Executing deleteDashboard [{}]", dashboardId); Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + deleteEntityRelations(dashboardId); dashboardDao.removeById(dashboardId.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java new file mode 100644 index 0000000000..eb9d9dee3d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.dao.relation.EntityTypeFilter; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; + +@Data +public class DeviceSearchQuery { + + private RelationsSearchParameters parameters; + @Nullable + private String relationType; + @Nullable + private List deviceTypes; + + public EntityRelationsQuery toEntitySearchQuery() { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(parameters); + query.setFilters( + Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(EntityType.DEVICE)))); + return query; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 35d34968cf..4715435dec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -53,4 +53,7 @@ public interface DeviceService { ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); + + ListenableFuture> findDevicesByQuery(DeviceSearchQuery query); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 3d1ce31349..ab6fa4e7fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -24,36 +25,40 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; 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.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.CustomerEntity; import org.thingsboard.server.dao.model.DeviceEntity; import org.thingsboard.server.dao.model.TenantEntity; +import org.thingsboard.server.dao.relation.EntitySearchDirection; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; +import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; -import static org.thingsboard.server.dao.DaoUtil.convertDataList; -import static org.thingsboard.server.dao.DaoUtil.getData; -import static org.thingsboard.server.dao.DaoUtil.toUUIDs; +import static org.thingsboard.server.dao.DaoUtil.*; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; -import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; -import static org.thingsboard.server.dao.service.Validator.validatePageLink; +import static org.thingsboard.server.dao.service.Validator.*; @Service @Slf4j -public class DeviceServiceImpl implements DeviceService { +public class DeviceServiceImpl extends BaseEntityService implements DeviceService { @Autowired private DeviceDao deviceDao; @@ -132,6 +137,7 @@ public class DeviceServiceImpl implements DeviceService { if (deviceCredentials != null) { deviceCredentialsService.deleteDeviceCredentials(deviceCredentials); } + deleteEntityRelations(deviceId); deviceDao.removeById(deviceId.getId()); } @@ -192,6 +198,32 @@ public class DeviceServiceImpl implements DeviceService { new CustomerDevicesUnassigner(tenantId).removeEntitites(customerId); } + @Override + public ListenableFuture> findDevicesByQuery(DeviceSearchQuery query) { + ListenableFuture> relations = relationService.findByQuery(query.toEntitySearchQuery()); + ListenableFuture> devices = Futures.transform(relations, (AsyncFunction, List>) relations1 -> { + EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); + List> futures = new ArrayList<>(); + for (EntityRelation relation : relations1) { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + if (entityId.getEntityType() == EntityType.DEVICE) { + futures.add(findDeviceByIdAsync(new DeviceId(entityId.getId()))); + } + } + return Futures.successfulAsList(futures); + }); + + devices = Futures.transform(devices, new Function, List>() { + @Nullable + @Override + public List apply(@Nullable List deviceList) { + return deviceList.stream().filter(device -> query.getDeviceTypes().contains(device.getType())).collect(Collectors.toList()); + } + }); + + return devices; + } + private DataValidator deviceValidator = new DataValidator() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java new file mode 100644 index 0000000000..3c1c528193 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.entity; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.relation.RelationService; + +/** + * Created by ashvayka on 04.05.17. + */ +@Slf4j +public class BaseEntityService { + + @Autowired + protected RelationService relationService; + + protected void deleteEntityRelations(EntityId entityId) { + log.trace("Executing deleteEntityRelations [{}]", entityId); + relationService.deleteEntityRelations(entityId); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java new file mode 100644 index 0000000000..0444d11af9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java @@ -0,0 +1,235 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model; + +import com.datastax.driver.core.utils.UUIDs; +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.PartitionKey; +import com.datastax.driver.mapping.annotations.Table; +import com.datastax.driver.mapping.annotations.Transient; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.asset.Asset; +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.dao.model.type.JsonCodec; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Table(name = ASSET_COLUMN_FAMILY_NAME) +public final class AssetEntity implements SearchTextEntity { + + @Transient + private static final long serialVersionUID = -1265181166886910152L; + + @PartitionKey(value = 0) + @Column(name = ID_PROPERTY) + private UUID id; + + @PartitionKey(value = 1) + @Column(name = ASSET_TENANT_ID_PROPERTY) + private UUID tenantId; + + @PartitionKey(value = 2) + @Column(name = ASSET_CUSTOMER_ID_PROPERTY) + private UUID customerId; + + @Column(name = ASSET_NAME_PROPERTY) + private String name; + + @Column(name = ASSET_TYPE_PROPERTY) + private String type; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ASSET_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class) + private JsonNode additionalInfo; + + public AssetEntity() { + super(); + } + + public AssetEntity(Asset asset) { + if (asset.getId() != null) { + this.id = asset.getId().getId(); + } + if (asset.getTenantId() != null) { + this.tenantId = asset.getTenantId().getId(); + } + if (asset.getCustomerId() != null) { + this.customerId = asset.getCustomerId().getId(); + } + this.name = asset.getName(); + this.type = asset.getType(); + this.additionalInfo = asset.getAdditionalInfo(); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTenantId() { + return tenantId; + } + + public void setTenantId(UUID tenantId) { + this.tenantId = tenantId; + } + + public UUID getCustomerId() { + return customerId; + } + + public void setCustomerId(UUID customerId) { + this.customerId = customerId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public JsonNode getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(JsonNode additionalInfo) { + this.additionalInfo = additionalInfo; + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode()); + result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AssetEntity other = (AssetEntity) obj; + if (additionalInfo == null) { + if (other.additionalInfo != null) + return false; + } else if (!additionalInfo.equals(other.additionalInfo)) + return false; + if (customerId == null) { + if (other.customerId != null) + return false; + } else if (!customerId.equals(other.customerId)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + if (tenantId == null) { + if (other.tenantId != null) + return false; + } else if (!tenantId.equals(other.tenantId)) + return false; + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AssetEntity [id="); + builder.append(id); + builder.append(", tenantId="); + builder.append(tenantId); + builder.append(", customerId="); + builder.append(customerId); + builder.append(", name="); + builder.append(name); + builder.append(", type="); + builder.append(type); + builder.append(", additionalInfo="); + builder.append(additionalInfo); + builder.append("]"); + return builder.toString(); + } + + @Override + public Asset toData() { + Asset asset = new Asset(new AssetId(id)); + asset.setCreatedTime(UUIDs.unixTimestamp(id)); + if (tenantId != null) { + asset.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + asset.setCustomerId(new CustomerId(customerId)); + } + asset.setName(name); + asset.setType(type); + asset.setAdditionalInfo(additionalInfo); + return asset; + } + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java index 740bf2ac44..69ed92cc1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java @@ -51,7 +51,10 @@ public final class DeviceEntity implements SearchTextEntity { @Column(name = DEVICE_NAME_PROPERTY) private String name; - + + @Column(name = DEVICE_TYPE_PROPERTY) + private String type; + @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; @@ -73,6 +76,7 @@ public final class DeviceEntity implements SearchTextEntity { this.customerId = device.getCustomerId().getId(); } this.name = device.getName(); + this.type = device.getType(); this.additionalInfo = device.getAdditionalInfo(); } @@ -108,6 +112,14 @@ public final class DeviceEntity implements SearchTextEntity { this.name = name; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public JsonNode getAdditionalInfo() { return additionalInfo; } @@ -138,6 +150,7 @@ public final class DeviceEntity implements SearchTextEntity { result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); return result; } @@ -171,6 +184,11 @@ public final class DeviceEntity implements SearchTextEntity { return false; } else if (!name.equals(other.name)) return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; if (tenantId == null) { if (other.tenantId != null) return false; @@ -190,6 +208,8 @@ public final class DeviceEntity implements SearchTextEntity { builder.append(customerId); builder.append(", name="); builder.append(name); + builder.append(", type="); + builder.append(type); builder.append(", additionalInfo="); builder.append(additionalInfo); builder.append("]"); @@ -207,6 +227,7 @@ public final class DeviceEntity implements SearchTextEntity { device.setCustomerId(new CustomerId(customerId)); } device.setName(name); + device.setType(type); device.setAdditionalInfo(additionalInfo); return device; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java index fc22a8dbe7..78928146cb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java @@ -35,7 +35,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*; */ @Data @NoArgsConstructor -@Table(name = DEVICE_COLUMN_FAMILY_NAME) +@Table(name = EVENT_COLUMN_FAMILY_NAME) public class EventEntity implements BaseEntity { @Transient @@ -98,23 +98,7 @@ public class EventEntity implements BaseEntity { Event event = new Event(new EventId(id)); event.setCreatedTime(UUIDs.unixTimestamp(id)); event.setTenantId(new TenantId(tenantId)); - switch (entityType) { - case TENANT: - event.setEntityId(new TenantId(entityId)); - break; - case DEVICE: - event.setEntityId(new DeviceId(entityId)); - break; - case CUSTOMER: - event.setEntityId(new CustomerId(entityId)); - break; - case RULE: - event.setEntityId(new RuleId(entityId)); - break; - case PLUGIN: - event.setEntityId(new PluginId(entityId)); - break; - } + event.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); event.setBody(body); event.setType(eventType); event.setUid(eventUId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 9c9c66b376..9f78e27557 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -120,12 +120,39 @@ public class ModelConstants { public static final String DEVICE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY; public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String DEVICE_NAME_PROPERTY = "name"; + public static final String DEVICE_TYPE_PROPERTY = "type"; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text"; public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; + /** + * Cassandra asset constants. + */ + public static final String ASSET_COLUMN_FAMILY_NAME = "asset"; + public static final String ASSET_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY; + public static final String ASSET_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String ASSET_NAME_PROPERTY = "name"; + public static final String ASSET_TYPE_PROPERTY = "type"; + public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + + public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text"; + public static final String ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_and_search_text"; + public static final String ASSET_BY_TENANT_AND_NAME_VIEW_NAME = "asset_by_tenant_and_name"; + + /** + * Cassandra entity relation constants. + */ + public static final String RELATION_COLUMN_FAMILY_NAME = "relation"; + public static final String RELATION_FROM_ID_PROPERTY = "from_id"; + public static final String RELATION_FROM_TYPE_PROPERTY = "from_type"; + public static final String RELATION_TO_ID_PROPERTY = "to_id"; + public static final String RELATION_TO_TYPE_PROPERTY = "to_type"; + public static final String RELATION_TYPE_PROPERTY = "relation_type"; + + public static final String RELATION_REVERSE_VIEW_NAME = "reverse_relation"; + /** * Cassandra device_credentials constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java index 99560d2b8a..5e0eb399f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java @@ -15,13 +15,14 @@ */ package org.thingsboard.server.dao.plugin; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.PluginId; +import org.thingsboard.server.common.data.id.RuleId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; @@ -29,7 +30,9 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.PluginMetaData; +import org.thingsboard.server.common.data.rule.RuleMetaData; import org.thingsboard.server.dao.component.ComponentDescriptorService; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DatabaseException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -48,10 +51,11 @@ import java.util.stream.Collectors; import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.getData; +import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j -public class BasePluginService implements PluginService { +public class BasePluginService extends BaseEntityService implements PluginService { //TODO: move to a better place. public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID); @@ -108,6 +112,13 @@ public class BasePluginService implements PluginService { return getData(pluginDao.findById(pluginId)); } + @Override + public ListenableFuture findPluginByIdAsync(PluginId pluginId) { + validateId(pluginId, "Incorrect plugin id for search plugin request."); + ListenableFuture pluginEntity = pluginDao.findByIdAsync(pluginId.getId()); + return Futures.transform(pluginEntity, (com.google.common.base.Function) input -> getData(input)); + } + @Override public PluginMetaData findPluginByApiToken(String apiToken) { Validator.validateString(apiToken, "Incorrect plugin apiToken for search request."); @@ -205,6 +216,7 @@ public class BasePluginService implements PluginService { @Override public void deletePluginById(PluginId pluginId) { Validator.validateId(pluginId, "Incorrect plugin id for delete request."); + deleteEntityRelations(pluginId); checkRulesAndDelete(pluginId.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java index f8173ff39a..371a46e0fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.plugin; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.PluginId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; @@ -29,6 +30,8 @@ public interface PluginService { PluginMetaData findPluginById(PluginId pluginId); + ListenableFuture findPluginByIdAsync(PluginId pluginId); + PluginMetaData findPluginByApiToken(String apiToken); TextPageData findSystemPlugins(TextPageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java new file mode 100644 index 0000000000..5fd663212c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java @@ -0,0 +1,279 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import com.datastax.driver.core.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Function; +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.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.AbstractAsyncDao; +import org.thingsboard.server.dao.model.ModelConstants; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by ashvayka on 25.04.17. + */ +@Component +@Slf4j +public class BaseRelationDao extends AbstractAsyncDao implements RelationDao { + + private static final String SELECT_COLUMNS = "SELECT " + + ModelConstants.RELATION_FROM_ID_PROPERTY + "," + + ModelConstants.RELATION_FROM_TYPE_PROPERTY + "," + + ModelConstants.RELATION_TO_ID_PROPERTY + "," + + ModelConstants.RELATION_TO_TYPE_PROPERTY + "," + + ModelConstants.RELATION_TYPE_PROPERTY + "," + + ModelConstants.ADDITIONAL_INFO_PROPERTY; + public static final String FROM = " FROM "; + public static final String WHERE = " WHERE "; + public static final String AND = " AND "; + + private PreparedStatement saveStmt; + private PreparedStatement findAllByFromStmt; + private PreparedStatement findAllByFromAndTypeStmt; + private PreparedStatement findAllByToStmt; + private PreparedStatement findAllByToAndTypeStmt; + private PreparedStatement checkRelationStmt; + private PreparedStatement deleteStmt; + private PreparedStatement deleteAllByEntityStmt; + + @PostConstruct + public void init() { + super.startExecutor(); + } + + @Override + public ListenableFuture> findAllByFrom(EntityId from) { + BoundStatement stmt = getFindAllByFromStmt().bind().setUUID(0, from.getId()).setString(1, from.getEntityType().name()); + return executeAsyncRead(from, stmt); + } + + @Override + public ListenableFuture> findAllByFromAndType(EntityId from, String relationType) { + BoundStatement stmt = getFindAllByFromAndTypeStmt().bind() + .setUUID(0, from.getId()) + .setString(1, from.getEntityType().name()) + .setString(2, relationType); + return executeAsyncRead(from, stmt); + } + + @Override + public ListenableFuture> findAllByTo(EntityId to) { + BoundStatement stmt = getFindAllByToStmt().bind().setUUID(0, to.getId()).setString(1, to.getEntityType().name()); + return executeAsyncRead(to, stmt); + } + + @Override + public ListenableFuture> findAllByToAndType(EntityId to, String relationType) { + BoundStatement stmt = getFindAllByToAndTypeStmt().bind() + .setUUID(0, to.getId()) + .setString(1, to.getEntityType().name()) + .setString(2, relationType); + return executeAsyncRead(to, stmt); + } + + @Override + public ListenableFuture checkRelation(EntityId from, EntityId to, String relationType) { + BoundStatement stmt = getCheckRelationStmt().bind() + .setUUID(0, from.getId()) + .setString(1, from.getEntityType().name()) + .setUUID(2, to.getId()) + .setString(3, to.getEntityType().name()) + .setString(4, relationType); + return getFuture(executeAsyncRead(stmt), rs -> rs != null ? rs.one() != null : false); + } + + @Override + public ListenableFuture saveRelation(EntityRelation relation) { + BoundStatement stmt = getSaveStmt().bind() + .setUUID(0, relation.getFrom().getId()) + .setString(1, relation.getFrom().getEntityType().name()) + .setUUID(2, relation.getTo().getId()) + .setString(3, relation.getTo().getEntityType().name()) + .setString(4, relation.getType()) + .set(5, relation.getAdditionalInfo(), JsonNode.class); + ResultSetFuture future = executeAsyncWrite(stmt); + return getBooleanListenableFuture(future); + } + + @Override + public ListenableFuture deleteRelation(EntityRelation relation) { + return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType()); + } + + @Override + public ListenableFuture deleteRelation(EntityId from, EntityId to, String relationType) { + BoundStatement stmt = getDeleteStmt().bind() + .setUUID(0, from.getId()) + .setString(1, from.getEntityType().name()) + .setUUID(2, to.getId()) + .setString(3, to.getEntityType().name()) + .setString(4, relationType); + ResultSetFuture future = executeAsyncWrite(stmt); + return getBooleanListenableFuture(future); + } + + @Override + public ListenableFuture deleteOutboundRelations(EntityId entity) { + BoundStatement stmt = getDeleteAllByEntityStmt().bind() + .setUUID(0, entity.getId()) + .setString(1, entity.getEntityType().name()); + ResultSetFuture future = executeAsyncWrite(stmt); + return getBooleanListenableFuture(future); + } + + private PreparedStatement getSaveStmt() { + if (saveStmt == null) { + saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + + "(" + ModelConstants.RELATION_FROM_ID_PROPERTY + + "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY + + "," + ModelConstants.RELATION_TO_ID_PROPERTY + + "," + ModelConstants.RELATION_TO_TYPE_PROPERTY + + "," + ModelConstants.RELATION_TYPE_PROPERTY + + "," + ModelConstants.ADDITIONAL_INFO_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"); + } + return saveStmt; + } + + private PreparedStatement getDeleteStmt() { + if (deleteStmt == null) { + deleteStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + + WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" + + AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" + + AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" + + AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ?" + + AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ?"); + } + return deleteStmt; + } + + private PreparedStatement getDeleteAllByEntityStmt() { + if (deleteAllByEntityStmt == null) { + deleteAllByEntityStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + + WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" + + AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?"); + } + return deleteAllByEntityStmt; + } + + private PreparedStatement getFindAllByFromStmt() { + if (findAllByFromStmt == null) { + findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " + + FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + + WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? "); + } + return findAllByFromStmt; + } + + private PreparedStatement getFindAllByFromAndTypeStmt() { + if (findAllByFromAndTypeStmt == null) { + findAllByFromAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " + + FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + + WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? "); + } + return findAllByFromAndTypeStmt; + } + + private PreparedStatement getFindAllByToStmt() { + if (findAllByToStmt == null) { + findAllByToStmt = getSession().prepare(SELECT_COLUMNS + " " + + FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " + + WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? "); + } + return findAllByToStmt; + } + + private PreparedStatement getFindAllByToAndTypeStmt() { + if (findAllByToAndTypeStmt == null) { + findAllByToAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " + + FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " + + WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? "); + } + return findAllByToAndTypeStmt; + } + + private PreparedStatement getCheckRelationStmt() { + if (checkRelationStmt == null) { + checkRelationStmt = getSession().prepare(SELECT_COLUMNS + " " + + FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " + + WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " + + AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? "); + } + return checkRelationStmt; + } + + private EntityRelation getEntityRelation(Row row) { + EntityRelation relation = new EntityRelation(); + relation.setType(row.getString(ModelConstants.RELATION_TYPE_PROPERTY)); + relation.setAdditionalInfo(row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY, JsonNode.class)); + relation.setFrom(toEntity(row, ModelConstants.RELATION_FROM_ID_PROPERTY, ModelConstants.RELATION_FROM_TYPE_PROPERTY)); + relation.setTo(toEntity(row, ModelConstants.RELATION_TO_ID_PROPERTY, ModelConstants.RELATION_TO_TYPE_PROPERTY)); + return relation; + } + + private EntityId toEntity(Row row, String uuidColumn, String typeColumn) { + return EntityIdFactory.getByTypeAndUuid(row.getString(typeColumn), row.getUUID(uuidColumn)); + } + + private ListenableFuture> executeAsyncRead(EntityId from, BoundStatement stmt) { + log.debug("Generated query [{}] for entity {}", stmt, from); + return getFuture(executeAsyncRead(stmt), rs -> { + List rows = rs.all(); + List entries = new ArrayList<>(rows.size()); + if (!rows.isEmpty()) { + rows.forEach(row -> { + entries.add(getEntityRelation(row)); + }); + } + return entries; + }); + } + + private ListenableFuture getBooleanListenableFuture(ResultSetFuture rsFuture) { + return getFuture(rsFuture, rs -> rs != null ? rs.wasApplied() : false); + } + + private ListenableFuture getFuture(ResultSetFuture future, java.util.function.Function transformer) { + return Futures.transform(future, new Function() { + @Nullable + @Override + public T apply(@Nullable ResultSet input) { + return transformer.apply(input); + } + }, readResultsProcessingExecutor); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java new file mode 100644 index 0000000000..1b572c64ad --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -0,0 +1,257 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.exception.DataValidationException; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by ashvayka on 28.04.17. + */ +@Service +@Slf4j +public class BaseRelationService implements RelationService { + + @Autowired + private RelationDao relationDao; + + @Override + public ListenableFuture checkRelation(EntityId from, EntityId to, String relationType) { + log.trace("Executing checkRelation [{}][{}][{}]", from, to, relationType); + validate(from, to, relationType); + return relationDao.checkRelation(from, to, relationType); + } + + @Override + public ListenableFuture saveRelation(EntityRelation relation) { + log.trace("Executing saveRelation [{}]", relation); + validate(relation); + return relationDao.saveRelation(relation); + } + + @Override + public ListenableFuture deleteRelation(EntityRelation relation) { + log.trace("Executing deleteRelation [{}]", relation); + validate(relation); + return relationDao.deleteRelation(relation); + } + + @Override + public ListenableFuture deleteRelation(EntityId from, EntityId to, String relationType) { + log.trace("Executing deleteRelation [{}][{}][{}]", from, to, relationType); + validate(from, to, relationType); + return relationDao.deleteRelation(from, to, relationType); + } + + @Override + public ListenableFuture deleteEntityRelations(EntityId entity) { + log.trace("Executing deleteEntityRelations [{}]", entity); + validate(entity); + ListenableFuture> inboundRelations = relationDao.findAllByTo(entity); + ListenableFuture> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction, List>() { + @Override + public ListenableFuture> apply(List relations) throws Exception { + List> results = new ArrayList<>(); + for (EntityRelation relation : relations) { + results.add(relationDao.deleteRelation(relation)); + } + return Futures.allAsList(results); + } + }); + + ListenableFuture inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction()); + + ListenableFuture outboundFuture = relationDao.deleteOutboundRelations(entity); + + return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction()); + } + + @Override + public ListenableFuture> findByFrom(EntityId from) { + log.trace("Executing findByFrom [{}]", from); + validate(from); + return relationDao.findAllByFrom(from); + } + + @Override + public ListenableFuture> findByFromAndType(EntityId from, String relationType) { + log.trace("Executing findByFromAndType [{}][{}]", from, relationType); + validate(from); + validateType(relationType); + return relationDao.findAllByFromAndType(from, relationType); + } + + @Override + public ListenableFuture> findByTo(EntityId to) { + log.trace("Executing findByTo [{}]", to); + validate(to); + return relationDao.findAllByTo(to); + } + + @Override + public ListenableFuture> findByToAndType(EntityId to, String relationType) { + log.trace("Executing findByToAndType [{}][{}]", to, relationType); + validate(to); + validateType(relationType); + return relationDao.findAllByToAndType(to, relationType); + } + + @Override + public ListenableFuture> findByQuery(EntityRelationsQuery query) { + log.trace("Executing findByQuery [{}][{}]", query); + RelationsSearchParameters params = query.getParameters(); + final List filters = query.getFilters(); + if (filters == null || filters.isEmpty()) { + log.warn("Failed to query relations. Filters are not set [{}]", query); + throw new RuntimeException("Filters are not set!"); + } + + int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE; + + try { + ListenableFuture> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>()); + return Futures.transform(relationSet, (Function, List>) input -> { + List relations = new ArrayList<>(); + for (EntityRelation relation : input) { + for (EntityTypeFilter filter : filters) { + if (match(filter, relation, params.getDirection())) { + relations.add(relation); + break; + } + } + } + return relations; + }); + } catch (Exception e) { + log.warn("Failed to query relations: [{}]", query, e); + throw new RuntimeException(e); + } + } + + protected void validate(EntityRelation relation) { + if (relation == null) { + throw new DataValidationException("Relation type should be specified!"); + } + validate(relation.getFrom(), relation.getTo(), relation.getType()); + } + + protected void validate(EntityId from, EntityId to, String type) { + validateType(type); + if (from == null) { + throw new DataValidationException("Relation should contain from entity!"); + } + if (to == null) { + throw new DataValidationException("Relation should contain to entity!"); + } + } + + private void validateType(String type) { + if (StringUtils.isEmpty(type)) { + throw new DataValidationException("Relation type should be specified!"); + } + } + + protected void validate(EntityId entity) { + if (entity == null) { + throw new DataValidationException("Entity should be specified!"); + } + } + + private Function, Boolean> getListToBooleanFunction() { + return new Function, Boolean>() { + @Nullable + @Override + public Boolean apply(@Nullable List results) { + for (Boolean result : results) { + if (result == null || !result) { + return false; + } + } + return true; + } + }; + } + + private boolean match(EntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { + if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) { + if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) { + return true; + } else { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + return filter.getEntityTypes().contains(entityId.getEntityType()); + } + } else { + return false; + } + } + + private ListenableFuture> findRelationsRecursively(final EntityId rootId, final EntitySearchDirection direction, int lvl, final ConcurrentHashMap uniqueMap) throws Exception { + if (lvl == 0) { + return Futures.immediateFuture(Collections.emptySet()); + } + lvl--; + //TODO: try to remove this blocking operation + Set children = new HashSet<>(findRelations(rootId, direction).get()); + Set childrenIds = new HashSet<>(); + for (EntityRelation childRelation : children) { + log.info("Found Relation: {}", childRelation); + EntityId childId; + if (direction == EntitySearchDirection.FROM) { + childId = childRelation.getTo(); + } else { + childId = childRelation.getFrom(); + } + if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) { + log.info("Adding Relation: {}", childId); + if (childrenIds.add(childId)) { + log.info("Added Relation: {}", childId); + } + } + } + List>> futures = new ArrayList<>(); + for (EntityId entityId : childrenIds) { + futures.add(findRelationsRecursively(entityId, direction, lvl, uniqueMap)); + } + //TODO: try to remove this blocking operation + List> relations = Futures.successfulAsList(futures).get(); + relations.forEach(r -> r.forEach(d -> children.add(d))); + return Futures.immediateFuture(children); + } + + private ListenableFuture> findRelations(final EntityId rootId, final EntitySearchDirection direction) { + ListenableFuture> relations; + if (direction == EntitySearchDirection.FROM) { + relations = findByFrom(rootId); + } else { + relations = findByTo(rootId); + } + return relations; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java new file mode 100644 index 0000000000..b19e9e545c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import lombok.Data; + +import java.util.List; + +/** + * Created by ashvayka on 02.05.17. + */ +@Data +public class EntityRelationsQuery { + + private RelationsSearchParameters parameters; + private List filters; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java new file mode 100644 index 0000000000..64715b1221 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +/** + * Created by ashvayka on 02.05.17. + */ +public enum EntitySearchDirection { + + FROM, TO; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java new file mode 100644 index 0000000000..9618ecf599 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * Created by ashvayka on 02.05.17. + */ +@Data +@AllArgsConstructor +public class EntityTypeFilter { + @Nullable + private String relationType; + @Nullable + private List entityTypes; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java new file mode 100644 index 0000000000..df47259371 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; + +import java.util.List; + +/** + * Created by ashvayka on 25.04.17. + */ +public interface RelationDao { + + ListenableFuture> findAllByFrom(EntityId from); + + ListenableFuture> findAllByFromAndType(EntityId from, String relationType); + + ListenableFuture> findAllByTo(EntityId to); + + ListenableFuture> findAllByToAndType(EntityId to, String relationType); + + ListenableFuture checkRelation(EntityId from, EntityId to, String relationType); + + ListenableFuture saveRelation(EntityRelation relation); + + ListenableFuture deleteRelation(EntityRelation relation); + + ListenableFuture deleteRelation(EntityId from, EntityId to, String relationType); + + ListenableFuture deleteOutboundRelations(EntityId entity); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java new file mode 100644 index 0000000000..f4f3a37185 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; + +import java.util.List; + +/** + * Created by ashvayka on 27.04.17. + */ +public interface RelationService { + + ListenableFuture checkRelation(EntityId from, EntityId to, String relationType); + + ListenableFuture saveRelation(EntityRelation relation); + + ListenableFuture deleteRelation(EntityRelation relation); + + ListenableFuture deleteRelation(EntityId from, EntityId to, String relationType); + + ListenableFuture deleteEntityRelations(EntityId entity); + + ListenableFuture> findByFrom(EntityId from); + + ListenableFuture> findByFromAndType(EntityId from, String relationType); + + ListenableFuture> findByTo(EntityId to); + + ListenableFuture> findByToAndType(EntityId to, String relationType); + + ListenableFuture> findByQuery(EntityRelationsQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java new file mode 100644 index 0000000000..65920d7e26 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.relation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; + +import java.util.UUID; + +/** + * Created by ashvayka on 03.05.17. + */ +@Data +@AllArgsConstructor +public class RelationsSearchParameters { + + private UUID rootId; + private EntityType rootType; + private EntitySearchDirection direction; + private int maxLevel = 1; + + public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel) { + this.rootId = entityId.getId(); + this.rootType = entityId.getEntityType(); + this.direction = direction; + this.maxLevel = maxLevel; + } + + public EntityId getEntityId() { + return EntityIdFactory.getByTypeAndUuid(rootType, rootId); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java index e57c9f99d5..fb5e9ce146 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java @@ -17,10 +17,13 @@ package org.thingsboard.server.dao.rule; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.RuleId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; @@ -31,9 +34,11 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.PluginMetaData; import org.thingsboard.server.common.data.rule.RuleMetaData; import org.thingsboard.server.dao.component.ComponentDescriptorService; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DatabaseException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.AssetEntity; import org.thingsboard.server.dao.model.RuleMetaDataEntity; import org.thingsboard.server.dao.plugin.PluginService; import org.thingsboard.server.dao.service.DataValidator; @@ -53,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @Slf4j -public class BaseRuleService implements RuleService { +public class BaseRuleService extends BaseEntityService implements RuleService { private final TenantId systemTenantId = new TenantId(NULL_UUID); @@ -166,6 +171,13 @@ public class BaseRuleService implements RuleService { return getData(ruleDao.findById(ruleId.getId())); } + @Override + public ListenableFuture findRuleByIdAsync(RuleId ruleId) { + validateId(ruleId, "Incorrect rule id for search rule request."); + ListenableFuture ruleEntity = ruleDao.findByIdAsync(ruleId.getId()); + return Futures.transform(ruleEntity, (com.google.common.base.Function) input -> getData(input)); + } + @Override public List findPluginRules(String pluginToken) { List ruleEntities = ruleDao.findRulesByPlugin(pluginToken); @@ -235,6 +247,7 @@ public class BaseRuleService implements RuleService { @Override public void deleteRuleById(RuleId ruleId) { validateId(ruleId, "Incorrect rule id for delete rule request."); + deleteEntityRelations(ruleId); ruleDao.deleteById(ruleId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java index 98ff8c08ef..23f4a013b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.rule; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.RuleId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; @@ -29,6 +30,8 @@ public interface RuleService { RuleMetaData findRuleById(RuleId ruleId); + ListenableFuture findRuleByIdAsync(RuleId ruleId); + List findPluginRules(String pluginToken); TextPageData findSystemRules(TextPageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index 70e9860095..bb4912ea66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.service; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -24,6 +25,19 @@ import java.util.UUID; public class Validator { + /** + * This method validate EntityId entity id. If entity id is invalid than throw + * IncorrectParameterException exception + * + * @param entityId the entityId + * @param errorMessage the error message for exception + */ + public static void validateEntityId(EntityId entityId, String errorMessage) { + if (entityId == null || entityId.getId() == null) { + throw new IncorrectParameterException(errorMessage); + } + } + /** * This method validate String string. If string is invalid than throw * IncorrectParameterException exception diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index 7e56e19c4d..53a4ef9f22 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; @@ -22,16 +23,16 @@ import org.thingsboard.server.common.data.page.TextPageLink; public interface TenantService { - public Tenant findTenantById(TenantId tenantId); - - public Tenant saveTenant(Tenant tenant); + Tenant findTenantById(TenantId tenantId); + + ListenableFuture findTenantByIdAsync(TenantId customerId); - public void deleteTenant(TenantId tenantId); + Tenant saveTenant(Tenant tenant); - public TextPageData findTenants(TextPageLink pageLink); + void deleteTenant(TenantId tenantId); - //public TextPageData findTenantsByTitle(String title, PageLink pageLink); + TextPageData findTenants(TextPageLink pageLink); - public void deleteTenants(); + void deleteTenants(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 9deb828fa5..bcc31a835e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -17,11 +17,16 @@ package org.thingsboard.server.dao.tenant; import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.getData; +import static org.thingsboard.server.dao.service.Validator.validateId; import java.util.List; +import com.google.common.base.Function; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; @@ -29,15 +34,15 @@ import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.model.CustomerEntity; import org.thingsboard.server.dao.model.TenantEntity; import org.thingsboard.server.dao.plugin.PluginService; import org.thingsboard.server.dao.rule.RuleService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.user.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.service.Validator; @@ -45,19 +50,19 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; @Service @Slf4j -public class TenantServiceImpl implements TenantService { - +public class TenantServiceImpl extends BaseEntityService implements TenantService { + private static final String DEFAULT_TENANT_REGION = "Global"; @Autowired private TenantDao tenantDao; - + @Autowired private UserService userService; - + @Autowired private CustomerService customerService; - + @Autowired private DeviceService deviceService; @@ -72,7 +77,7 @@ public class TenantServiceImpl implements TenantService { @Autowired private PluginService pluginService; - + @Override public Tenant findTenantById(TenantId tenantId) { log.trace("Executing findTenantById [{}]", tenantId); @@ -81,6 +86,14 @@ public class TenantServiceImpl implements TenantService { return getData(tenantEntity); } + @Override + public ListenableFuture findTenantByIdAsync(TenantId tenantId) { + log.trace("Executing TenantIdAsync [{}]", tenantId); + validateId(tenantId, "Incorrect tenantId " + tenantId); + ListenableFuture tenantEntity = tenantDao.findByIdAsync(tenantId.getId()); + return Futures.transform(tenantEntity, (Function) input -> getData(input)); + } + @Override public Tenant saveTenant(Tenant tenant) { log.trace("Executing saveTenant [{}]", tenant); @@ -102,6 +115,7 @@ public class TenantServiceImpl implements TenantService { ruleService.deleteRulesByTenantId(tenantId); pluginService.deletePluginsByTenantId(tenantId); tenantDao.removeById(tenantId.getId()); + deleteEntityRelations(tenantId); } @Override @@ -131,10 +145,10 @@ public class TenantServiceImpl implements TenantService { } } }; - + private PaginatedRemover tenantsRemover = new PaginatedRemover() { - + @Override protected List findEntities(String region, TextPageLink pageLink) { return tenantDao.findTenantsByRegion(region, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java index cbf27fa9ac..26fe3fbb45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java @@ -26,6 +26,7 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.dao.AbstractAsyncDao; @@ -94,8 +95,8 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } @Override - public ListenableFuture> findAllAsync(String entityType, UUID entityId, List queries) { - List>> futures = queries.stream().map(query -> findAllAsync(entityType, entityId, query)).collect(Collectors.toList()); + public ListenableFuture> findAllAsync(EntityId entityId, List queries) { + List>> futures = queries.stream().map(query -> findAllAsync(entityId, query)).collect(Collectors.toList()); return Futures.transform(Futures.allAsList(futures), new Function>, List>() { @Nullable @Override @@ -108,9 +109,9 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } - private ListenableFuture> findAllAsync(String entityType, UUID entityId, TsKvQuery query) { + private ListenableFuture> findAllAsync(EntityId entityId, TsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { - return findAllAsyncWithLimit(entityType, entityId, query); + return findAllAsyncWithLimit(entityId, query); } else { long step = Math.max(query.getInterval(), minAggregationStepMs); long stepTs = query.getStartTs(); @@ -119,7 +120,7 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao long startTs = stepTs; long endTs = stepTs + step; TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation()); - futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); + futures.add(findAndAggregateAsync(entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); stepTs = endTs; } ListenableFuture>> future = Futures.allAsList(futures); @@ -133,11 +134,11 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } } - private ListenableFuture> findAllAsyncWithLimit(String entityType, UUID entityId, TsKvQuery query) { + private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, TsKvQuery query) { long minPartition = toPartitionTs(query.getStartTs()); long maxPartition = toPartitionTs(query.getEndTs()); - ResultSetFuture partitionsFuture = fetchPartitions(entityType, entityId, query.getKey(), minPartition, maxPartition); + ResultSetFuture partitionsFuture = fetchPartitions(entityId, query.getKey(), minPartition, maxPartition); final SimpleListenableFuture> resultFuture = new SimpleListenableFuture<>(); final ListenableFuture> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); @@ -145,13 +146,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao Futures.addCallback(partitionsListFuture, new FutureCallback>() { @Override public void onSuccess(@Nullable List partitions) { - TsKvQueryCursor cursor = new TsKvQueryCursor(entityType, entityId, query, partitions); + TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions); findAllAsyncSequentiallyWithLimit(cursor, resultFuture); } @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityType, entityId, minPartition, maxPartition, t); + log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); } }, readResultsProcessingExecutor); @@ -187,19 +188,19 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } } - private ListenableFuture> findAndAggregateAsync(String entityType, UUID entityId, TsKvQuery query, long minPartition, long maxPartition) { + private ListenableFuture> findAndAggregateAsync(EntityId entityId, TsKvQuery query, long minPartition, long maxPartition) { final Aggregation aggregation = query.getAggregation(); final String key = query.getKey(); final long startTs = query.getStartTs(); final long endTs = query.getEndTs(); final long ts = startTs + (endTs - startTs) / 2; - ResultSetFuture partitionsFuture = fetchPartitions(entityType, entityId, key, minPartition, maxPartition); + ResultSetFuture partitionsFuture = fetchPartitions(entityId, key, minPartition, maxPartition); ListenableFuture> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); ListenableFuture> aggregationChunks = Futures.transform(partitionsListFuture, - getFetchChunksAsyncFunction(entityType, entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor); + getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor); return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor); } @@ -209,21 +210,21 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).collect(Collectors.toList()); } - private AsyncFunction, List> getFetchChunksAsyncFunction(String entityType, UUID entityId, String key, Aggregation aggregation, long startTs, long endTs) { + private AsyncFunction, List> getFetchChunksAsyncFunction(EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) { return partitions -> { try { PreparedStatement proto = getFetchStmt(aggregation); List futures = new ArrayList<>(partitions.size()); for (Long partition : partitions) { - log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityType, entityId); + log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityId.getEntityType(), entityId.getId()); BoundStatement stmt = proto.bind(); - stmt.setString(0, entityType); - stmt.setUUID(1, entityId); + stmt.setString(0, entityId.getEntityType().name()); + stmt.setUUID(1, entityId.getId()); stmt.setString(2, key); stmt.setLong(3, partition); stmt.setLong(4, startTs); stmt.setLong(5, endTs); - log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId); + log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId()); futures.add(executeAsyncRead(stmt)); } return Futures.allAsList(futures); @@ -235,30 +236,30 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } @Override - public ResultSetFuture findLatest(String entityType, UUID entityId, String key) { + public ResultSetFuture findLatest(EntityId entityId, String key) { BoundStatement stmt = getFindLatestStmt().bind(); - stmt.setString(0, entityType); - stmt.setUUID(1, entityId); + stmt.setString(0, entityId.getEntityType().name()); + stmt.setUUID(1, entityId.getId()); stmt.setString(2, key); - log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId); + log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId()); return executeAsyncRead(stmt); } @Override - public ResultSetFuture findAllLatest(String entityType, UUID entityId) { + public ResultSetFuture findAllLatest(EntityId entityId) { BoundStatement stmt = getFindAllLatestStmt().bind(); - stmt.setString(0, entityType); - stmt.setUUID(1, entityId); - log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId); + stmt.setString(0, entityId.getEntityType().name()); + stmt.setUUID(1, entityId.getId()); + log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId()); return executeAsyncRead(stmt); } @Override - public ResultSetFuture save(String entityType, UUID entityId, long partition, TsKvEntry tsKvEntry) { + public ResultSetFuture save(EntityId entityId, long partition, TsKvEntry tsKvEntry) { DataType type = tsKvEntry.getDataType(); BoundStatement stmt = getSaveStmt(type).bind() - .setString(0, entityType) - .setUUID(1, entityId) + .setString(0, entityId.getEntityType().name()) + .setUUID(1, entityId.getId()) .setString(2, tsKvEntry.getKey()) .setLong(3, partition) .setLong(4, tsKvEntry.getTs()); @@ -267,11 +268,11 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } @Override - public ResultSetFuture saveLatest(String entityType, UUID entityId, TsKvEntry tsKvEntry) { + public ResultSetFuture saveLatest(EntityId entityId, TsKvEntry tsKvEntry) { DataType type = tsKvEntry.getDataType(); BoundStatement stmt = getLatestStmt(type).bind() - .setString(0, entityType) - .setUUID(1, entityId) + .setString(0, entityId.getEntityType().name()) + .setUUID(1, entityId.getId()) .setString(2, tsKvEntry.getKey()) .setLong(3, tsKvEntry.getTs()); addValue(tsKvEntry, stmt, 4); @@ -279,11 +280,11 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao } @Override - public ResultSetFuture savePartition(String entityType, UUID entityId, long partition, String key) { - log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityType, entityId, key); + public ResultSetFuture savePartition(EntityId entityId, long partition, String key) { + log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key); return executeAsyncWrite(getPartitionInsertStmt().bind() - .setString(0, entityType) - .setUUID(1, entityId) + .setString(0, entityId.getEntityType().name()) + .setUUID(1, entityId.getId()) .setLong(2, partition) .setString(3, key)); } @@ -339,9 +340,9 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao * Select existing partitions from the table * {@link ModelConstants#TS_KV_PARTITIONS_CF} for the given entity */ - private ResultSetFuture fetchPartitions(String entityType, UUID entityId, String key, long minPartition, long maxPartition) { - Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType)) - .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId)).and(eq(ModelConstants.KEY_COLUMN, key)); + private ResultSetFuture fetchPartitions(EntityId entityId, String key, long minPartition, long maxPartition) { + Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityId.getEntityType().name())) + .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId.getId())).and(eq(ModelConstants.KEY_COLUMN, key)); select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition)); select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition)); return executeAsyncRead(select); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index f27ed6e605..5d722f995f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -23,6 +23,7 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.BaseTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -59,30 +60,30 @@ public class BaseTimeseriesService implements TimeseriesService { private TimeseriesDao timeseriesDao; @Override - public ListenableFuture> findAll(String entityType, UUIDBased entityId, List queries) { - validate(entityType, entityId); + public ListenableFuture> findAll(EntityId entityId, List queries) { + validate(entityId); queries.forEach(query -> validate(query)); - return timeseriesDao.findAllAsync(entityType, entityId.getId(), queries); + return timeseriesDao.findAllAsync(entityId, queries); } @Override - public ListenableFuture> findLatest(String entityType, UUIDBased entityId, Collection keys) { - validate(entityType, entityId); + public ListenableFuture> findLatest(EntityId entityId, Collection keys) { + validate(entityId); List futures = Lists.newArrayListWithExpectedSize(keys.size()); keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); - keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityType, entityId.getId(), key))); + keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityId, key))); return Futures.allAsList(futures); } @Override - public ResultSetFuture findAllLatest(String entityType, UUIDBased entityId) { - validate(entityType, entityId); - return timeseriesDao.findAllLatest(entityType, entityId.getId()); + public ResultSetFuture findAllLatest(EntityId entityId) { + validate(entityId); + return timeseriesDao.findAllLatest(entityId); } @Override - public ListenableFuture> save(String entityType, UUIDBased entityId, TsKvEntry tsKvEntry) { - validate(entityType, entityId); + public ListenableFuture> save(EntityId entityId, TsKvEntry tsKvEntry) { + validate(entityId); if (tsKvEntry == null) { throw new IncorrectParameterException("Key value entry can't be null"); } @@ -90,13 +91,13 @@ public class BaseTimeseriesService implements TimeseriesService { long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs()); List futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); - saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs); + saveAndRegisterFutures(futures, entityId, tsKvEntry, partitionTs); return Futures.allAsList(futures); } @Override - public ListenableFuture> save(String entityType, UUIDBased entityId, List tsKvEntries) { - validate(entityType, entityId); + public ListenableFuture> save(EntityId entityId, List tsKvEntries) { + validate(entityId); List futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * INSERTS_PER_ENTRY); for (TsKvEntry tsKvEntry : tsKvEntries) { if (tsKvEntry == null) { @@ -104,7 +105,7 @@ public class BaseTimeseriesService implements TimeseriesService { } UUID uid = entityId.getId(); long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs()); - saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs); + saveAndRegisterFutures(futures, entityId, tsKvEntry, partitionTs); } return Futures.allAsList(futures); } @@ -119,15 +120,14 @@ public class BaseTimeseriesService implements TimeseriesService { return timeseriesDao.convertResultToTsKvEntryList(rs.all()); } - private void saveAndRegisterFutures(List futures, String entityType, TsKvEntry tsKvEntry, UUID uid, long partitionTs) { - futures.add(timeseriesDao.savePartition(entityType, uid, partitionTs, tsKvEntry.getKey())); - futures.add(timeseriesDao.saveLatest(entityType, uid, tsKvEntry)); - futures.add(timeseriesDao.save(entityType, uid, partitionTs, tsKvEntry)); + private void saveAndRegisterFutures(List futures, EntityId entityId, TsKvEntry tsKvEntry, long partitionTs) { + futures.add(timeseriesDao.savePartition(entityId, partitionTs, tsKvEntry.getKey())); + futures.add(timeseriesDao.saveLatest(entityId, tsKvEntry)); + futures.add(timeseriesDao.save(entityId, partitionTs, tsKvEntry)); } - private static void validate(String entityType, UUIDBased entityId) { - Validator.validateString(entityType, "Incorrect entityType " + entityType); - Validator.validateId(entityId, "Incorrect entityId " + entityId); + private static void validate(EntityId entityId) { + Validator.validateEntityId(entityId, "Incorrect entityId " + entityId); } private static void validate(TsKvQuery query) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java index 177003ddf2..1f9871eda1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.timeseries; import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.Row; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvQuery; @@ -33,17 +34,17 @@ public interface TimeseriesDao { long toPartitionTs(long ts); - ListenableFuture> findAllAsync(String entityType, UUID entityId, List queries); + ListenableFuture> findAllAsync(EntityId entityId, List queries); - ResultSetFuture findLatest(String entityType, UUID entityId, String key); + ResultSetFuture findLatest(EntityId entityId, String key); - ResultSetFuture findAllLatest(String entityType, UUID entityId); + ResultSetFuture findAllLatest(EntityId entityId); - ResultSetFuture save(String entityType, UUID entityId, long partition, TsKvEntry tsKvEntry); + ResultSetFuture save(EntityId entityId, long partition, TsKvEntry tsKvEntry); - ResultSetFuture savePartition(String entityType, UUID entityId, long partition, String key); + ResultSetFuture savePartition(EntityId entityId, long partition, String key); - ResultSetFuture saveLatest(String entityType, UUID entityId, TsKvEntry tsKvEntry); + ResultSetFuture saveLatest(EntityId entityId, TsKvEntry tsKvEntry); TsKvEntry convertResultToTsKvEntry(Row row); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index cd53e9468f..5c9c961b28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -20,6 +20,7 @@ import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.Row; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvQuery; @@ -33,15 +34,15 @@ import java.util.Set; */ public interface TimeseriesService { - ListenableFuture> findAll(String entityType, UUIDBased entityId, List queries); + ListenableFuture> findAll(EntityId entityId, List queries); - ListenableFuture> findLatest(String entityType, UUIDBased entityId, Collection keys); + ListenableFuture> findLatest(EntityId entityId, Collection keys); - ResultSetFuture findAllLatest(String entityType, UUIDBased entityId); + ResultSetFuture findAllLatest(EntityId entityId); - ListenableFuture> save(String entityType, UUIDBased entityId, TsKvEntry tsKvEntry); + ListenableFuture> save(EntityId entityId, TsKvEntry tsKvEntry); - ListenableFuture> save(String entityType, UUIDBased entityId, List tsKvEntry); + ListenableFuture> save(EntityId entityId, List tsKvEntry); TsKvEntry convertResultToTsKvEntry(Row row); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 9ad91021f1..64101110b4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -35,20 +35,19 @@ import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.BaseEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.*; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j -public class UserServiceImpl implements UserService { +public class UserServiceImpl extends BaseEntityService implements UserService { @Autowired private UserDao userDao; @@ -169,6 +168,7 @@ public class UserServiceImpl implements UserService { validateId(userId, "Incorrect userId " + userId); UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userId.getId()); userCredentialsDao.removeById(userCredentialsEntity.getId()); + deleteEntityRelations(userId); userDao.removeById(userId.getId()); } diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql index 6e4543026b..c3e7cdae64 100644 --- a/dao/src/main/resources/schema.cql +++ b/dao/src/main/resources/schema.cql @@ -156,6 +156,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.device ( tenant_id timeuuid, customer_id timeuuid, name text, + type text, search_text text, additional_info text, PRIMARY KEY (id, tenant_id, customer_id) @@ -202,6 +203,56 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_credent WHERE credentials_id IS NOT NULL AND id IS NOT NULL PRIMARY KEY ( credentials_id, id ); + +CREATE TABLE IF NOT EXISTS thingsboard.asset ( + id timeuuid, + tenant_id timeuuid, + customer_id timeuuid, + name text, + type text, + search_text text, + additional_info text, + PRIMARY KEY (id, tenant_id, customer_id) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_name AS + SELECT * + from thingsboard.asset + WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, name, id, customer_id) + WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_search_text AS + SELECT * + from thingsboard.asset + WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, search_text, id, customer_id) + WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_and_search_text AS + SELECT * + from thingsboard.asset + WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( customer_id, tenant_id, search_text, id ) + WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC ); + +CREATE TABLE IF NOT EXISTS thingsboard.relation ( + from_id timeuuid, + from_type text, + to_id timeuuid, + to_type text, + relation_type text, + additional_info text, + PRIMARY KEY ((from_id, from_type), relation_type, to_id, to_type) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS +SELECT * +from thingsboard.relation +WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL +PRIMARY KEY ((to_id, to_type), relation_type, from_id, from_type) +WITH CLUSTERING ORDER BY ( relation_type ASC, from_id ASC, from_type ASC); + CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle ( id timeuuid, tenant_id timeuuid, diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index f66e6eee69..903207c1d5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -47,6 +47,7 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.plugin.PluginService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TenantService; @@ -110,6 +111,9 @@ public abstract class AbstractServiceTest { @Autowired protected EventService eventService; + @Autowired + protected RelationService relationService; + @Autowired private ComponentDescriptorService componentDescriptorService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java new file mode 100644 index 0000000000..3f5c55affd --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java @@ -0,0 +1,283 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import com.datastax.driver.core.utils.UUIDs; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.relation.EntityRelationsQuery; +import org.thingsboard.server.dao.relation.EntitySearchDirection; +import org.thingsboard.server.dao.relation.EntityTypeFilter; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class RelationServiceImplTest extends AbstractServiceTest { + + @Before + public void before() { + } + + @After + public void after() { + } + + @Test + public void testSaveRelation() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(UUIDs.timeBased()); + AssetId childId = new AssetId(UUIDs.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(saveRelation(relation)); + + Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get()); + + Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").get()); + } + + @Test + public void testDeleteRelation() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(UUIDs.timeBased()); + AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId subChildId = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + saveRelation(relationB); + + Assert.assertTrue(relationService.deleteRelation(relationA).get()); + + Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get()); + } + + @Test + public void testDeleteEntityRelations() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(UUIDs.timeBased()); + AssetId childId = new AssetId(UUIDs.timeBased()); + AssetId subChildId = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + saveRelation(relationB); + + Assert.assertTrue(relationService.deleteEntityRelations(childId).get()); + + Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get()); + + Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get()); + } + + @Test + public void testFindFrom() throws ExecutionException, InterruptedException { + AssetId parentA = new AssetId(UUIDs.timeBased()); + AssetId parentB = new AssetId(UUIDs.timeBased()); + AssetId childA = new AssetId(UUIDs.timeBased()); + AssetId childB = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE); + EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE); + + EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE); + EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE); + + saveRelation(relationA1); + saveRelation(relationA2); + + saveRelation(relationB1); + saveRelation(relationB2); + + List relations = relationService.findByFrom(parentA).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(EntityRelation.CONTAINS_TYPE, relation.getType()); + Assert.assertEquals(parentA, relation.getFrom()); + Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo())); + } + + relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(2, relations.size()); + + relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByFrom(parentB).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(EntityRelation.MANAGES_TYPE, relation.getType()); + Assert.assertEquals(parentB, relation.getFrom()); + Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo())); + } + + relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(0, relations.size()); + } + + private Boolean saveRelation(EntityRelation relationA1) throws ExecutionException, InterruptedException { + return relationService.saveRelation(relationA1).get(); + } + + @Test + public void testFindTo() throws ExecutionException, InterruptedException { + AssetId parentA = new AssetId(UUIDs.timeBased()); + AssetId parentB = new AssetId(UUIDs.timeBased()); + AssetId childA = new AssetId(UUIDs.timeBased()); + AssetId childB = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE); + EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE); + + EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE); + EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE); + + saveRelation(relationA1); + saveRelation(relationA2); + + saveRelation(relationB1); + saveRelation(relationB2); + + // Data propagation to views is async + Thread.sleep(3000); + + List relations = relationService.findByTo(childA).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(childA, relation.getTo()); + Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom())); + } + + relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE).get(); + Assert.assertEquals(1, relations.size()); + + relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(1, relations.size()); + + relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE).get(); + Assert.assertEquals(0, relations.size()); + + relations = relationService.findByTo(childB).get(); + Assert.assertEquals(2, relations.size()); + for (EntityRelation relation : relations) { + Assert.assertEquals(childB, relation.getTo()); + Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom())); + } + } + + @Test + public void testCyclicRecursiveRelation() throws ExecutionException, InterruptedException { + // A -> B -> C -> A + AssetId assetA = new AssetId(UUIDs.timeBased()); + AssetId assetB = new AssetId(UUIDs.timeBased()); + AssetId assetC = new AssetId(UUIDs.timeBased()); + + EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE); + EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); + EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + saveRelation(relationB); + saveRelation(relationC); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1)); + query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + List relations = relationService.findByQuery(query).get(); + Assert.assertEquals(3, relations.size()); + Assert.assertTrue(relations.contains(relationA)); + Assert.assertTrue(relations.contains(relationB)); + Assert.assertTrue(relations.contains(relationC)); + } + + @Test + public void testRecursiveRelation() throws ExecutionException, InterruptedException { + // A -> B -> [C,D] + AssetId assetA = new AssetId(UUIDs.timeBased()); + AssetId assetB = new AssetId(UUIDs.timeBased()); + AssetId assetC = new AssetId(UUIDs.timeBased()); + DeviceId deviceD = new DeviceId(UUIDs.timeBased()); + + EntityRelation relationAB = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE); + EntityRelation relationBC = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); + EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE); + + + saveRelation(relationAB); + saveRelation(relationBC); + saveRelation(relationBD); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1)); + query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + List relations = relationService.findByQuery(query).get(); + Assert.assertEquals(2, relations.size()); + Assert.assertTrue(relations.contains(relationAB)); + Assert.assertTrue(relations.contains(relationBC)); + } + + + @Test(expected = DataValidationException.class) + public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException { + EntityRelation relation = new EntityRelation(); + relation.setTo(new AssetId(UUIDs.timeBased())); + relation.setType(EntityRelation.CONTAINS_TYPE); + Assert.assertTrue(saveRelation(relation)); + } + + @Test(expected = DataValidationException.class) + public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException { + EntityRelation relation = new EntityRelation(); + relation.setFrom(new AssetId(UUIDs.timeBased())); + relation.setType(EntityRelation.CONTAINS_TYPE); + Assert.assertTrue(saveRelation(relation)); + } + + @Test(expected = DataValidationException.class) + public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException { + EntityRelation relation = new EntityRelation(); + relation.setFrom(new AssetId(UUIDs.timeBased())); + relation.setTo(new AssetId(UUIDs.timeBased())); + Assert.assertTrue(saveRelation(relation)); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java index 134f4710c7..8ced7ea31c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java @@ -66,7 +66,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS); - ResultSetFuture rsFuture = tsService.findAllLatest(DataConstants.DEVICE, deviceId); + ResultSetFuture rsFuture = tsService.findAllLatest(deviceId); List tsList = tsService.convertResultSetToTsKvEntryList(rsFuture.get()); assertNotNull(tsList); @@ -95,7 +95,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS); - List rs = tsService.findLatest(DataConstants.DEVICE, deviceId, Collections.singleton(STRING_KEY)).get(); + List rs = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get(); Assert.assertEquals(1, rs.size()); Assert.assertEquals(toTsEntry(TS, stringKvEntry), tsService.convertResultToTsKvEntry(rs.get(0).one())); } @@ -114,7 +114,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { entries.add(save(deviceId, 45000, 500)); entries.add(save(deviceId, 55000, 600)); - List list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, + List list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 60000, 20000, 3, Aggregation.NONE))).get(); assertEquals(3, list.size()); assertEquals(55000, list.get(0).getTs()); @@ -126,7 +126,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { assertEquals(35000, list.get(2).getTs()); assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue()); - list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, + list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 60000, 20000, 3, Aggregation.AVG))).get(); assertEquals(3, list.size()); assertEquals(10000, list.get(0).getTs()); @@ -138,7 +138,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { assertEquals(50000, list.get(2).getTs()); assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue()); - list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, + list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 60000, 20000, 3, Aggregation.SUM))).get(); assertEquals(3, list.size()); @@ -151,7 +151,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { assertEquals(50000, list.get(2).getTs()); assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue()); - list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, + list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 60000, 20000, 3, Aggregation.MIN))).get(); assertEquals(3, list.size()); @@ -164,7 +164,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { assertEquals(50000, list.get(2).getTs()); assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue()); - list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, + list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 60000, 20000, 3, Aggregation.MAX))).get(); assertEquals(3, list.size()); @@ -177,7 +177,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { assertEquals(50000, list.get(2).getTs()); assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue()); - list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, + list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 60000, 20000, 3, Aggregation.COUNT))).get(); assertEquals(3, list.size()); @@ -193,15 +193,15 @@ public class TimeseriesServiceTest extends AbstractServiceTest { private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception { TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value)); - tsService.save(DataConstants.DEVICE, deviceId, entry).get(); + tsService.save(deviceId, entry).get(); return entry; } private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException { - tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, stringKvEntry)).get(); - tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, longKvEntry)).get(); - tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, doubleKvEntry)).get(); - tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, booleanKvEntry)).get(); + tsService.save(deviceId, toTsEntry(ts, stringKvEntry)).get(); + tsService.save(deviceId, toTsEntry(ts, longKvEntry)).get(); + tsService.save(deviceId, toTsEntry(ts, doubleKvEntry)).get(); + tsService.save(deviceId, toTsEntry(ts, booleanKvEntry)).get(); } private static TsKvEntry toTsEntry(long ts, KvEntry entry) { diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java index 7b7d0ec811..b1288d93d4 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java @@ -19,4 +19,9 @@ package org.thingsboard.server.extensions.api.exception; * Created by ashvayka on 21.02.17. */ public class UnauthorizedException extends Exception { + + public UnauthorizedException(String message) { + super(message); + } + } diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java index 988410d794..2477ac3d37 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java @@ -49,7 +49,7 @@ public interface PluginContext { Device RPC API */ - Optional resolve(DeviceId deviceId); + Optional resolve(EntityId entityId); void getDevice(DeviceId deviceId, PluginCallback pluginCallback); @@ -77,33 +77,33 @@ public interface PluginContext { */ - void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback callback); + void saveTsData(EntityId entityId, TsKvEntry entry, PluginCallback callback); - void saveTsData(DeviceId deviceId, List entry, PluginCallback callback); + void saveTsData(EntityId entityId, List entry, PluginCallback callback); - void loadTimeseries(DeviceId deviceId, List queries, PluginCallback> callback); + void loadTimeseries(EntityId entityId, List queries, PluginCallback> callback); - void loadLatestTimeseries(DeviceId deviceId, Collection keys, PluginCallback> callback); + void loadLatestTimeseries(EntityId entityId, Collection keys, PluginCallback> callback); - void loadLatestTimeseries(DeviceId deviceId, PluginCallback> callback); + void loadLatestTimeseries(EntityId entityId, PluginCallback> callback); /* Attributes API */ - void saveAttributes(TenantId tenantId, DeviceId deviceId, String attributeType, List attributes, PluginCallback callback); + void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List attributes, PluginCallback callback); - void removeAttributes(TenantId tenantId, DeviceId deviceId, String scope, List attributeKeys, PluginCallback callback); + void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List attributeKeys, PluginCallback callback); - void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, PluginCallback> callback); + void loadAttribute(EntityId entityId, String attributeType, String attributeKey, PluginCallback> callback); - void loadAttributes(DeviceId deviceId, String attributeType, Collection attributeKeys, PluginCallback> callback); + void loadAttributes(EntityId entityId, String attributeType, Collection attributeKeys, PluginCallback> callback); - void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback> callback); + void loadAttributes(EntityId entityId, String attributeType, PluginCallback> callback); - void loadAttributes(DeviceId deviceId, Collection attributeTypes, PluginCallback> callback); + void loadAttributes(EntityId entityId, Collection attributeTypes, PluginCallback> callback); - void loadAttributes(DeviceId deviceId, Collection attributeTypes, Collection attributeKeys, PluginCallback> callback); + void loadAttributes(EntityId entityId, Collection attributeTypes, Collection attributeKeys, PluginCallback> callback); void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback> callback); } diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml index e9d0e18d9a..b1b874471e 100644 --- a/extensions-core/pom.xml +++ b/extensions-core/pom.xml @@ -41,6 +41,11 @@ extensions-api provided + + org.thingsboard.common + transport + provided + com.google.code.gson gson diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java index 8cb1bf364e..60f46cee3f 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java @@ -19,6 +19,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.extensions.api.plugins.PluginCallback; @@ -39,7 +40,7 @@ import java.util.function.Function; @Slf4j public class SubscriptionManager { - private final Map> subscriptionsByDeviceId = new HashMap<>(); + private final Map> subscriptionsByEntityId = new HashMap<>(); private final Map> subscriptionsByWsSessionId = new HashMap<>(); @@ -48,28 +49,28 @@ public class SubscriptionManager { @Setter private TelemetryRpcMsgHandler rpcHandler; - public void addLocalWsSubscription(PluginContext ctx, String sessionId, DeviceId deviceId, SubscriptionState sub) { - Optional server = ctx.resolve(deviceId); + public void addLocalWsSubscription(PluginContext ctx, String sessionId, EntityId entityId, SubscriptionState sub) { + Optional server = ctx.resolve(entityId); Subscription subscription; if (server.isPresent()) { ServerAddress address = server.get(); - log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), deviceId, address); + log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address); subscription = new Subscription(sub, true, address); rpcHandler.onNewSubscription(ctx, address, sessionId, subscription); } else { - log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), deviceId); + log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId); subscription = new Subscription(sub, true); } - registerSubscription(sessionId, deviceId, subscription); + registerSubscription(sessionId, entityId, subscription); } public void addRemoteWsSubscription(PluginContext ctx, ServerAddress address, String sessionId, Subscription subscription) { - DeviceId deviceId = subscription.getDeviceId(); - log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address); - registerSubscription(sessionId, deviceId, subscription); + EntityId entityId = subscription.getEntityId(); + log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), entityId, address); + registerSubscription(sessionId, entityId, subscription); if (subscription.getType() == SubscriptionType.ATTRIBUTES) { final Map keyStates = subscription.getKeyStates(); - ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback>() { + ctx.loadAttributes(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List values) { List missedUpdates = new ArrayList<>(); @@ -95,7 +96,7 @@ public class SubscriptionManager { queries.add(new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs)); }); - ctx.loadTimeseries(deviceId, queries, new PluginCallback>() { + ctx.loadTimeseries(entityId, queries, new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List missedUpdates) { if (!missedUpdates.isEmpty()) { @@ -112,11 +113,11 @@ public class SubscriptionManager { } - private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) { - Set deviceSubscriptions = subscriptionsByDeviceId.get(subscription.getDeviceId()); + private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) { + Set deviceSubscriptions = subscriptionsByEntityId.get(subscription.getEntityId()); if (deviceSubscriptions == null) { deviceSubscriptions = new HashSet<>(); - subscriptionsByDeviceId.put(deviceId, deviceSubscriptions); + subscriptionsByEntityId.put(entityId, deviceSubscriptions); } deviceSubscriptions.add(subscription); Map sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId); @@ -133,7 +134,7 @@ public class SubscriptionManager { if (sessionSubscriptions != null) { Subscription subscription = sessionSubscriptions.remove(subscriptionId); if (subscription != null) { - DeviceId deviceId = subscription.getDeviceId(); + EntityId entityId = subscription.getEntityId(); if (subscription.isLocal() && subscription.getServer() != null) { rpcHandler.onSubscriptionClose(ctx, subscription.getServer(), sessionId, subscription.getSubscriptionId()); } @@ -143,13 +144,13 @@ public class SubscriptionManager { } else { log.debug("[{}] Removed session subscription.", sessionId); } - Set deviceSubscriptions = subscriptionsByDeviceId.get(deviceId); + Set deviceSubscriptions = subscriptionsByEntityId.get(entityId); if (deviceSubscriptions != null) { boolean result = deviceSubscriptions.remove(subscription); if (result) { if (deviceSubscriptions.size() == 0) { log.debug("[{}] Removed last subscription for particular device.", sessionId); - subscriptionsByDeviceId.remove(deviceId); + subscriptionsByEntityId.remove(entityId); } else { log.debug("[{}] Removed device subscription.", sessionId); } @@ -167,8 +168,8 @@ public class SubscriptionManager { } } - public void onLocalSubscriptionUpdate(PluginContext ctx, DeviceId deviceId, SubscriptionType type, Function> f) { - Set deviceSubscriptions = subscriptionsByDeviceId.get(deviceId); + public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function> f) { + Set deviceSubscriptions = subscriptionsByEntityId.get(entityId); if (deviceSubscriptions != null) { deviceSubscriptions.stream().filter(s -> type == s.getType()).forEach(s -> { String sessionId = s.getWsSessionId(); @@ -184,7 +185,7 @@ public class SubscriptionManager { } }); } else { - log.debug("[{}] No device subscriptions to process!", deviceId); + log.debug("[{}] No device subscriptions to process!", entityId); } } @@ -197,10 +198,10 @@ public class SubscriptionManager { } } - public void onAttributesUpdateFromServer(PluginContext ctx, DeviceId deviceId, String scope, List attributes) { - Optional serverAddress = ctx.resolve(deviceId); + public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List attributes) { + Optional serverAddress = ctx.resolve(entityId); if (!serverAddress.isPresent()) { - onLocalSubscriptionUpdate(ctx, deviceId, SubscriptionType.ATTRIBUTES, s -> { + onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.ATTRIBUTES, s -> { List subscriptionUpdate = new ArrayList(); for (AttributeKvEntry kv : attributes) { if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) { @@ -210,7 +211,24 @@ public class SubscriptionManager { return subscriptionUpdate; }); } else { - rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), deviceId, scope, attributes); + rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), entityId, scope, attributes); + } + } + + public void onTimeseriesUpdateFromServer(PluginContext ctx, EntityId entityId, List entries) { + Optional serverAddress = ctx.resolve(entityId); + if (!serverAddress.isPresent()) { + onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.TIMESERIES, s -> { + List subscriptionUpdate = new ArrayList(); + for (TsKvEntry kv : entries) { + if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) { + subscriptionUpdate.add(kv); + } + } + return subscriptionUpdate; + }); + } else { + rpcHandler.onTimeseriesUpdate(ctx, serverAddress.get(), entityId, entries); } } @@ -243,11 +261,11 @@ public class SubscriptionManager { int sessionSubscriptionSize = sessionSubscriptions.size(); for (Subscription subscription : sessionSubscriptions.values()) { - DeviceId deviceId = subscription.getDeviceId(); - Set deviceSubscriptions = subscriptionsByDeviceId.get(deviceId); + EntityId entityId = subscription.getEntityId(); + Set deviceSubscriptions = subscriptionsByEntityId.get(entityId); deviceSubscriptions.remove(subscription); if (deviceSubscriptions.isEmpty()) { - subscriptionsByDeviceId.remove(deviceId); + subscriptionsByEntityId.remove(entityId); } } subscriptionsByWsSessionId.remove(sessionId); @@ -272,9 +290,9 @@ public class SubscriptionManager { public void onClusterUpdate(PluginContext ctx) { log.trace("Processing cluster onUpdate msg!"); - Iterator>> deviceIterator = subscriptionsByDeviceId.entrySet().iterator(); + Iterator>> deviceIterator = subscriptionsByEntityId.entrySet().iterator(); while (deviceIterator.hasNext()) { - Map.Entry> e = deviceIterator.next(); + Map.Entry> e = deviceIterator.next(); Set subscriptions = e.getValue(); Optional newAddressOptional = ctx.resolve(e.getKey()); if (newAddressOptional.isPresent()) { @@ -317,6 +335,6 @@ public class SubscriptionManager { public void clear() { subscriptionsByWsSessionId.clear(); - subscriptionsByDeviceId.clear(); + subscriptionsByEntityId.clear(); } } \ No newline at end of file diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java index 145f8c483a..9555dd640d 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java @@ -28,7 +28,8 @@ import lombok.NoArgsConstructor; public class GetHistoryCmd implements TelemetryPluginCmd { private int cmdId; - private String deviceId; + private String entityType; + private String entityId; private String keys; private long startTs; private long endTs; diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java index 3574eae0ec..0b69b89cb8 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java @@ -26,7 +26,8 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT public abstract class SubscriptionCmd implements TelemetryPluginCmd { private int cmdId; - private String deviceId; + private String entityType; + private String entityId; private String keys; private String scope; private boolean unsubscribe; @@ -35,7 +36,7 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd { @Override public String toString() { - return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; + return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java new file mode 100644 index 0000000000..d7340c610d --- /dev/null +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2017 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.extensions.core.plugin.telemetry.handlers; + +/** + * Created by ashvayka on 08.05.17. + */ +public enum TelemetryFeature { + + ATTRIBUTES, TIMESERIES; + + public static TelemetryFeature forName(String name) { + return TelemetryFeature.valueOf(name.toUpperCase()); + } + +} diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java index 633f620e97..5b32474b17 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java @@ -16,13 +16,19 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.handlers; import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.*; +import org.thingsboard.server.common.msg.core.TelemetryUploadRequest; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.extensions.api.plugins.PluginCallback; import org.thingsboard.server.extensions.api.plugins.PluginContext; import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler; @@ -50,121 +56,116 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException { RestRequest request = msg.getRequest(); String[] pathParams = request.getPathParams(); - if (pathParams.length >= 3) { - String deviceIdStr = pathParams[0]; - String method = pathParams[1]; - String entity = pathParams[2]; - String scope = pathParams.length >= 4 ? pathParams[3] : null; - if (StringUtils.isEmpty(method) || StringUtils.isEmpty(entity) || StringUtils.isEmpty(deviceIdStr)) { - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); - return; - } + if (pathParams.length < 4) { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); + return; + } - DeviceId deviceId = DeviceId.fromString(deviceIdStr); + String entityType = pathParams[0]; + String entityIdStr = pathParams[1]; + String method = pathParams[2]; + TelemetryFeature feature = TelemetryFeature.forName(pathParams[3]); + String scope = pathParams.length >= 5 ? pathParams[4] : null; + if (StringUtils.isEmpty(entityType) || EntityType.valueOf(entityType) == null || StringUtils.isEmpty(entityIdStr) || StringUtils.isEmpty(method) || StringUtils.isEmpty(feature)) { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); + return; + } - if (method.equals("keys")) { - if (entity.equals("timeseries")) { - ctx.loadLatestTimeseries(deviceId, new PluginCallback>() { - @Override - public void onSuccess(PluginContext ctx, List value) { - List keys = value.stream().map(tsKv -> tsKv.getKey()).collect(Collectors.toList()); - msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK)); - } + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - @Override - public void onFailure(PluginContext ctx, Exception e) { - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); - } - }); - } else if (entity.equals("attributes")) { - PluginCallback> callback = getAttributeKeysPluginCallback(msg); - if (!StringUtils.isEmpty(scope)) { - ctx.loadAttributes(deviceId, scope, callback); - } else { - ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback); + if (method.equals("keys")) { + if (feature == TelemetryFeature.TIMESERIES) { + ctx.loadLatestTimeseries(entityId, new PluginCallback>() { + @Override + public void onSuccess(PluginContext ctx, List value) { + List keys = value.stream().map(tsKv -> tsKv.getKey()).collect(Collectors.toList()); + msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK)); + } + + @Override + public void onFailure(PluginContext ctx, Exception e) { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); } + }); + } else if (feature == TelemetryFeature.ATTRIBUTES) { + PluginCallback> callback = getAttributeKeysPluginCallback(msg); + if (!StringUtils.isEmpty(scope)) { + ctx.loadAttributes(entityId, scope, callback); + } else { + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), callback); } - } else if (method.equals("values")) { - if ("timeseries".equals(entity)) { - String keysStr = request.getParameter("keys"); - List keys = Arrays.asList(keysStr.split(",")); + } + } else if (method.equals("values")) { + if (feature == TelemetryFeature.TIMESERIES) { + String keysStr = request.getParameter("keys"); + List keys = Arrays.asList(keysStr.split(",")); - Optional startTs = request.getLongParamValue("startTs"); - Optional endTs = request.getLongParamValue("endTs"); - Optional interval = request.getLongParamValue("interval"); - Optional limit = request.getIntParamValue("limit"); + Optional startTs = request.getLongParamValue("startTs"); + Optional endTs = request.getLongParamValue("endTs"); + Optional interval = request.getLongParamValue("interval"); + Optional limit = request.getIntParamValue("limit"); - if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) { - if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) { - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); - return; - } - Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); + if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) { + if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); + return; + } + Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); + + List queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)) + .collect(Collectors.toList()); + ctx.loadTimeseries(entityId, queries, getTsKvListCallback(msg)); + } else { + ctx.loadLatestTimeseries(entityId, keys, getTsKvListCallback(msg)); + } + } else if (feature == TelemetryFeature.ATTRIBUTES) { + String keys = request.getParameter("keys", ""); - List queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)) - .collect(Collectors.toList()); - ctx.loadTimeseries(deviceId, queries, getTsKvListCallback(msg)); + PluginCallback> callback = getAttributeValuesPluginCallback(msg); + if (!StringUtils.isEmpty(scope)) { + if (!StringUtils.isEmpty(keys)) { + List keyList = Arrays.asList(keys.split(",")); + ctx.loadAttributes(entityId, scope, keyList, callback); } else { - ctx.loadLatestTimeseries(deviceId, keys, getTsKvListCallback(msg)); + ctx.loadAttributes(entityId, scope, callback); } - } else if ("attributes".equals(entity)) { - String keys = request.getParameter("keys", ""); - - PluginCallback> callback = getAttributeValuesPluginCallback(msg); - if (!StringUtils.isEmpty(scope)) { - if (!StringUtils.isEmpty(keys)) { - List keyList = Arrays.asList(keys.split(",")); - ctx.loadAttributes(deviceId, scope, keyList, callback); - } else { - ctx.loadAttributes(deviceId, scope, callback); - } + } else { + if (!StringUtils.isEmpty(keys)) { + List keyList = Arrays.asList(keys.split(",")); + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), keyList, callback); } else { - if (!StringUtils.isEmpty(keys)) { - List keyList = Arrays.asList(keys.split(",")); - ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keyList, callback); - } else { - ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback); - } + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), callback); } } } - } else { - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); } } - private PluginCallback> getTsKvListCallback(final PluginRestMsg msg) { - return new PluginCallback>() { - @Override - public void onSuccess(PluginContext ctx, List data) { - Map> result = new LinkedHashMap<>(); - for (TsKvEntry entry : data) { - List vList = result.get(entry.getKey()); - if (vList == null) { - vList = new ArrayList<>(); - result.put(entry.getKey(), vList); - } - vList.add(new TsData(entry.getTs(), entry.getValueAsString())); - } - msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK)); - } - - @Override - public void onFailure(PluginContext ctx, Exception e) { - log.error("Failed to fetch historical data", e); - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); - } - }; - } - @Override public void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException { RestRequest request = msg.getRequest(); try { String[] pathParams = request.getPathParams(); + EntityId entityId; + String scope; + TelemetryFeature feature; if (pathParams.length == 2) { - DeviceId deviceId = DeviceId.fromString(pathParams[0]); - String scope = pathParams[1]; + entityId = DeviceId.fromString(pathParams[0]); + scope = pathParams[1]; + feature = TelemetryFeature.ATTRIBUTES; + } else if (pathParams.length == 3) { + entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]); + scope = pathParams[2]; + feature = TelemetryFeature.ATTRIBUTES; + } else if (pathParams.length == 4) { + entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]); + feature = TelemetryFeature.forName(pathParams[2].toUpperCase()); + scope = pathParams[3]; + } else { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); + return; + } + if (feature == TelemetryFeature.ATTRIBUTES) { if (DataConstants.SERVER_SCOPE.equals(scope) || DataConstants.SHARED_SCOPE.equals(scope)) { JsonNode jsonNode = jsonMapper.readTree(request.getRequestBody()); @@ -185,11 +186,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { } }); if (attributes.size() > 0) { - ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, attributes, new PluginCallback() { + ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), entityId, scope, attributes, new PluginCallback() { @Override public void onSuccess(PluginContext ctx, Void value) { msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); - subscriptionManager.onAttributesUpdateFromServer(ctx, deviceId, scope, attributes); + subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes); } @Override @@ -202,6 +203,28 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { } } } + } else if (feature == TelemetryFeature.TIMESERIES) { + TelemetryUploadRequest telemetryRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(request.getRequestBody())); + List entries = new ArrayList<>(); + for (Map.Entry> entry : telemetryRequest.getData().entrySet()) { + for (KvEntry kv : entry.getValue()) { + entries.add(new BasicTsKvEntry(entry.getKey(), kv)); + } + } + ctx.saveTsData(entityId, entries, new PluginCallback() { + @Override + public void onSuccess(PluginContext ctx, Void value) { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); + subscriptionManager.onTimeseriesUpdateFromServer(ctx, entityId, entries); + } + + @Override + public void onFailure(PluginContext ctx, Exception e) { + log.error("Failed to save attributes", e); + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + }); + return; } } catch (IOException | RuntimeException e) { log.debug("Failed to process POST request due to exception", e); @@ -214,29 +237,38 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { RestRequest request = msg.getRequest(); try { String[] pathParams = request.getPathParams(); + EntityId entityId; + String scope; if (pathParams.length == 2) { - DeviceId deviceId = DeviceId.fromString(pathParams[0]); - String scope = pathParams[1]; - if (DataConstants.SERVER_SCOPE.equals(scope) || - DataConstants.SHARED_SCOPE.equals(scope) || - DataConstants.CLIENT_SCOPE.equals(scope)) { - String keysParam = request.getParameter("keys"); - if (!StringUtils.isEmpty(keysParam)) { - String[] keys = keysParam.split(","); - ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, Arrays.asList(keys), new PluginCallback() { - @Override - public void onSuccess(PluginContext ctx, Void value) { - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); - } + entityId = DeviceId.fromString(pathParams[0]); + scope = pathParams[1]; + } else if (pathParams.length == 3) { + entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]); + scope = pathParams[2]; + } else { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); + return; + } - @Override - public void onFailure(PluginContext ctx, Exception e) { - log.error("Failed to remove attributes", e); - msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); - } - }); - return; - } + if (DataConstants.SERVER_SCOPE.equals(scope) || + DataConstants.SHARED_SCOPE.equals(scope) || + DataConstants.CLIENT_SCOPE.equals(scope)) { + String keysParam = request.getParameter("keys"); + if (!StringUtils.isEmpty(keysParam)) { + String[] keys = keysParam.split(","); + ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), entityId, scope, Arrays.asList(keys), new PluginCallback() { + @Override + public void onSuccess(PluginContext ctx, Void value) { + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); + } + + @Override + public void onFailure(PluginContext ctx, Exception e) { + log.error("Failed to remove attributes", e); + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + }); + return; } } } catch (RuntimeException e) { @@ -278,4 +310,29 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { } }; } + + + private PluginCallback> getTsKvListCallback(final PluginRestMsg msg) { + return new PluginCallback>() { + @Override + public void onSuccess(PluginContext ctx, List data) { + Map> result = new LinkedHashMap<>(); + for (TsKvEntry entry : data) { + List vList = result.get(entry.getKey()); + if (vList == null) { + vList = new ArrayList<>(); + result.put(entry.getKey(), vList); + } + vList.add(new TsData(entry.getTs(), entry.getValueAsString())); + } + msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK)); + } + + @Override + public void onFailure(PluginContext ctx, Exception e) { + log.error("Failed to fetch historical data", e); + msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + }; + } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java index a0fc9fc99c..a1b734e991 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java @@ -18,7 +18,8 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.handlers; import com.google.protobuf.InvalidProtocolBufferException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.extensions.api.plugins.PluginContext; @@ -44,9 +45,10 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { private static final int SUBSCRIPTION_CLAZZ = 1; private static final int ATTRIBUTES_UPDATE_CLAZZ = 2; - private static final int SUBSCRIPTION_UPDATE_CLAZZ = 3; - private static final int SESSION_CLOSE_CLAZZ = 4; - private static final int SUBSCRIPTION_CLOSE_CLAZZ = 5; + private static final int TIMESERIES_UPDATE_CLAZZ = 3; + private static final int SUBSCRIPTION_UPDATE_CLAZZ = 4; + private static final int SESSION_CLOSE_CLAZZ = 5; + private static final int SUBSCRIPTION_CLOSE_CLAZZ = 6; @Override public void process(PluginContext ctx, RpcMsg msg) { @@ -60,6 +62,9 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { case ATTRIBUTES_UPDATE_CLAZZ: processAttributeUpdate(ctx, msg); break; + case TIMESERIES_UPDATE_CLAZZ: + processTimeseriesUpdate(ctx, msg); + break; case SESSION_CLOSE_CLAZZ: processSessionClose(ctx, msg); break; @@ -88,10 +93,21 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } - subscriptionManager.onAttributesUpdateFromServer(ctx, DeviceId.fromString(proto.getDeviceId()), proto.getScope(), + subscriptionManager.onAttributesUpdateFromServer(ctx, EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), proto.getScope(), proto.getDataList().stream().map(this::toAttribute).collect(Collectors.toList())); } + private void processTimeseriesUpdate(PluginContext ctx, RpcMsg msg) { + TimeseriesUpdateProto proto; + try { + proto = TimeseriesUpdateProto.parseFrom(msg.getMsgData()); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + subscriptionManager.onTimeseriesUpdateFromServer(ctx, EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), + proto.getDataList().stream().map(this::toTimeseries).collect(Collectors.toList())); + } + private void processSubscriptionCmd(PluginContext ctx, RpcMsg msg) { SubscriptionProto proto; try { @@ -101,7 +117,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { } Map statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs)); Subscription subscription = new Subscription( - new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), DeviceId.fromString(proto.getDeviceId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap), + new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap), false, msg.getServerAddress()); subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription); } @@ -110,7 +126,8 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { SubscriptionProto.Builder builder = SubscriptionProto.newBuilder(); builder.setSessionId(sessionId); builder.setSubscriptionId(cmd.getSubscriptionId()); - builder.setDeviceId(cmd.getDeviceId().toString()); + builder.setEntityType(cmd.getEntityId().getEntityType().name()); + builder.setEntityId(cmd.getEntityId().getId().toString()); builder.setType(cmd.getType().name()); builder.setAllKeys(cmd.isAllKeys()); cmd.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build())); @@ -195,41 +212,62 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { } } - public void onAttributesUpdate(PluginContext ctx, ServerAddress address, DeviceId deviceId, String scope, List attributes) { - ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(deviceId, scope, attributes).toByteArray())); + public void onAttributesUpdate(PluginContext ctx, ServerAddress address, EntityId entityId, String scope, List attributes) { + ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(entityId, scope, attributes).toByteArray())); } - private AttributeUpdateProto getAttributesUpdateProto(DeviceId deviceId, String scope, List attributes) { + public void onTimeseriesUpdate(PluginContext ctx, ServerAddress address, EntityId entityId, List tsKvEntries) { + ctx.sendPluginRpcMsg(new RpcMsg(address, TIMESERIES_UPDATE_CLAZZ, getTimeseriesUpdateProto(entityId, tsKvEntries).toByteArray())); + } + + private TimeseriesUpdateProto getTimeseriesUpdateProto(EntityId entityId, List tsKvEntries) { + TimeseriesUpdateProto.Builder builder = TimeseriesUpdateProto.newBuilder(); + builder.setEntityId(entityId.getId().toString()); + builder.setEntityType(entityId.getEntityType().name()); + tsKvEntries.forEach(attr -> builder.addData(toKeyValueProto(attr.getTs(), attr).build())); + return builder.build(); + } + + private AttributeUpdateProto getAttributesUpdateProto(EntityId entityId, String scope, List attributes) { AttributeUpdateProto.Builder builder = AttributeUpdateProto.newBuilder(); - builder.setDeviceId(deviceId.toString()); + builder.setEntityId(entityId.getId().toString()); + builder.setEntityType(entityId.getEntityType().name()); builder.setScope(scope); - attributes.forEach( - attr -> { - AttributeUpdateValueListProto.Builder dataBuilder = AttributeUpdateValueListProto.newBuilder(); - dataBuilder.setKey(attr.getKey()); - dataBuilder.setTs(attr.getLastUpdateTs()); - dataBuilder.setValueType(attr.getDataType().ordinal()); - switch (attr.getDataType()) { - case BOOLEAN: - dataBuilder.setBoolValue(attr.getBooleanValue().get()); - break; - case LONG: - dataBuilder.setLongValue(attr.getLongValue().get()); - break; - case DOUBLE: - dataBuilder.setDoubleValue(attr.getDoubleValue().get()); - break; - case STRING: - dataBuilder.setStrValue(attr.getStrValue().get()); - break; - } - builder.addData(dataBuilder.build()); - } - ); + attributes.forEach(attr -> builder.addData(toKeyValueProto(attr.getLastUpdateTs(), attr).build())); return builder.build(); } - private AttributeKvEntry toAttribute(AttributeUpdateValueListProto proto) { + private KeyValueProto.Builder toKeyValueProto(long ts, KvEntry attr) { + KeyValueProto.Builder dataBuilder = KeyValueProto.newBuilder(); + dataBuilder.setKey(attr.getKey()); + dataBuilder.setTs(ts); + dataBuilder.setValueType(attr.getDataType().ordinal()); + switch (attr.getDataType()) { + case BOOLEAN: + dataBuilder.setBoolValue(attr.getBooleanValue().get()); + break; + case LONG: + dataBuilder.setLongValue(attr.getLongValue().get()); + break; + case DOUBLE: + dataBuilder.setDoubleValue(attr.getDoubleValue().get()); + break; + case STRING: + dataBuilder.setStrValue(attr.getStrValue().get()); + break; + } + return dataBuilder; + } + + private AttributeKvEntry toAttribute(KeyValueProto proto) { + return new BaseAttributeKvEntry(getKvEntry(proto), proto.getTs()); + } + + private TsKvEntry toTimeseries(KeyValueProto proto) { + return new BasicTsKvEntry(proto.getTs(), getKvEntry(proto)); + } + + private KvEntry getKvEntry(KeyValueProto proto) { KvEntry entry = null; DataType type = DataType.values()[proto.getValueType()]; switch (type) { @@ -246,7 +284,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { entry = new StringDataEntry(proto.getKey(), proto.getStrValue()); break; } - return new BaseAttributeKvEntry(entry, proto.getTs()); + return entry; } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java index f018aaab13..c231ffacbe 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java @@ -19,7 +19,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.extensions.api.exception.UnauthorizedException; import org.thingsboard.server.extensions.api.plugins.PluginCallback; @@ -101,8 +102,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { if (cmd.isUnsubscribe()) { unsubscribe(ctx, cmd, sessionId); } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) { - log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), cmd.getDeviceId()); - DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); + log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId); Optional> keysOptional = getKeys(cmd); SubscriptionState sub; if (keysOptional.isPresent()) { @@ -118,8 +119,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { keys.forEach(key -> subState.put(key, 0L)); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState); - subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState); + subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @Override @@ -138,9 +139,9 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { }; if (StringUtils.isEmpty(cmd.getScope())) { - ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keys, callback); + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), keys, callback); } else { - ctx.loadAttributes(deviceId, cmd.getScope(), keys, callback); + ctx.loadAttributes(entityId, cmd.getScope(), keys, callback); } } else { PluginCallback> callback = new PluginCallback>() { @@ -152,8 +153,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { Map subState = new HashMap<>(attributesData.size()); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState); - subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState); + subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @Override @@ -166,9 +167,9 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { }; if (StringUtils.isEmpty(cmd.getScope())) { - ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback); + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), callback); } else { - ctx.loadAttributes(deviceId, cmd.getScope(), callback); + ctx.loadAttributes(entityId, cmd.getScope(), callback); } } } @@ -183,33 +184,33 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { if (cmd.isUnsubscribe()) { unsubscribe(ctx, cmd, sessionId); } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) { - DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); Optional> keysOptional = getKeys(cmd); if (keysOptional.isPresent()) { long startTs; if (cmd.getTimeWindow() > 0) { List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); - log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); + log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId); startTs = cmd.getStartTs(); long endTs = cmd.getStartTs() + cmd.getTimeWindow(); List queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); - ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); + ctx.loadTimeseries(entityId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys)); } else { List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); startTs = System.currentTimeMillis(); - log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId()); - ctx.loadLatestTimeseries(deviceId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); + log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId); + ctx.loadLatestTimeseries(entityId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys)); } } else { - ctx.loadLatestTimeseries(deviceId, new PluginCallback>() { + ctx.loadLatestTimeseries(entityId, new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List data) { sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(data.size()); data.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, true, subState); - subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState); + subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @Override @@ -230,7 +231,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { } } - private PluginCallback> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final DeviceId deviceId, final long startTs, final List keys) { + private PluginCallback> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) { return new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List data) { @@ -239,8 +240,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, startTs)); data.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState); - subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState); + subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @Override @@ -263,7 +264,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { sendWsMsg(ctx, sessionRef, update); return; } - if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) { + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Device id is empty!"); sendWsMsg(ctx, sessionRef, update); @@ -275,10 +276,11 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { sendWsMsg(ctx, sessionRef, update); return; } - DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); List keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); - List queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); - ctx.loadTimeseries(deviceId, queries, new PluginCallback>() { + List queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))) + .collect(Collectors.toList()); + ctx.loadTimeseries(entityId, queries, new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List data) { sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); @@ -321,7 +323,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { } private void unsubscribe(PluginContext ctx, SubscriptionCmd cmd, String sessionId) { - if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) { + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { cleanupWebSocketSession(ctx, sessionId); } else { subscriptionManager.removeSubscription(ctx, sessionId, cmd.getCmdId()); @@ -329,7 +331,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { } private boolean validateSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd) { - if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) { + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Device id is empty!"); sendWsMsg(ctx, sessionRef, update); diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java index 430a559698..1285cfa903 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java @@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.sub; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.cluster.ServerAddress; import java.util.Map; @@ -42,8 +43,8 @@ public class Subscription { return getSub().getSubscriptionId(); } - public DeviceId getDeviceId() { - return getSub().getDeviceId(); + public EntityId getEntityId() { + return getSub().getEntityId(); } public SubscriptionType getType() { diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java index fbadd64ad8..5e15fda502 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java @@ -16,9 +16,8 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.sub; import lombok.AllArgsConstructor; -import lombok.Data; import lombok.Getter; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import java.util.Map; @@ -30,7 +29,7 @@ public class SubscriptionState { @Getter private final String wsSessionId; @Getter private final int subscriptionId; - @Getter private final DeviceId deviceId; + @Getter private final EntityId entityId; @Getter private final SubscriptionType type; @Getter private final boolean allKeys; @Getter private final Map keyStates; @@ -44,7 +43,7 @@ public class SubscriptionState { if (subscriptionId != that.subscriptionId) return false; if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false; - if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null) return false; + if (entityId != null ? !entityId.equals(that.entityId) : that.entityId != null) return false; return type == that.type; } @@ -52,7 +51,7 @@ public class SubscriptionState { public int hashCode() { int result = wsSessionId != null ? wsSessionId.hashCode() : 0; result = 31 * result + subscriptionId; - result = 31 * result + (deviceId != null ? deviceId.hashCode() : 0); + result = 31 * result + (entityId != null ? entityId.hashCode() : 0); result = 31 * result + (type != null ? type.hashCode() : 0); return result; } @@ -61,7 +60,7 @@ public class SubscriptionState { public String toString() { return "SubscriptionState{" + "type=" + type + - ", deviceId=" + deviceId + + ", entityId=" + entityId + ", subscriptionId=" + subscriptionId + ", wsSessionId='" + wsSessionId + '\'' + '}'; diff --git a/extensions-core/src/main/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto index be97b4f493..2bfef5908b 100644 --- a/extensions-core/src/main/proto/telemetry.proto +++ b/extensions-core/src/main/proto/telemetry.proto @@ -22,10 +22,11 @@ option java_outer_classname = "TelemetryPluginProtos"; message SubscriptionProto { string sessionId = 1; int32 subscriptionId = 2; - string deviceId = 3; - string type = 4; - bool allKeys = 5; - repeated SubscriptionKetStateProto keyStates = 6; + string entityType = 3; + string entityId = 4; + string type = 5; + bool allKeys = 6; + repeated SubscriptionKetStateProto keyStates = 7; } message SubscriptionUpdateProto { @@ -37,9 +38,16 @@ message SubscriptionUpdateProto { } message AttributeUpdateProto { - string deviceId = 1; - string scope = 2; - repeated AttributeUpdateValueListProto data = 3; + string entityType = 1; + string entityId = 2; + string scope = 3; + repeated KeyValueProto data = 4; +} + +message TimeseriesUpdateProto { + string entityType = 1; + string entityId = 2; + repeated KeyValueProto data = 4; } message SessionCloseProto { @@ -62,7 +70,7 @@ message SubscriptionUpdateValueListProto { repeated string value = 3; } -message AttributeUpdateValueListProto { +message KeyValueProto { string key = 1; int64 ts = 2; int32 valueType = 3; diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java index b8de7c57e1..a78319afd9 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java @@ -82,6 +82,7 @@ public class GatewaySessionCtx { }); GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device); devices.put(deviceName, ctx); + log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName); processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg()))); processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg()))); } @@ -94,6 +95,9 @@ public class GatewaySessionCtx { if (deviceSessionCtx != null) { processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId())); deviceSessionCtx.setClosed(true); + log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName); + } else { + log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName); } ack(msg); } @@ -191,7 +195,8 @@ public class GatewaySessionCtx { private String checkDeviceConnected(String deviceName) { if (!devices.containsKey(deviceName)) { - throw new RuntimeException("Device is not connected!"); + log.debug("[{}] Missing device [{}] for the gateway session", gatewaySessionId, deviceName); + throw new RuntimeException("Device " + deviceName + " is not connected!"); } else { return deviceName; } diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js new file mode 100644 index 0000000000..f685b1e3f7 --- /dev/null +++ b/ui/src/app/api/asset.service.js @@ -0,0 +1,261 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export default angular.module('thingsboard.api.asset', []) + .factory('assetService', AssetService) + .name; + +/*@ngInject*/ +function AssetService($http, $q, customerService, userService) { + + var service = { + getAsset: getAsset, + getAssets: getAssets, + saveAsset: saveAsset, + deleteAsset: deleteAsset, + assignAssetToCustomer: assignAssetToCustomer, + unassignAssetFromCustomer: unassignAssetFromCustomer, + makeAssetPublic: makeAssetPublic, + getTenantAssets: getTenantAssets, + getCustomerAssets: getCustomerAssets, + findByQuery: findByQuery, + fetchAssetsByNameFilter: fetchAssetsByNameFilter + } + + return service; + + function getAsset(assetId, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/asset/' + assetId; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getAssets(assetIds, config) { + var deferred = $q.defer(); + var ids = ''; + for (var i=0;i0) { + ids += ','; + } + ids += assetIds[i]; + } + var url = '/api/assets?assetIds=' + ids; + $http.get(url, config).then(function success(response) { + var assets = response.data; + assets.sort(function (asset1, asset2) { + var id1 = asset1.id.id; + var id2 = asset2.id.id; + var index1 = assetIds.indexOf(id1); + var index2 = assetIds.indexOf(id2); + return index1 - index2; + }); + deferred.resolve(assets); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function saveAsset(asset, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/asset'; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.post(url, asset, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function deleteAsset(assetId, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/asset/' + assetId; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.delete(url, config).then(function success() { + deferred.resolve(); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function assignAssetToCustomer(customerId, assetId, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/customer/' + customerId + '/asset/' + assetId; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.post(url, null, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function unassignAssetFromCustomer(assetId, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/customer/asset/' + assetId; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.delete(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function makeAssetPublic(assetId, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/customer/public/asset/' + assetId; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.post(url, null, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getTenantAssets(pageLink, applyCustomersInfo, config) { + var deferred = $q.defer(); + var url = '/api/tenant/assets?limit=' + pageLink.limit; + if (angular.isDefined(pageLink.textSearch)) { + url += '&textSearch=' + pageLink.textSearch; + } + if (angular.isDefined(pageLink.idOffset)) { + url += '&idOffset=' + pageLink.idOffset; + } + if (angular.isDefined(pageLink.textOffset)) { + url += '&textOffset=' + pageLink.textOffset; + } + $http.get(url, config).then(function success(response) { + if (applyCustomersInfo) { + customerService.applyAssignedCustomersInfo(response.data.data).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(response.data); + } + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config) { + var deferred = $q.defer(); + var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit; + if (angular.isDefined(pageLink.textSearch)) { + url += '&textSearch=' + pageLink.textSearch; + } + if (angular.isDefined(pageLink.idOffset)) { + url += '&idOffset=' + pageLink.idOffset; + } + if (angular.isDefined(pageLink.textOffset)) { + url += '&textOffset=' + pageLink.textOffset; + } + $http.get(url, config).then(function success(response) { + if (applyCustomersInfo) { + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(response.data); + } + }, function fail() { + deferred.reject(); + }); + + return deferred.promise; + } + + function findByQuery(query, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/assets'; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.post(url, query, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function fetchAssetsByNameFilter(assetNameFilter, limit, applyCustomersInfo, config) { + var deferred = $q.defer(); + var user = userService.getCurrentUser(); + var promise; + var pageLink = {limit: limit, textSearch: assetNameFilter}; + if (user.authority === 'CUSTOMER_USER') { + var customerId = user.customerId; + promise = getCustomerAssets(customerId, pageLink, applyCustomersInfo, config); + } else { + promise = getTenantAssets(pageLink, applyCustomersInfo, config); + } + promise.then( + function success(result) { + if (result.data && result.data.length > 0) { + deferred.resolve(result.data); + } else { + deferred.resolve(null); + } + }, + function fail() { + deferred.resolve(null); + } + ); + return deferred.promise; + } + +} diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js new file mode 100644 index 0000000000..53d5341139 --- /dev/null +++ b/ui/src/app/api/attribute.service.js @@ -0,0 +1,282 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export default angular.module('thingsboard.api.attribute', []) + .factory('attributeService', AttributeService) + .name; + +/*@ngInject*/ +function AttributeService($http, $q, $filter, types, telemetryWebsocketService) { + + var entityAttributesSubscriptionMap = {}; + + var service = { + getEntityKeys: getEntityKeys, + getEntityTimeseriesValues: getEntityTimeseriesValues, + getEntityAttributesValues: getEntityAttributesValues, + getEntityAttributes: getEntityAttributes, + subscribeForEntityAttributes: subscribeForEntityAttributes, + unsubscribeForEntityAttributes: unsubscribeForEntityAttributes, + saveEntityAttributes: saveEntityAttributes, + deleteEntityAttributes: deleteEntityAttributes + } + + return service; + + function getEntityKeys(entityType, entityId, query, type) { + var deferred = $q.defer(); + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/'; + if (type === types.dataKeyType.timeseries) { + url += 'timeseries'; + } else if (type === types.dataKeyType.attribute) { + url += 'attributes'; + } + $http.get(url, null).then(function success(response) { + var result = []; + if (response.data) { + if (query) { + var dataKeys = response.data; + var lowercaseQuery = angular.lowercase(query); + for (var i=0; i -1) { + attribute = attributes[index]; + } else { + attribute = { + key: key + }; + index = attributes.push(attribute)-1; + keys[key] = index; + } + var attrData = data[key][0]; + attribute.lastUpdateTs = attrData[0]; + attribute.value = attrData[1]; + } + if (entityAttributesSubscription.subscriptionCallback) { + entityAttributesSubscription.subscriptionCallback(attributes); + } + } + } + + function subscribeForEntityAttributes(entityType, entityId, attributeScope) { + var subscriptionId = entityType + entityId + attributeScope; + var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId]; + if (!entityAttributesSubscription) { + var subscriptionCommand = { + entityType: entityType, + entityId: entityId, + scope: attributeScope + }; + + var type = attributeScope === types.latestTelemetry.value ? + types.dataKeyType.timeseries : types.dataKeyType.attribute; + + var subscriber = { + subscriptionCommand: subscriptionCommand, + type: type, + onData: function (data) { + if (data.data) { + onSubscriptionData(data.data, subscriptionId); + } + } + }; + entityAttributesSubscription = { + subscriber: subscriber, + attributes: null + }; + entityAttributesSubscriptionMap[subscriptionId] = entityAttributesSubscription; + telemetryWebsocketService.subscribe(subscriber); + } + return subscriptionId; + } + + function unsubscribeForEntityAttributes(subscriptionId) { + var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId]; + if (entityAttributesSubscription) { + telemetryWebsocketService.unsubscribe(entityAttributesSubscription.subscriber); + delete entityAttributesSubscriptionMap[subscriptionId]; + } + } + + function saveEntityAttributes(entityType, entityId, attributeScope, attributes) { + var deferred = $q.defer(); + var attributesData = {}; + var deleteAttributes = []; + for (var a=0; a 0) { + keys += ','; + } + keys += attributes[i].key; + } + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys; + $http.delete(url).then(function success() { + deferred.resolve(); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + +} \ No newline at end of file diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js index b2a7897916..743fbe9ed8 100644 --- a/ui/src/app/api/dashboard.service.js +++ b/ui/src/app/api/dashboard.service.js @@ -24,6 +24,8 @@ function DashboardService($http, $q, $location, customerService) { getCustomerDashboards: getCustomerDashboards, getServerTimeDiff: getServerTimeDiff, getDashboard: getDashboard, + getDashboardInfo: getDashboardInfo, + getTenantDashboardsByTenantId: getTenantDashboardsByTenantId, getTenantDashboards: getTenantDashboards, deleteDashboard: deleteDashboard, saveDashboard: saveDashboard, @@ -34,6 +36,26 @@ function DashboardService($http, $q, $location, customerService) { return service; + function getTenantDashboardsByTenantId(tenantId, pageLink) { + var deferred = $q.defer(); + var url = '/api/tenant/' + tenantId + '/dashboards?limit=' + pageLink.limit; + if (angular.isDefined(pageLink.textSearch)) { + url += '&textSearch=' + pageLink.textSearch; + } + if (angular.isDefined(pageLink.idOffset)) { + url += '&idOffset=' + pageLink.idOffset; + } + if (angular.isDefined(pageLink.textOffset)) { + url += '&textOffset=' + pageLink.textOffset; + } + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + function getTenantDashboards(pageLink) { var deferred = $q.defer(); var url = '/api/tenant/dashboards?limit=' + pageLink.limit; @@ -94,7 +116,7 @@ function DashboardService($http, $q, $location, customerService) { var deferred = $q.defer(); var url = '/api/dashboard/serverTime'; var ct1 = Date.now(); - $http.get(url, null).then(function success(response) { + $http.get(url, { ignoreLoading: true }).then(function success(response) { var ct2 = Date.now(); var st = response.data; var stDiff = Math.ceil(st - (ct1+ct2)/2); @@ -116,6 +138,17 @@ function DashboardService($http, $q, $location, customerService) { return deferred.promise; } + function getDashboardInfo(dashboardId) { + var deferred = $q.defer(); + var url = '/api/dashboard/info/' + dashboardId; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + function saveDashboard(dashboard) { var deferred = $q.defer(); var url = '/api/dashboard'; diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js index bf9a057b2a..27da468944 100644 --- a/ui/src/app/api/datasource.service.js +++ b/ui/src/app/api/datasource.service.js @@ -35,11 +35,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t return service; - function subscribeToDatasource(listener) { var datasource = listener.datasource; - if (datasource.type === types.datasourceType.device && !listener.deviceId) { + if (datasource.type === types.datasourceType.entity && (!listener.entityId || !listener.entityType)) { return; } @@ -64,8 +63,9 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t if (listener.subscriptionType === types.widgetType.timeseries.value) { datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow); } - if (datasourceSubscription.datasourceType === types.datasourceType.device) { - datasourceSubscription.deviceId = listener.deviceId; + if (datasourceSubscription.datasourceType === types.datasourceType.entity) { + datasourceSubscription.entityType = listener.entityType; + datasourceSubscription.entityId = listener.entityId; } listener.datasourceSubscriptionKey = utils.objectHashCode(datasourceSubscription); @@ -141,7 +141,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody); } } - if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) { + if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) { if (datasourceType === types.datasourceType.function) { key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type; } else { @@ -191,7 +191,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic function syncListener(listener) { var key; var dataKey; - if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) { + if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) { for (key in dataKeys) { var dataKeysList = dataKeys[key]; for (var i = 0; i < dataKeysList.length; i++) { @@ -220,7 +220,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic var tsKeyNames = []; var dataKey; - if (datasourceType === types.datasourceType.device) { + if (datasourceType === types.datasourceType.entity) { //send subscribe command @@ -252,7 +252,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic if (history) { var historyCommand = { - deviceId: datasourceSubscription.deviceId, + entityType: datasourceSubscription.entityType, + entityId: datasourceSubscription.entityId, keys: tsKeys, startTs: subsTw.fixedWindow.startTimeMs, endTs: subsTw.fixedWindow.endTimeMs, @@ -282,7 +283,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic } else { subscriptionCommand = { - deviceId: datasourceSubscription.deviceId, + entityType: datasourceSubscription.entityType, + entityId: datasourceSubscription.entityId, keys: tsKeys }; @@ -328,7 +330,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic if (attrKeys.length > 0) { subscriptionCommand = { - deviceId: datasourceSubscription.deviceId, + entityType: datasourceSubscription.entityType, + entityId: datasourceSubscription.entityId, keys: attrKeys }; @@ -404,7 +407,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic $timeout.cancel(timer); timer = null; } - if (datasourceType === types.datasourceType.device) { + if (datasourceType === types.datasourceType.entity) { for (var cmdId in subscribers) { var subscriber = subscribers[cmdId]; telemetryWebsocketService.unsubscribe(subscriber); diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index ea197a1bc5..d12d025050 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -20,10 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes]) .name; /*@ngInject*/ -function DeviceService($http, $q, $filter, userService, customerService, telemetryWebsocketService, types) { - - - var deviceAttributesSubscriptionMap = {}; +function DeviceService($http, $q, attributeService, customerService, types) { var service = { assignDeviceToCustomer: assignDeviceToCustomer, @@ -31,12 +28,7 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet getCustomerDevices: getCustomerDevices, getDevice: getDevice, getDevices: getDevices, - processDeviceAliases: processDeviceAliases, - checkDeviceAlias: checkDeviceAlias, - fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter, getDeviceCredentials: getDeviceCredentials, - getDeviceKeys: getDeviceKeys, - getDeviceTimeseriesValues: getDeviceTimeseriesValues, getTenantDevices: getTenantDevices, saveDevice: saveDevice, saveDeviceCredentials: saveDeviceCredentials, @@ -48,7 +40,8 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet saveDeviceAttributes: saveDeviceAttributes, deleteDeviceAttributes: deleteDeviceAttributes, sendOneWayRpcCommand: sendOneWayRpcCommand, - sendTwoWayRpcCommand: sendTwoWayRpcCommand + sendTwoWayRpcCommand: sendTwoWayRpcCommand, + findByQuery: findByQuery } return service; @@ -159,174 +152,6 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet return deferred.promise; } - function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) { - var deferred = $q.defer(); - var user = userService.getCurrentUser(); - var promise; - var pageLink = {limit: limit, textSearch: deviceNameFilter}; - if (user.authority === 'CUSTOMER_USER') { - var customerId = user.customerId; - promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config); - } else { - promise = getTenantDevices(pageLink, applyCustomersInfo, config); - } - promise.then( - function success(result) { - if (result.data && result.data.length > 0) { - deferred.resolve(result.data); - } else { - deferred.resolve(null); - } - }, - function fail() { - deferred.resolve(null); - } - ); - return deferred.promise; - } - - function deviceToDeviceInfo(device) { - return { name: device.name, id: device.id.id }; - } - - function devicesToDevicesInfo(devices) { - var devicesInfo = []; - for (var d = 0; d < devices.length; d++) { - devicesInfo.push(deviceToDeviceInfo(devices[d])); - } - return devicesInfo; - } - - function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) { - if (index < aliasIds.length) { - var aliasId = aliasIds[index]; - var deviceAlias = deviceAliases[aliasId]; - var alias = deviceAlias.alias; - if (!deviceAlias.deviceFilter) { - getDevice(deviceAlias.deviceId).then( - function success(device) { - var resolvedAlias = {alias: alias, deviceId: device.id.id}; - resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias; - resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [ - deviceToDeviceInfo(device) - ]; - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - }, - function fail() { - if (!resolution.error) { - resolution.error = 'dashboard.invalid-aliases-config'; - } - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - } - ); - } else { - var deviceFilter = deviceAlias.deviceFilter; - if (deviceFilter.useFilter) { - var deviceNameFilter = deviceFilter.deviceNameFilter; - fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then( - function(devices) { - if (devices && devices != null) { - var resolvedAlias = {alias: alias, deviceId: devices[0].id.id}; - resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias; - resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices); - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - } else { - if (!resolution.error) { - resolution.error = 'dashboard.invalid-aliases-config'; - } - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - } - }); - } else { - var deviceList = deviceFilter.deviceList; - getDevices(deviceList).then( - function success(devices) { - if (devices && devices.length > 0) { - var resolvedAlias = {alias: alias, deviceId: devices[0].id.id}; - resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias; - resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices); - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - } else { - if (!resolution.error) { - resolution.error = 'dashboard.invalid-aliases-config'; - } - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - } - }, - function fail() { - if (!resolution.error) { - resolution.error = 'dashboard.invalid-aliases-config'; - } - index++; - processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred); - } - ); - } - } - } else { - deferred.resolve(resolution); - } - } - - function processDeviceAliases(deviceAliases) { - var deferred = $q.defer(); - var resolution = { - aliasesInfo: { - deviceAliases: {}, - deviceAliasesInfo: {} - } - }; - var aliasIds = []; - if (deviceAliases) { - for (var aliasId in deviceAliases) { - aliasIds.push(aliasId); - } - } - processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred); - return deferred.promise; - } - - function checkDeviceAlias(deviceAlias) { - var deferred = $q.defer(); - var deviceFilter; - if (deviceAlias.deviceId) { - deviceFilter = { - useFilter: false, - deviceNameFilter: '', - deviceList: [deviceAlias.deviceId] - } - } else { - deviceFilter = deviceAlias.deviceFilter; - } - var promise; - if (deviceFilter.useFilter) { - var deviceNameFilter = deviceFilter.deviceNameFilter; - promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false); - } else { - var deviceList = deviceFilter.deviceList; - promise = getDevices(deviceList); - } - promise.then( - function success(devices) { - if (devices && devices.length > 0) { - deferred.resolve(true); - } else { - deferred.resolve(false); - } - }, - function fail() { - deferred.resolve(false); - } - ); - return deferred.promise; - } - function saveDevice(device) { var deferred = $q.defer(); var url = '/api/device'; @@ -404,198 +229,24 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet return deferred.promise; } - function getDeviceKeys(deviceId, query, type) { - var deferred = $q.defer(); - var url = '/api/plugins/telemetry/' + deviceId + '/keys/'; - if (type === types.dataKeyType.timeseries) { - url += 'timeseries'; - } else if (type === types.dataKeyType.attribute) { - url += 'attributes'; - } - $http.get(url, null).then(function success(response) { - var result = []; - if (response.data) { - if (query) { - var dataKeys = response.data; - var lowercaseQuery = angular.lowercase(query); - for (var i=0; i -1) { - attribute = attributes[index]; - } else { - attribute = { - key: key - }; - index = attributes.push(attribute)-1; - keys[key] = index; - } - var attrData = data[key][0]; - attribute.lastUpdateTs = attrData[0]; - attribute.value = attrData[1]; - } - if (deviceAttributesSubscription.subscriptionCallback) { - deviceAttributesSubscription.subscriptionCallback(attributes); - } - } + return attributeService.getEntityAttributes(types.entityType.device, deviceId, attributeScope, query, successCallback, config); } function subscribeForDeviceAttributes(deviceId, attributeScope) { - var subscriptionId = deviceId + attributeScope; - var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId]; - if (!deviceAttributesSubscription) { - var subscriptionCommand = { - deviceId: deviceId, - scope: attributeScope - }; - - var type = attributeScope === types.latestTelemetry.value ? - types.dataKeyType.timeseries : types.dataKeyType.attribute; - - var subscriber = { - subscriptionCommand: subscriptionCommand, - type: type, - onData: function (data) { - if (data.data) { - onSubscriptionData(data.data, subscriptionId); - } - } - }; - deviceAttributesSubscription = { - subscriber: subscriber, - attributes: null - } - deviceAttributesSubscriptionMap[subscriptionId] = deviceAttributesSubscription; - telemetryWebsocketService.subscribe(subscriber); - } - return subscriptionId; + return attributeService.subscribeForEntityAttributes(types.entityType.device, deviceId, attributeScope); } + function unsubscribeForDeviceAttributes(subscriptionId) { - var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId]; - if (deviceAttributesSubscription) { - telemetryWebsocketService.unsubscribe(deviceAttributesSubscription.subscriber); - delete deviceAttributesSubscriptionMap[subscriptionId]; - } + attributeService.unsubscribeForEntityAttributes(subscriptionId); } function saveDeviceAttributes(deviceId, attributeScope, attributes) { - var deferred = $q.defer(); - var attributesData = {}; - for (var a=0; a 0) { - keys += ','; - } - keys += attributes[i].key; - } - var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope + '?keys=' + keys; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail() { - deferred.reject(); - }); - return deferred.promise; + return attributeService.deleteEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes); } function sendOneWayRpcCommand(deviceId, requestBody) { @@ -620,4 +271,19 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet return deferred.promise; } + function findByQuery(query, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/devices'; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.post(url, query, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + } diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js new file mode 100644 index 0000000000..998607ad65 --- /dev/null +++ b/ui/src/app/api/entity-relation.service.js @@ -0,0 +1,136 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export default angular.module('thingsboard.api.entityRelation', []) + .factory('entityRelationService', EntityRelationService) + .name; + +/*@ngInject*/ +function EntityRelationService($http, $q) { + + var service = { + saveRelation: saveRelation, + deleteRelation: deleteRelation, + deleteRelations: deleteRelations, + findByFrom: findByFrom, + findByFromAndType: findByFromAndType, + findByTo: findByTo, + findByToAndType: findByToAndType, + findByQuery: findByQuery + } + + return service; + + function saveRelation(relation) { + var deferred = $q.defer(); + var url = '/api/relation'; + $http.post(url, relation).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function deleteRelation(fromId, fromType, relationType, toId, toType) { + var deferred = $q.defer(); + var url = '/api/relation?fromId=' + fromId; + url += '&fromType=' + fromType; + url += '&relationType=' + relationType; + url += '&toId=' + toId; + url += '&toType=' + toType; + $http.delete(url).then(function success() { + deferred.resolve(); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function deleteRelations(entityId, entityType) { + var deferred = $q.defer(); + var url = '/api/relations?entityId=' + entityId; + url += '&entityType=' + entityType; + $http.delete(url).then(function success() { + deferred.resolve(); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + + function findByFrom(fromId, fromType) { + var deferred = $q.defer(); + var url = '/api/relations?fromId=' + fromId; + url += '&fromType=' + fromType; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function findByFromAndType(fromId, fromType, relationType) { + var deferred = $q.defer(); + var url = '/api/relations?fromId=' + fromId; + url += '&fromType=' + fromType; + url += '&relationType=' + relationType; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function findByTo(toId, toType) { + var deferred = $q.defer(); + var url = '/api/relations?toId=' + toId; + url += '&toType=' + toType; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function findByToAndType(toId, toType, relationType) { + var deferred = $q.defer(); + var url = '/api/relations?toId=' + toId; + url += '&toType=' + toType; + url += '&relationType=' + relationType; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function findByQuery(query) { + var deferred = $q.defer(); + var url = '/api/relations'; + $http.post(url, query).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + +} diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js new file mode 100644 index 0000000000..43c537cafc --- /dev/null +++ b/ui/src/app/api/entity.service.js @@ -0,0 +1,777 @@ +/* + * Copyright © 2016-2017 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 thingsboardTypes from '../common/types.constant'; + +export default angular.module('thingsboard.api.entity', [thingsboardTypes]) + .factory('entityService', EntityService) + .name; + +/*@ngInject*/ +function EntityService($http, $q, $filter, $translate, userService, deviceService, + assetService, tenantService, customerService, + ruleService, pluginService, entityRelationService, attributeService, types, utils) { + var service = { + getEntity: getEntity, + getEntities: getEntities, + getEntitiesByNameFilter: getEntitiesByNameFilter, + entityName: entityName, + processEntityAliases: processEntityAliases, + getEntityKeys: getEntityKeys, + checkEntityAlias: checkEntityAlias, + createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo, + getRelatedEntities: getRelatedEntities, + saveRelatedEntity: saveRelatedEntity, + getRelatedEntity: getRelatedEntity, + deleteRelatedEntity: deleteRelatedEntity, + moveEntity: moveEntity + }; + + return service; + + function getEntityPromise(entityType, entityId, config) { + var promise; + switch (entityType) { + case types.entityType.device: + promise = deviceService.getDevice(entityId, true, config); + break; + case types.entityType.asset: + promise = assetService.getAsset(entityId, true, config); + break; + case types.entityType.tenant: + promise = tenantService.getTenant(entityId); + break; + case types.entityType.customer: + promise = customerService.getCustomer(entityId); + break; + case types.entityType.rule: + promise = ruleService.getRule(entityId); + break; + case types.entityType.plugin: + promise = pluginService.getPlugin(entityId); + break; + } + return promise; + } + + function getEntity(entityType, entityId, config) { + var deferred = $q.defer(); + var promise = getEntityPromise(entityType, entityId, config); + if (promise) { + promise.then( + function success(result) { + deferred.resolve(result); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.reject(); + } + return deferred.promise; + } + + function getEntitiesByIdsPromise(fetchEntityFunction, entityIds) { + var tasks = []; + var deferred = $q.defer(); + for (var i=0;i 0) { + deferred.resolve(result.data); + } else { + deferred.resolve(null); + } + }, + function fail() { + deferred.resolve(null); + } + ); + return deferred.promise; + } + + function entityName(entityType, entity) { + var name = ''; + switch (entityType) { + case types.entityType.device: + case types.entityType.asset: + case types.entityType.rule: + case types.entityType.plugin: + name = entity.name; + break; + case types.entityType.tenant: + case types.entityType.customer: + name = entity.title; + break; + } + return name; + } + + function entityToEntityInfo(entityType, entity) { + return { name: entityName(entityType, entity), entityType: entityType, id: entity.id.id }; + } + + function entitiesToEntitiesInfo(entityType, entities) { + var entitiesInfo = []; + for (var d = 0; d < entities.length; d++) { + entitiesInfo.push(entityToEntityInfo(entityType, entities[d])); + } + return entitiesInfo; + } + + function processEntityAlias(index, aliasIds, entityAliases, resolution, deferred) { + if (index < aliasIds.length) { + var aliasId = aliasIds[index]; + var entityAlias = entityAliases[aliasId]; + var alias = entityAlias.alias; + var entityFilter = entityAlias.entityFilter; + if (entityFilter.useFilter) { + var entityNameFilter = entityFilter.entityNameFilter; + getEntitiesByNameFilter(entityAlias.entityType, entityNameFilter, 100).then( + function(entities) { + if (entities && entities != null) { + var resolvedAlias = {alias: alias, entityType: entityAlias.entityType, entityId: entities[0].id.id}; + resolution.aliasesInfo.entityAliases[aliasId] = resolvedAlias; + resolution.aliasesInfo.entityAliasesInfo[aliasId] = entitiesToEntitiesInfo(entityAlias.entityType, entities); + index++; + processEntityAlias(index, aliasIds, entityAliases, resolution, deferred); + } else { + if (!resolution.error) { + resolution.error = 'dashboard.invalid-aliases-config'; + } + index++; + processEntityAlias(index, aliasIds, entityAliases, resolution, deferred); + } + }); + } else { + var entityList = entityFilter.entityList; + getEntities(entityAlias.entityType, entityList).then( + function success(entities) { + if (entities && entities.length > 0) { + var resolvedAlias = {alias: alias, entityType: entityAlias.entityType, entityId: entities[0].id.id}; + resolution.aliasesInfo.entityAliases[aliasId] = resolvedAlias; + resolution.aliasesInfo.entityAliasesInfo[aliasId] = entitiesToEntitiesInfo(entityAlias.entityType, entities); + index++; + processEntityAlias(index, aliasIds, entityAliases, resolution, deferred); + } else { + if (!resolution.error) { + resolution.error = 'dashboard.invalid-aliases-config'; + } + index++; + processEntityAlias(index, aliasIds, entityAliases, resolution, deferred); + } + }, + function fail() { + if (!resolution.error) { + resolution.error = 'dashboard.invalid-aliases-config'; + } + index++; + processEntityAlias(index, aliasIds, entityAliases, resolution, deferred); + } + ); + } + } else { + deferred.resolve(resolution); + } + } + + function processEntityAliases(entityAliases) { + var deferred = $q.defer(); + var resolution = { + aliasesInfo: { + entityAliases: {}, + entityAliasesInfo: {} + } + }; + var aliasIds = []; + if (entityAliases) { + for (var aliasId in entityAliases) { + aliasIds.push(aliasId); + } + } + processEntityAlias(0, aliasIds, entityAliases, resolution, deferred); + return deferred.promise; + } + + function getEntityKeys(entityType, entityId, query, type) { + var deferred = $q.defer(); + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/'; + if (type === types.dataKeyType.timeseries) { + url += 'timeseries'; + } else if (type === types.dataKeyType.attribute) { + url += 'attributes'; + } + $http.get(url, null).then(function success(response) { + var result = []; + if (response.data) { + if (query) { + var dataKeys = response.data; + var lowercaseQuery = angular.lowercase(query); + for (var i=0; i 0) { + deferred.resolve(true); + } else { + deferred.resolve(false); + } + }, + function fail() { + deferred.resolve(false); + } + ); + return deferred.promise; + } + + function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) { + var deferred = $q.defer(); + var datasources = []; + processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred); + return deferred.promise; + } + + function processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) { + if (index < subscriptionsInfo.length) { + var subscriptionInfo = validateSubscriptionInfo(subscriptionsInfo[index]); + if (subscriptionInfo.type === types.datasourceType.entity) { + if (subscriptionInfo.entityId) { + getEntity(subscriptionInfo.entityType, subscriptionInfo.entityId, {ignoreLoading: true}).then( + function success(entity) { + createDatasourceFromSubscription(subscriptionInfo, datasources, entity); + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + }, + function fail() { + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + ); + } else if (subscriptionInfo.entityName || subscriptionInfo.entityNamePrefix + || subscriptionInfo.entityIds) { + var promise; + if (subscriptionInfo.entityName) { + promise = getEntitiesByNameFilter(subscriptionInfo.entityType, subscriptionInfo.entityName, 1, {ignoreLoading: true}); + } else if (subscriptionInfo.entityNamePrefix) { + promise = getEntitiesByNameFilter(subscriptionInfo.entityType, subscriptionInfo.entityNamePrefix, 100, {ignoreLoading: true}); + } else if (subscriptionInfo.entityIds) { + promise = getEntities(subscriptionInfo.entityType, subscriptionInfo.entityIds, {ignoreLoading: true}); + } + promise.then( + function success(entities) { + if (entities && entities.length > 0) { + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + createDatasourceFromSubscription(subscriptionInfo, datasources, entity); + } + } + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + }, + function fail() { + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + ) + } else { + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + } else if (subscriptionInfo.type === types.datasourceType.function) { + createDatasourceFromSubscription(subscriptionInfo, datasources); + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + } else { + deferred.resolve(datasources); + } + } + + function validateSubscriptionInfo(subscriptionInfo) { + if (subscriptionInfo.type === 'device') { + subscriptionInfo.type = types.datasourceType.entity; + subscriptionInfo.entityType = types.entityType.device; + if (subscriptionInfo.deviceId) { + subscriptionInfo.entityId = subscriptionInfo.deviceId; + } else if (subscriptionInfo.deviceName) { + subscriptionInfo.entityName = subscriptionInfo.deviceName; + } else if (subscriptionInfo.deviceNamePrefix) { + subscriptionInfo.entityNamePrefix = subscriptionInfo.deviceNamePrefix; + } else if (subscriptionInfo.deviceIds) { + subscriptionInfo.entityIds = subscriptionInfo.deviceIds; + } + } + return subscriptionInfo; + } + + function createDatasourceFromSubscription(subscriptionInfo, datasources, entity) { + var datasource; + if (subscriptionInfo.type === types.datasourceType.entity) { + datasource = { + type: subscriptionInfo.type, + entityName: entity.name ? entity.name : entity.title, + name: entity.name ? entity.name : entity.title, + entityType: subscriptionInfo.entityType, + entityId: entity.id.id, + dataKeys: [] + } + } else if (subscriptionInfo.type === types.datasourceType.function) { + datasource = { + type: subscriptionInfo.type, + name: subscriptionInfo.name || types.datasourceType.function, + dataKeys: [] + } + } + datasources.push(datasource); + if (subscriptionInfo.timeseries) { + createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources); + } + if (subscriptionInfo.attributes) { + createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources); + } + if (subscriptionInfo.functions) { + createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources); + } + } + + function createDatasourceKeys(keyInfos, type, datasource, datasources) { + for (var i=0;i -1) { + var deleteRelatedEntityPromise = deleteRelatedEntity(relationEntityId, deleteRelatedEntityTypes); + deleteRelatedEntitiesTasks.push(deleteRelatedEntityPromise); + } + } + deleteRelatedEntitiesTasks.push(deleteEntityPromise(entityId)); + $q.all(deleteRelatedEntitiesTasks).then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ) + } else { + deleteEntityPromise(entityId).then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + } + return deferred.promise; + } + + function moveEntity(entityId, prevParentId, targetParentId) { + var deferred = $q.defer(); + entityRelationService.deleteRelation(prevParentId.id, prevParentId.entityType, + types.entityRelationType.contains, entityId.id, entityId.entityType).then( + function success() { + var relation = { + from: targetParentId, + to: entityId, + type: types.entityRelationType.contains + }; + entityRelationService.saveRelation(relation).then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + function saveEntityPromise(entity) { + var entityType = entity.id.entityType; + if (!entity.id.id) { + delete entity.id; + } + if (entityType == types.entityType.asset) { + return assetService.saveAsset(entity); + } else if (entityType == types.entityType.device) { + return deviceService.saveDevice(entity); + } + } + + function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred) { + var entity = {}; + entity.id = relatedEntity.id; + entity.name = relatedEntity.name; + entity.type = relatedEntity.type; + saveEntityPromise(entity).then( + function success(entity) { + relatedEntity.id = entity.id; + var relation = { + from: parentEntityId, + to: relatedEntity.id, + type: types.entityRelationType.contains + }; + entityRelationService.saveRelation(relation).then( + function success() { + updateEntity(entity, relatedEntity, keys, deferred); + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + } + + function updateRelatedEntity(relatedEntity, keys, deferred) { + getEntityPromise(relatedEntity.id.entityType, relatedEntity.id.id, {ignoreLoading: true}).then( + function success(entity) { + updateEntity(entity, relatedEntity, keys, deferred); + }, + function fail() { + deferred.reject(); + } + ); + } + + function updateEntity(entity, relatedEntity, keys, deferred) { + if (!angular.equals(entity.name, relatedEntity.name) || !angular.equals(entity.type, relatedEntity.type)) { + entity.name = relatedEntity.name; + entity.type = relatedEntity.type; + saveEntityPromise(entity).then( + function success (entity) { + updateEntityAttributes(entity, relatedEntity, keys, deferred); + }, + function fail() { + deferred.reject(); + } + ); + } else { + updateEntityAttributes(entity, relatedEntity, keys, deferred); + } + } + + function updateEntityAttributes(entity, relatedEntity, keys, deferred) { + var attributes = []; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + attributes.push({key: key, value: relatedEntity[key]}); + } + attributeService.saveEntityAttributes(entity.id.entityType, entity.id.id, types.attributesScope.server.value, attributes) + .then( + function success() { + deferred.resolve(relatedEntity); + }, + function fail() { + deferred.reject(); + } + ); + } + + function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel) { + + var searchQuery = { + parameters: { + rootId: rootEntityId.id, + rootType: rootEntityId.entityType, + direction: types.entitySearchDirection.from + }, + relationType: types.entityRelationType.contains + }; + + if (maxLevel) { + searchQuery.parameters.maxLevel = maxLevel; + } else { + searchQuery.parameters.maxLevel = 1; + } + + if (entityType == types.entityType.asset) { + searchQuery.assetTypes = entitySubTypes; + } else if (entityType == types.entityType.device) { + searchQuery.deviceTypes = entitySubTypes; + } else { + return null; //Not supported + } + + return searchQuery; + } + + function constructEntity(entity, keys, typeTranslatePrefix) { + var deferred = $q.defer(); + if (typeTranslatePrefix) { + entity.typeName = $translate.instant(typeTranslatePrefix+'.'+entity.type); + } else { + entity.typeName = entity.type; + } + attributeService.getEntityAttributesValues(entity.id.entityType, entity.id.id, + types.attributesScope.server.value, keys.join(','), + {ignoreLoading: true}).then( + function success(attributes) { + if (attributes && attributes.length > 0) { + for (var i=0;i 0) { + return foundAttributes[0].value; + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index afb9a06158..633bcc36c5 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -64,7 +64,7 @@ export default class Subscription { this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){}; this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){}; - this.datasources = options.datasources; + this.datasources = this.ctx.utils.validateDatasources(options.datasources); this.datasourceListeners = []; this.data = []; this.hiddenData = []; @@ -172,12 +172,6 @@ export default class Subscription { this.startWatchingTimewindow(); } } - - registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { - subscription.checkSubscriptions(); - }); - - this.registrations.push(registration); } startWatchingTimewindow() { @@ -204,29 +198,11 @@ export default class Subscription { } initRpc() { - if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; - if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) { - this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId; + if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) { + this.targetDeviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId; } - var subscription = this; - var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { - var deviceId = null; - if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) { - deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId; - } - if (!angular.equals(deviceId, subscription.targetDeviceId)) { - subscription.targetDeviceId = deviceId; - if (subscription.targetDeviceId) { - subscription.rpcEnabled = true; - } else { - subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false; - } - subscription.callbacks.rpcStateChanged(subscription); - } - }); - this.registrations.push(registration); } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { this.targetDeviceId = this.targetDeviceIds[0]; } @@ -342,6 +318,14 @@ export default class Subscription { this.onDataUpdated(); } + onAliasesChanged() { + if (this.type === this.ctx.types.widgetType.rpc.value) { + this.checkRpcTarget(); + } else { + this.checkSubscriptions(); + } + } + onDataUpdated(apply) { if (this.cafs['dataUpdated']) { this.cafs['dataUpdated'](); @@ -493,25 +477,30 @@ export default class Subscription { var datasource = this.datasources[i]; if (angular.isFunction(datasource)) continue; - var deviceId = null; - if (datasource.type === this.ctx.types.datasourceType.device) { + var entityId = null; + var entityType = null; + if (datasource.type === this.ctx.types.datasourceType.entity) { var aliasName = null; - var deviceName = null; - if (datasource.deviceId) { - deviceId = datasource.deviceId; - datasource.name = datasource.deviceName; - aliasName = datasource.deviceName; - deviceName = datasource.deviceName; - } else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) { - deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; - datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - deviceName = ''; - var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; - for (var d = 0; d < devicesInfo.length; d++) { - if (devicesInfo[d].id === deviceId) { - deviceName = devicesInfo[d].name; - break; + var entityName = null; + if (datasource.entityId) { + entityId = datasource.entityId; + entityType = datasource.entityType; + datasource.name = datasource.entityName; + aliasName = datasource.entityName; + entityName = datasource.entityName; + } else if (datasource.entityAliasId) { + if (this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId]) { + entityId = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityId; + entityType = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityType; + datasource.name = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias; + aliasName = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias; + entityName = ''; + var entitiesInfo = this.ctx.aliasesInfo.entityAliasesInfo[datasource.entityAliasId]; + for (var d = 0; d < entitiesInfo.length; d++) { + if (entitiesInfo[d].id === entityId) { + entityName = entitiesInfo[d].name; + break; + } } } } @@ -519,7 +508,7 @@ export default class Subscription { datasource.name = datasource.name || this.ctx.types.datasourceType.function; } for (var dk = 0; dk < datasource.dataKeys.length; dk++) { - updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName); + updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, entityName, aliasName); } var subscription = this; @@ -528,7 +517,8 @@ export default class Subscription { subscriptionType: this.type, subscriptionTimewindow: this.subscriptionTimewindow, datasource: datasource, - deviceId: deviceId, + entityType: entityType, + entityId: entityId, dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); }, @@ -563,19 +553,38 @@ export default class Subscription { } } + checkRpcTarget() { + var deviceId = null; + if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) { + deviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId; + } + if (!angular.equals(deviceId, this.targetDeviceId)) { + this.targetDeviceId = deviceId; + if (this.targetDeviceId) { + this.rpcEnabled = true; + } else { + this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false; + } + this.callbacks.rpcStateChanged(this); + } + } + checkSubscriptions() { var subscriptionsChanged = false; for (var i = 0; i < this.datasourceListeners.length; i++) { var listener = this.datasourceListeners[i]; - var deviceId = null; + var entityId = null; + var entityType = null; var aliasName = null; - if (listener.datasource.type === this.ctx.types.datasourceType.device) { - if (listener.datasource.deviceAliasId && - this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { - deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; - aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; + if (listener.datasource.type === this.ctx.types.datasourceType.entity) { + if (listener.datasource.entityAliasId && + this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId]) { + entityId = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityId; + entityType = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityType; + aliasName = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].alias; } - if (!angular.equals(deviceId, listener.deviceId) || + if (!angular.equals(entityId, listener.entityId) || + !angular.equals(entityType, listener.entityType) || !angular.equals(aliasName, listener.datasource.name)) { subscriptionsChanged = true; break; @@ -606,7 +615,7 @@ export default class Subscription { const varsRegex = /\$\{([^\}]*)\}/g; -function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) { +function updateDataKeyLabel(dataKey, dsName, entityName, aliasName) { var pattern = dataKey.pattern; var label = dataKey.pattern; var match = varsRegex.exec(pattern); @@ -615,8 +624,10 @@ function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) { var variableName = match[1]; if (variableName === 'dsName') { label = label.split(variable).join(dsName); + } else if (variableName === 'entityName') { + label = label.split(variable).join(entityName); } else if (variableName === 'deviceName') { - label = label.split(variable).join(deviceName); + label = label.split(variable).join(entityName); } else if (variableName === 'aliasName') { label = label.split(variable).join(aliasName); } diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 4dc41f7d99..a5bb36c4ca 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -262,7 +262,13 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi function fetchAllowedDashboardIds() { var pageLink = {limit: 100}; - dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then( + var fetchDashboardsPromise; + if (currentUser.authority === 'TENANT_ADMIN') { + fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink); + } else { + fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink); + } + fetchDashboardsPromise.then( function success(result) { var dashboards = result.data; for (var d=0;d + +
+ +
+

asset.add

+ +
+ + + +
+
+ + + +
+ +
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | translate }} + +
+
diff --git a/ui/src/app/asset/add-assets-to-customer.controller.js b/ui/src/app/asset/add-assets-to-customer.controller.js new file mode 100644 index 0000000000..53feb7deff --- /dev/null +++ b/ui/src/app/asset/add-assets-to-customer.controller.js @@ -0,0 +1,123 @@ +/* + * Copyright © 2016-2017 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. + */ +/*@ngInject*/ +export default function AddAssetsToCustomerController(assetService, $mdDialog, $q, customerId, assets) { + + var vm = this; + + vm.assets = assets; + vm.searchText = ''; + + vm.assign = assign; + vm.cancel = cancel; + vm.hasData = hasData; + vm.noData = noData; + vm.searchAssetTextUpdated = searchAssetTextUpdated; + vm.toggleAssetSelection = toggleAssetSelection; + + vm.theAssets = { + getItemAtIndex: function (index) { + if (index > vm.assets.data.length) { + vm.theAssets.fetchMoreItems_(index); + return null; + } + var item = vm.assets.data[index]; + if (item) { + item.indexNumber = index + 1; + } + return item; + }, + + getLength: function () { + if (vm.assets.hasNext) { + return vm.assets.data.length + vm.assets.nextPageLink.limit; + } else { + return vm.assets.data.length; + } + }, + + fetchMoreItems_: function () { + if (vm.assets.hasNext && !vm.assets.pending) { + vm.assets.pending = true; + assetService.getTenantAssets(vm.assets.nextPageLink, false).then( + function success(assets) { + vm.assets.data = vm.assets.data.concat(assets.data); + vm.assets.nextPageLink = assets.nextPageLink; + vm.assets.hasNext = assets.hasNext; + if (vm.assets.hasNext) { + vm.assets.nextPageLink.limit = vm.assets.pageSize; + } + vm.assets.pending = false; + }, + function fail() { + vm.assets.hasNext = false; + vm.assets.pending = false; + }); + } + } + }; + + function cancel () { + $mdDialog.cancel(); + } + + function assign() { + var tasks = []; + for (var assetId in vm.assets.selections) { + tasks.push(assetService.assignAssetToCustomer(customerId, assetId)); + } + $q.all(tasks).then(function () { + $mdDialog.hide(); + }); + } + + function noData() { + return vm.assets.data.length == 0 && !vm.assets.hasNext; + } + + function hasData() { + return vm.assets.data.length > 0; + } + + function toggleAssetSelection($event, asset) { + $event.stopPropagation(); + var selected = angular.isDefined(asset.selected) && asset.selected; + asset.selected = !selected; + if (asset.selected) { + vm.assets.selections[asset.id.id] = true; + vm.assets.selectedCount++; + } else { + delete vm.assets.selections[asset.id.id]; + vm.assets.selectedCount--; + } + } + + function searchAssetTextUpdated() { + vm.assets = { + pageSize: vm.assets.pageSize, + data: [], + nextPageLink: { + limit: vm.assets.pageSize, + textSearch: vm.searchText + }, + selections: {}, + selectedCount: 0, + hasNext: true, + pending: false + }; + } + +} diff --git a/ui/src/app/asset/add-assets-to-customer.tpl.html b/ui/src/app/asset/add-assets-to-customer.tpl.html new file mode 100644 index 0000000000..18e23ce0ca --- /dev/null +++ b/ui/src/app/asset/add-assets-to-customer.tpl.html @@ -0,0 +1,77 @@ + + +
+ +
+

asset.assign-asset-to-customer

+ + + + +
+
+ + + +
+
+ asset.assign-asset-to-customer-text + + + + search + + + +
+ asset.no-assets-text + + + + + {{ asset.name }} + + + +
+
+
+
+ + + + {{ 'action.assign' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
\ No newline at end of file diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html new file mode 100644 index 0000000000..3c06558a82 --- /dev/null +++ b/ui/src/app/asset/asset-card.tpl.html @@ -0,0 +1,19 @@ + +
{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
+
{{'asset.public' | translate}}
diff --git a/ui/src/app/asset/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html new file mode 100644 index 0000000000..8cf0c961b6 --- /dev/null +++ b/ui/src/app/asset/asset-fieldset.tpl.html @@ -0,0 +1,71 @@ + +{{ 'asset.make-public' | translate }} +{{ 'asset.assign-to-customer' | translate }} +{{ isPublic ? 'asset.make-private' : 'asset.unassign-from-customer' | translate }} +{{ 'asset.delete' | translate }} + +
+ + + asset.copyId + +
+ + + + + + +
+ {{ 'asset.asset-public' | translate }} +
+
+ + + +
+
asset.name-required
+
+
+ + + +
+
asset.type-required
+
+
+ + + + +
+
diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js new file mode 100644 index 0000000000..d0944ae143 --- /dev/null +++ b/ui/src/app/asset/asset.controller.js @@ -0,0 +1,506 @@ +/* + * Copyright © 2016-2017 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import addAssetTemplate from './add-asset.tpl.html'; +import assetCard from './asset-card.tpl.html'; +import assignToCustomerTemplate from './assign-to-customer.tpl.html'; +import addAssetsToCustomerTemplate from './add-assets-to-customer.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export function AssetCardController(types) { + + var vm = this; + + vm.types = types; + + vm.isAssignedToCustomer = function() { + if (vm.item && vm.item.customerId && vm.parentCtl.assetsScope === 'tenant' && + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } + + vm.isPublic = function() { + if (vm.item && vm.item.assignedCustomer && vm.parentCtl.assetsScope === 'tenant' && vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } +} + + +/*@ngInject*/ +export function AssetController(userService, assetService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) { + + var customerId = $stateParams.customerId; + + var assetActionsList = []; + + var assetGroupActionsList = []; + + var vm = this; + + vm.types = types; + + vm.assetGridConfig = { + deleteItemTitleFunc: deleteAssetTitle, + deleteItemContentFunc: deleteAssetText, + deleteItemsTitleFunc: deleteAssetsTitle, + deleteItemsActionTitleFunc: deleteAssetsActionTitle, + deleteItemsContentFunc: deleteAssetsText, + + saveItemFunc: saveAsset, + + getItemTitleFunc: getAssetTitle, + + itemCardController: 'AssetCardController', + itemCardTemplateUrl: assetCard, + parentCtl: vm, + + actionsList: assetActionsList, + groupActionsList: assetGroupActionsList, + + onGridInited: gridInited, + + addItemTemplateUrl: addAssetTemplate, + + addItemText: function() { return $translate.instant('asset.add-asset-text') }, + noItemsText: function() { return $translate.instant('asset.no-assets-text') }, + itemDetailsText: function() { return $translate.instant('asset.asset-details') }, + isDetailsReadOnly: isCustomerUser, + isSelectionEnabled: function () { + return !isCustomerUser(); + } + }; + + if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { + vm.assetGridConfig.items = $stateParams.items; + } + + if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { + vm.assetGridConfig.topIndex = $stateParams.topIndex; + } + + vm.assetsScope = $state.$current.data.assetsType; + + vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; + vm.unassignFromCustomer = unassignFromCustomer; + + initController(); + + function initController() { + var fetchAssetsFunction = null; + var deleteAssetFunction = null; + var refreshAssetsParamsFunction = null; + + var user = userService.getCurrentUser(); + + if (user.authority === 'CUSTOMER_USER') { + vm.assetsScope = 'customer_user'; + customerId = user.customerId; + } + if (customerId) { + vm.customerAssetsTitle = $translate.instant('customer.assets'); + customerService.getShortCustomerInfo(customerId).then( + function success(info) { + if (info.isPublic) { + vm.customerAssetsTitle = $translate.instant('customer.public-assets'); + } + } + ); + } + + if (vm.assetsScope === 'tenant') { + fetchAssetsFunction = function (pageLink) { + return assetService.getTenantAssets(pageLink, true); + }; + deleteAssetFunction = function (assetId) { + return assetService.deleteAsset(assetId); + }; + refreshAssetsParamsFunction = function() { + return {"topIndex": vm.topIndex}; + }; + + assetActionsList.push({ + onAction: function ($event, item) { + makePublic($event, item); + }, + name: function() { return $translate.instant('action.share') }, + details: function() { return $translate.instant('asset.make-public') }, + icon: "share", + isEnabled: function(asset) { + return asset && (!asset.customerId || asset.customerId.id === types.id.nullUid); + } + }); + + assetActionsList.push( + { + onAction: function ($event, item) { + assignToCustomer($event, [ item.id.id ]); + }, + name: function() { return $translate.instant('action.assign') }, + details: function() { return $translate.instant('asset.assign-to-customer') }, + icon: "assignment_ind", + isEnabled: function(asset) { + return asset && (!asset.customerId || asset.customerId.id === types.id.nullUid); + } + } + ); + + assetActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, false); + }, + name: function() { return $translate.instant('action.unassign') }, + details: function() { return $translate.instant('asset.unassign-from-customer') }, + icon: "assignment_return", + isEnabled: function(asset) { + return asset && asset.customerId && asset.customerId.id !== types.id.nullUid && !asset.assignedCustomer.isPublic; + } + } + ); + + assetActionsList.push({ + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('asset.make-private') }, + icon: "reply", + isEnabled: function(asset) { + return asset && asset.customerId && asset.customerId.id !== types.id.nullUid && asset.assignedCustomer.isPublic; + } + }); + + assetActionsList.push( + { + onAction: function ($event, item) { + vm.grid.deleteItem($event, item); + }, + name: function() { return $translate.instant('action.delete') }, + details: function() { return $translate.instant('asset.delete') }, + icon: "delete" + } + ); + + assetGroupActionsList.push( + { + onAction: function ($event, items) { + assignAssetsToCustomer($event, items); + }, + name: function() { return $translate.instant('asset.assign-assets') }, + details: function(selectedCount) { + return $translate.instant('asset.assign-assets-text', {count: selectedCount}, "messageformat"); + }, + icon: "assignment_ind" + } + ); + + assetGroupActionsList.push( + { + onAction: function ($event) { + vm.grid.deleteItems($event); + }, + name: function() { return $translate.instant('asset.delete-assets') }, + details: deleteAssetsActionTitle, + icon: "delete" + } + ); + + + + } else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') { + fetchAssetsFunction = function (pageLink) { + return assetService.getCustomerAssets(customerId, pageLink, true); + }; + deleteAssetFunction = function (assetId) { + return assetService.unassignAssetFromCustomer(assetId); + }; + refreshAssetsParamsFunction = function () { + return {"customerId": customerId, "topIndex": vm.topIndex}; + }; + + if (vm.assetsScope === 'customer') { + assetActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, false); + }, + name: function() { return $translate.instant('action.unassign') }, + details: function() { return $translate.instant('asset.unassign-from-customer') }, + icon: "assignment_return", + isEnabled: function(asset) { + return asset && !asset.assignedCustomer.isPublic; + } + } + ); + assetActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('asset.make-private') }, + icon: "reply", + isEnabled: function(asset) { + return asset && asset.assignedCustomer.isPublic; + } + } + ); + + assetGroupActionsList.push( + { + onAction: function ($event, items) { + unassignAssetsFromCustomer($event, items); + }, + name: function() { return $translate.instant('asset.unassign-assets') }, + details: function(selectedCount) { + return $translate.instant('asset.unassign-assets-action-title', {count: selectedCount}, "messageformat"); + }, + icon: "assignment_return" + } + ); + + vm.assetGridConfig.addItemAction = { + onAction: function ($event) { + addAssetsToCustomer($event); + }, + name: function() { return $translate.instant('asset.assign-assets') }, + details: function() { return $translate.instant('asset.assign-new-asset') }, + icon: "add" + }; + + + } else if (vm.assetsScope === 'customer_user') { + vm.assetGridConfig.addItemAction = {}; + } + } + + vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction; + vm.assetGridConfig.fetchItemsFunc = fetchAssetsFunction; + vm.assetGridConfig.deleteItemFunc = deleteAssetFunction; + + } + + function deleteAssetTitle(asset) { + return $translate.instant('asset.delete-asset-title', {assetName: asset.name}); + } + + function deleteAssetText() { + return $translate.instant('asset.delete-asset-text'); + } + + function deleteAssetsTitle(selectedCount) { + return $translate.instant('asset.delete-assets-title', {count: selectedCount}, 'messageformat'); + } + + function deleteAssetsActionTitle(selectedCount) { + return $translate.instant('asset.delete-assets-action-title', {count: selectedCount}, 'messageformat'); + } + + function deleteAssetsText () { + return $translate.instant('asset.delete-assets-text'); + } + + function gridInited(grid) { + vm.grid = grid; + } + + function getAssetTitle(asset) { + return asset ? asset.name : ''; + } + + function saveAsset(asset) { + var deferred = $q.defer(); + assetService.saveAsset(asset).then( + function success(savedAsset) { + var assets = [ savedAsset ]; + customerService.applyAssignedCustomersInfo(assets).then( + function success(items) { + if (items && items.length == 1) { + deferred.resolve(items[0]); + } else { + deferred.reject(); + } + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + function isCustomerUser() { + return vm.assetsScope === 'customer_user'; + } + + function assignToCustomer($event, assetIds) { + if ($event) { + $event.stopPropagation(); + } + var pageSize = 10; + customerService.getCustomers({limit: pageSize, textSearch: ''}).then( + function success(_customers) { + var customers = { + pageSize: pageSize, + data: _customers.data, + nextPageLink: _customers.nextPageLink, + selection: null, + hasNext: _customers.hasNext, + pending: false + }; + if (customers.hasNext) { + customers.nextPageLink.limit = pageSize; + } + $mdDialog.show({ + controller: 'AssignAssetToCustomerController', + controllerAs: 'vm', + templateUrl: assignToCustomerTemplate, + locals: {assetIds: assetIds, customers: customers}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event + }).then(function () { + vm.grid.refreshList(); + }, function () { + }); + }, + function fail() { + }); + } + + function addAssetsToCustomer($event) { + if ($event) { + $event.stopPropagation(); + } + var pageSize = 10; + assetService.getTenantAssets({limit: pageSize, textSearch: ''}, false).then( + function success(_assets) { + var assets = { + pageSize: pageSize, + data: _assets.data, + nextPageLink: _assets.nextPageLink, + selections: {}, + selectedCount: 0, + hasNext: _assets.hasNext, + pending: false + }; + if (assets.hasNext) { + assets.nextPageLink.limit = pageSize; + } + $mdDialog.show({ + controller: 'AddAssetsToCustomerController', + controllerAs: 'vm', + templateUrl: addAssetsToCustomerTemplate, + locals: {customerId: customerId, assets: assets}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event + }).then(function () { + vm.grid.refreshList(); + }, function () { + }); + }, + function fail() { + }); + } + + function assignAssetsToCustomer($event, items) { + var assetIds = []; + for (var id in items.selections) { + assetIds.push(id); + } + assignToCustomer($event, assetIds); + } + + function unassignFromCustomer($event, asset, isPublic) { + if ($event) { + $event.stopPropagation(); + } + var title; + var content; + var label; + if (isPublic) { + title = $translate.instant('asset.make-private-asset-title', {assetName: asset.name}); + content = $translate.instant('asset.make-private-asset-text'); + label = $translate.instant('asset.make-private'); + } else { + title = $translate.instant('asset.unassign-asset-title', {assetName: asset.name}); + content = $translate.instant('asset.unassign-asset-text'); + label = $translate.instant('asset.unassign-asset'); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(label) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + assetService.unassignAssetFromCustomer(asset.id.id).then(function success() { + vm.grid.refreshList(); + }); + }); + } + + function unassignAssetsFromCustomer($event, items) { + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('asset.unassign-assets-title', {count: items.selectedCount}, 'messageformat')) + .htmlContent($translate.instant('asset.unassign-assets-text')) + .ariaLabel($translate.instant('asset.unassign-asset')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + var tasks = []; + for (var id in items.selections) { + tasks.push(assetService.unassignAssetFromCustomer(id)); + } + $q.all(tasks).then(function () { + vm.grid.refreshList(); + }); + }); + } + + function makePublic($event, asset) { + if ($event) { + $event.stopPropagation(); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('asset.make-public-asset-title', {assetName: asset.name})) + .htmlContent($translate.instant('asset.make-public-asset-text')) + .ariaLabel($translate.instant('asset.make-public')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + assetService.makeAssetPublic(asset.id.id).then(function success() { + vm.grid.refreshList(); + }); + }); + } +} diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js new file mode 100644 index 0000000000..8c13082735 --- /dev/null +++ b/ui/src/app/asset/asset.directive.js @@ -0,0 +1,71 @@ +/* + * Copyright © 2016-2017 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import assetFieldsetTemplate from './asset-fieldset.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function AssetDirective($compile, $templateCache, toast, $translate, types, assetService, customerService) { + var linker = function (scope, element) { + var template = $templateCache.get(assetFieldsetTemplate); + element.html(template); + + scope.isAssignedToCustomer = false; + scope.isPublic = false; + scope.assignedCustomer = null; + + scope.$watch('asset', function(newVal) { + if (newVal) { + if (scope.asset.customerId && scope.asset.customerId.id !== types.id.nullUid) { + scope.isAssignedToCustomer = true; + customerService.getShortCustomerInfo(scope.asset.customerId.id).then( + function success(customer) { + scope.assignedCustomer = customer; + scope.isPublic = customer.isPublic; + } + ); + } else { + scope.isAssignedToCustomer = false; + scope.isPublic = false; + scope.assignedCustomer = null; + } + } + }); + + scope.onAssetIdCopied = function() { + toast.showSuccess($translate.instant('asset.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + + + $compile(element.contents())(scope); + } + return { + restrict: "E", + link: linker, + scope: { + asset: '=', + isEdit: '=', + assetScope: '=', + theForm: '=', + onAssignToCustomer: '&', + onMakePublic: '&', + onUnassignFromCustomer: '&', + onDeleteAsset: '&' + } + }; +} diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js new file mode 100644 index 0000000000..c9a312df40 --- /dev/null +++ b/ui/src/app/asset/asset.routes.js @@ -0,0 +1,68 @@ +/* + * Copyright © 2016-2017 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import assetsTemplate from './assets.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function AssetRoutes($stateProvider) { + $stateProvider + .state('home.assets', { + url: '/assets', + params: {'topIndex': 0}, + module: 'private', + auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], + views: { + "content@home": { + templateUrl: assetsTemplate, + controller: 'AssetController', + controllerAs: 'vm' + } + }, + data: { + assetsType: 'tenant', + searchEnabled: true, + pageTitle: 'asset.assets' + }, + ncyBreadcrumb: { + label: '{"icon": "domain", "label": "asset.assets"}' + } + }) + .state('home.customers.assets', { + url: '/:customerId/assets', + params: {'topIndex': 0}, + module: 'private', + auth: ['TENANT_ADMIN'], + views: { + "content@home": { + templateUrl: assetsTemplate, + controllerAs: 'vm', + controller: 'AssetController' + } + }, + data: { + assetsType: 'customer', + searchEnabled: true, + pageTitle: 'customer.assets' + }, + ncyBreadcrumb: { + label: '{"icon": "domain", "label": "{{ vm.customerAssetsTitle }}", "translate": "false"}' + } + }); + +} diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html new file mode 100644 index 0000000000..fb3cf56c86 --- /dev/null +++ b/ui/src/app/asset/assets.tpl.html @@ -0,0 +1,58 @@ + + + +
+
+ + + + + + + + + + + + + + + + +
diff --git a/ui/src/app/asset/assign-to-customer.controller.js b/ui/src/app/asset/assign-to-customer.controller.js new file mode 100644 index 0000000000..602e599ae9 --- /dev/null +++ b/ui/src/app/asset/assign-to-customer.controller.js @@ -0,0 +1,123 @@ +/* + * Copyright © 2016-2017 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. + */ +/*@ngInject*/ +export default function AssignAssetToCustomerController(customerService, assetService, $mdDialog, $q, assetIds, customers) { + + var vm = this; + + vm.customers = customers; + vm.searchText = ''; + + vm.assign = assign; + vm.cancel = cancel; + vm.isCustomerSelected = isCustomerSelected; + vm.hasData = hasData; + vm.noData = noData; + vm.searchCustomerTextUpdated = searchCustomerTextUpdated; + vm.toggleCustomerSelection = toggleCustomerSelection; + + vm.theCustomers = { + getItemAtIndex: function (index) { + if (index > vm.customers.data.length) { + vm.theCustomers.fetchMoreItems_(index); + return null; + } + var item = vm.customers.data[index]; + if (item) { + item.indexNumber = index + 1; + } + return item; + }, + + getLength: function () { + if (vm.customers.hasNext) { + return vm.customers.data.length + vm.customers.nextPageLink.limit; + } else { + return vm.customers.data.length; + } + }, + + fetchMoreItems_: function () { + if (vm.customers.hasNext && !vm.customers.pending) { + vm.customers.pending = true; + customerService.getCustomers(vm.customers.nextPageLink).then( + function success(customers) { + vm.customers.data = vm.customers.data.concat(customers.data); + vm.customers.nextPageLink = customers.nextPageLink; + vm.customers.hasNext = customers.hasNext; + if (vm.customers.hasNext) { + vm.customers.nextPageLink.limit = vm.customers.pageSize; + } + vm.customers.pending = false; + }, + function fail() { + vm.customers.hasNext = false; + vm.customers.pending = false; + }); + } + } + }; + + function cancel() { + $mdDialog.cancel(); + } + + function assign() { + var tasks = []; + for (var assetId in assetIds) { + tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId])); + } + $q.all(tasks).then(function () { + $mdDialog.hide(); + }); + } + + function noData() { + return vm.customers.data.length == 0 && !vm.customers.hasNext; + } + + function hasData() { + return vm.customers.data.length > 0; + } + + function toggleCustomerSelection($event, customer) { + $event.stopPropagation(); + if (vm.isCustomerSelected(customer)) { + vm.customers.selection = null; + } else { + vm.customers.selection = customer; + } + } + + function isCustomerSelected(customer) { + return vm.customers.selection != null && customer && + customer.id.id === vm.customers.selection.id.id; + } + + function searchCustomerTextUpdated() { + vm.customers = { + pageSize: vm.customers.pageSize, + data: [], + nextPageLink: { + limit: vm.customers.pageSize, + textSearch: vm.searchText + }, + selection: null, + hasNext: true, + pending: false + }; + } +} diff --git a/ui/src/app/asset/assign-to-customer.tpl.html b/ui/src/app/asset/assign-to-customer.tpl.html new file mode 100644 index 0000000000..fba56cec77 --- /dev/null +++ b/ui/src/app/asset/assign-to-customer.tpl.html @@ -0,0 +1,76 @@ + + +
+ +
+

asset.assign-asset-to-customer

+ + + + +
+
+ + + +
+
+ asset.assign-to-customer-text + + + + search + + + +
+ customer.no-customers-text + + + + + {{ customer.title }} + + + +
+
+
+
+ + + + {{ 'action.assign' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
\ No newline at end of file diff --git a/ui/src/app/asset/index.js b/ui/src/app/asset/index.js new file mode 100644 index 0000000000..62ab201b69 --- /dev/null +++ b/ui/src/app/asset/index.js @@ -0,0 +1,43 @@ +/* + * Copyright © 2016-2017 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 uiRouter from 'angular-ui-router'; +import thingsboardGrid from '../components/grid.directive'; +import thingsboardEvent from '../event'; +import thingsboardApiUser from '../api/user.service'; +import thingsboardApiAsset from '../api/asset.service'; +import thingsboardApiCustomer from '../api/customer.service'; + +import AssetRoutes from './asset.routes'; +import {AssetController, AssetCardController} from './asset.controller'; +import AssignAssetToCustomerController from './assign-to-customer.controller'; +import AddAssetsToCustomerController from './add-assets-to-customer.controller'; +import AssetDirective from './asset.directive'; + +export default angular.module('thingsboard.asset', [ + uiRouter, + thingsboardGrid, + thingsboardEvent, + thingsboardApiUser, + thingsboardApiAsset, + thingsboardApiCustomer +]) + .config(AssetRoutes) + .controller('AssetController', AssetController) + .controller('AssetCardController', AssetCardController) + .controller('AssignAssetToCustomerController', AssignAssetToCustomerController) + .controller('AddAssetsToCustomerController', AddAssetsToCustomerController) + .directive('tbAsset', AssetDirective) + .name; diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js new file mode 100644 index 0000000000..57317b5b71 --- /dev/null +++ b/ui/src/app/common/dashboard-utils.service.js @@ -0,0 +1,437 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default angular.module('thingsboard.dashboardUtils', []) + .factory('dashboardUtils', DashboardUtils) + .name; + +/*@ngInject*/ +function DashboardUtils(types, utils, timeService) { + + var service = { + validateAndUpdateDashboard: validateAndUpdateDashboard, + getRootStateId: getRootStateId, + createSingleWidgetDashboard: createSingleWidgetDashboard, + getStateLayoutsData: getStateLayoutsData, + createDefaultState: createDefaultState, + createDefaultLayoutData: createDefaultLayoutData, + setLayouts: setLayouts, + updateLayoutSettings: updateLayoutSettings, + addWidgetToLayout: addWidgetToLayout, + removeWidgetFromLayout: removeWidgetFromLayout, + isSingleLayoutDashboard: isSingleLayoutDashboard, + removeUnusedWidgets: removeUnusedWidgets, + getWidgetsArray: getWidgetsArray + }; + + return service; + + function validateAndUpdateEntityAliases(configuration) { + if (angular.isUndefined(configuration.entityAliases)) { + configuration.entityAliases = {}; + if (configuration.deviceAliases) { + var deviceAliases = configuration.deviceAliases; + for (var aliasId in deviceAliases) { + var deviceAlias = deviceAliases[aliasId]; + var alias = deviceAlias.alias; + var entityFilter = { + useFilter: false, + entityNameFilter: '', + entityList: [] + } + if (deviceAlias.deviceFilter) { + entityFilter.useFilter = deviceAlias.deviceFilter.useFilter; + entityFilter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter; + entityFilter.entityList = deviceAlias.deviceFilter.deviceList; + } else if (deviceAlias.deviceId) { + entityFilter.entityList = [deviceAlias.deviceId]; + } + var entityAlias = { + id: aliasId, + alias: alias, + entityType: types.entityType.device, + entityFilter: entityFilter + }; + configuration.entityAliases[aliasId] = entityAlias; + } + delete configuration.deviceAliases; + } + } + return configuration; + } + + function validateAndUpdateWidget(widget) { + if (!widget.config) { + widget.config = {}; + } + if (!widget.config.datasources) { + widget.config.datasources = []; + } + widget.config.datasources.forEach(function(datasource) { + if (datasource.type === 'device') { + datasource.type = types.datasourceType.entity; + } + if (datasource.deviceAliasId) { + datasource.entityAliasId = datasource.deviceAliasId; + delete datasource.deviceAliasId; + } + }); + return widget; + } + + function createDefaultLayoutData() { + return { + widgets: {}, + gridSettings: { + backgroundColor: '#eeeeee', + color: 'rgba(0,0,0,0.870588)', + columns: 24, + margins: [10, 10], + backgroundSizeMode: '100%' + } + }; + } + + function createDefaultLayouts() { + return { + 'main': createDefaultLayoutData() + }; + } + + function createDefaultState(name, root) { + return { + name: name, + root: root, + layouts: createDefaultLayouts() + } + } + + function validateAndUpdateDashboard(dashboard) { + if (!dashboard.configuration) { + dashboard.configuration = {}; + } + if (angular.isUndefined(dashboard.configuration.widgets)) { + dashboard.configuration.widgets = {}; + } else if (angular.isArray(dashboard.configuration.widgets)) { + var widgetsMap = {}; + dashboard.configuration.widgets.forEach(function (widget) { + if (!widget.id) { + widget.id = utils.guid(); + } + widgetsMap[widget.id] = validateAndUpdateWidget(widget); + }); + dashboard.configuration.widgets = widgetsMap; + } + if (angular.isUndefined(dashboard.configuration.states)) { + dashboard.configuration.states = { + 'default': createDefaultState('Default', true) + }; + + var mainLayout = dashboard.configuration.states['default'].layouts['main']; + for (var id in dashboard.configuration.widgets) { + var widget = dashboard.configuration.widgets[id]; + mainLayout.widgets[id] = { + sizeX: widget.sizeX, + sizeY: widget.sizeY, + row: widget.row, + col: widget.col, + }; + } + } else { + var states = dashboard.configuration.states; + var rootFound = false; + for (var stateId in states) { + var state = states[stateId]; + if (angular.isUndefined(state.root)) { + state.root = false; + } else if (state.root) { + rootFound = true; + } + } + if (!rootFound) { + var firstStateId = Object.keys(states)[0]; + states[firstStateId].root = true; + } + } + dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration); + + if (angular.isUndefined(dashboard.configuration.timewindow)) { + dashboard.configuration.timewindow = timeService.defaultTimewindow(); + } + if (angular.isUndefined(dashboard.configuration.settings)) { + dashboard.configuration.settings = {}; + dashboard.configuration.settings.stateControllerId = 'default'; + dashboard.configuration.settings.showTitle = true; + dashboard.configuration.settings.showDashboardsSelect = true; + dashboard.configuration.settings.showEntitiesSelect = true; + dashboard.configuration.settings.showDashboardTimewindow = true; + dashboard.configuration.settings.showDashboardExport = true; + } else { + if (angular.isUndefined(dashboard.configuration.settings.stateControllerId)) { + dashboard.configuration.settings.stateControllerId = 'default'; + } + } + if (angular.isDefined(dashboard.configuration.gridSettings)) { + var gridSettings = dashboard.configuration.gridSettings; + if (angular.isDefined(gridSettings.showTitle)) { + dashboard.configuration.settings.showTitle = gridSettings.showTitle; + delete gridSettings.showTitle; + } + if (angular.isDefined(gridSettings.titleColor)) { + dashboard.configuration.settings.titleColor = gridSettings.titleColor; + delete gridSettings.titleColor; + } + if (angular.isDefined(gridSettings.showDevicesSelect)) { + dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect; + delete gridSettings.showDevicesSelect; + } + if (angular.isDefined(gridSettings.showEntitiesSelect)) { + dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect; + delete gridSettings.showEntitiesSelect; + } + if (angular.isDefined(gridSettings.showDashboardTimewindow)) { + dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow; + delete gridSettings.showDashboardTimewindow; + } + if (angular.isDefined(gridSettings.showDashboardExport)) { + dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport; + delete gridSettings.showDashboardExport; + } + dashboard.configuration.states['default'].layouts['main'].gridSettings = gridSettings; + delete dashboard.configuration.gridSettings; + } + return dashboard; + } + + function getRootStateId(states) { + for (var stateId in states) { + var state = states[stateId]; + if (state.root) { + return stateId; + } + } + return Object.keys(states)[0]; + } + + function createSingleWidgetDashboard(widget) { + if (!widget.id) { + widget.id = utils.guid(); + } + var dashboard = {}; + dashboard = validateAndUpdateDashboard(dashboard); + dashboard.configuration.widgets[widget.id] = widget; + dashboard.configuration.states['default'].layouts['main'].widgets[widget.id] = { + sizeX: widget.sizeX, + sizeY: widget.sizeY, + row: widget.row, + col: widget.col, + }; + return dashboard; + } + + function getStateLayoutsData(dashboard, targetState) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var state = states[targetState]; + if (state) { + var allWidgets = dashboardConfiguration.widgets; + var result = {}; + for (var l in state.layouts) { + var layout = state.layouts[l]; + if (layout) { + result[l] = { + widgets: [], + widgetLayouts: {}, + gridSettings: {} + } + for (var id in layout.widgets) { + result[l].widgets.push(allWidgets[id]); + } + result[l].widgetLayouts = layout.widgets; + result[l].gridSettings = layout.gridSettings; + } + } + return result; + } else { + return null; + } + } + + function setLayouts(dashboard, targetState, newLayouts) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var state = states[targetState]; + var addedCount = 0; + var removedCount = 0; + for (var l in state.layouts) { + if (!newLayouts[l]) { + removedCount++; + } + } + for (l in newLayouts) { + if (!state.layouts[l]) { + addedCount++; + } + } + state.layouts = newLayouts; + var layoutsCount = Object.keys(state.layouts).length; + var newColumns; + if (addedCount) { + for (l in state.layouts) { + newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount; + state.layouts[l].gridSettings.columns = newColumns; + } + } + if (removedCount) { + for (l in state.layouts) { + newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount; + state.layouts[l].gridSettings.columns = newColumns; + } + } + removeUnusedWidgets(dashboard); + } + + function updateLayoutSettings(layout, gridSettings) { + var prevGridSettings = layout.gridSettings; + var prevColumns = prevGridSettings ? prevGridSettings.columns : 24; + var ratio = gridSettings.columns / prevColumns; + layout.gridSettings = gridSettings; + for (var w in layout.widgets) { + var widget = layout.widgets[w]; + widget.sizeX = Math.round(widget.sizeX * ratio); + widget.sizeY = Math.round(widget.sizeY * ratio); + widget.col = Math.round(widget.col * ratio); + widget.row = Math.round(widget.row * ratio); + } + } + + function addWidgetToLayout(dashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var state = states[targetState]; + var layout = state.layouts[targetLayout]; + var layoutCount = Object.keys(state.layouts).length; + if (!widget.id) { + widget.id = utils.guid(); + } + if (!dashboardConfiguration.widgets[widget.id]) { + dashboardConfiguration.widgets[widget.id] = widget; + } + var widgetLayout = { + sizeX: originalSize ? originalSize.sizeX : widget.sizeX, + sizeY: originalSize ? originalSize.sizeY : widget.sizeY, + mobileOrder: widget.config.mobileOrder, + mobileHeight: widget.config.mobileHeight + }; + + if (angular.isUndefined(originalColumns)) { + originalColumns = 24; + } + + var gridSettings = layout.gridSettings; + var columns = 24; + if (gridSettings && gridSettings.columns) { + columns = gridSettings.columns; + } + + columns = columns * layoutCount; + + if (columns != originalColumns) { + var ratio = columns / originalColumns; + widgetLayout.sizeX *= ratio; + widgetLayout.sizeY *= ratio; + } + + if (row > -1 && column > - 1) { + widgetLayout.row = row; + widgetLayout.col = column; + } else { + row = 0; + for (var w in layout.widgets) { + var existingLayout = layout.widgets[w]; + var wRow = existingLayout.row ? existingLayout.row : 0; + var wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1; + var bottom = wRow + wSizeY; + row = Math.max(row, bottom); + } + widgetLayout.row = row; + widgetLayout.col = 0; + } + + layout.widgets[widget.id] = widgetLayout; + } + + function removeWidgetFromLayout(dashboard, targetState, targetLayout, widgetId) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var state = states[targetState]; + var layout = state.layouts[targetLayout]; + delete layout.widgets[widgetId]; + removeUnusedWidgets(dashboard); + } + + function isSingleLayoutDashboard(dashboard) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var stateKeys = Object.keys(states); + if (stateKeys.length === 1) { + var state = states[stateKeys[0]]; + var layouts = state.layouts; + var layoutKeys = Object.keys(layouts); + if (layoutKeys.length === 1) { + return { + state: stateKeys[0], + layout: layoutKeys[0] + } + } + } + return null; + } + + function removeUnusedWidgets(dashboard) { + var dashboardConfiguration = dashboard.configuration; + var states = dashboardConfiguration.states; + var widgets = dashboardConfiguration.widgets; + for (var widgetId in widgets) { + var found = false; + for (var s in states) { + var state = states[s]; + for (var l in state.layouts) { + var layout = state.layouts[l]; + if (layout.widgets[widgetId]) { + found = true; + break; + } + } + } + if (!found) { + delete dashboardConfiguration.widgets[widgetId]; + } + + } + } + + function getWidgetsArray(dashboard) { + var widgetsArray = []; + var dashboardConfiguration = dashboard.configuration; + var widgets = dashboardConfiguration.widgets; + for (var widgetId in widgets) { + var widget = widgets[widgetId]; + widgetsArray.push(widget); + } + return widgetsArray; + } +} diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index df321f795b..a8d8556f38 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -79,7 +79,7 @@ export default angular.module('thingsboard.types', []) }, datasourceType: { function: "function", - device: "device" + entity: "entity" }, dataKeyType: { timeseries: "timeseries", @@ -93,11 +93,20 @@ export default angular.module('thingsboard.types', []) plugin: "PLUGIN" }, entityType: { - tenant: "TENANT", device: "DEVICE", - customer: "CUSTOMER", + asset: "ASSET", rule: "RULE", - plugin: "PLUGIN" + plugin: "PLUGIN", + tenant: "TENANT", + customer: "CUSTOMER" + }, + entitySearchDirection: { + from: "FROM", + to: "TO" + }, + entityRelationType: { + contains: "Contains", + manages: "Manages" }, eventType: { alarm: { @@ -122,7 +131,7 @@ export default angular.module('thingsboard.types', []) name: "attribute.scope-latest-telemetry", clientSide: true }, - deviceAttributesScope: { + attributesScope: { client: { value: "CLIENT_SCOPE", name: "attribute.scope-client", @@ -198,6 +207,9 @@ export default angular.module('thingsboard.types', []) systemBundleAlias: { charts: "charts", cards: "cards" + }, + translate: { + dashboardStatePrefix: "dashboardState.state." } } ).name; diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 6e05bbbf6c..b324cbfa45 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) .name; /*@ngInject*/ -function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) { +function Utils($mdColorPalette, $rootScope, $window, types) { var predefinedFunctions = {}, predefinedFunctionsList = [], @@ -106,8 +106,9 @@ function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) { isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, filterSearchTextEntities: filterSearchTextEntities, guid: guid, - createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo, - isLocalUrl: isLocalUrl + isLocalUrl: isLocalUrl, + validateDatasources: validateDatasources, + createKey: createKey } return service; @@ -300,21 +301,37 @@ function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) { return getMaterialColor(index); } - /*var defaultDataKey = { - name: 'f(x)', - type: types.dataKeyType.function, - label: 'Sin', - color: getMaterialColor(0), - funcBody: getPredefinedFunctionBody('Sin'), - settings: {}, - _hash: Math.random() - }; + function isLocalUrl(url) { + var parser = document.createElement('a'); //eslint-disable-line + parser.href = url; + var host = parser.hostname; + if (host === "localhost" || host === "127.0.0.1") { + return true; + } else { + return false; + } + } - var defaultDatasource = { - type: types.datasourceType.function, - name: types.datasourceType.function, - dataKeys: [angular.copy(defaultDataKey)] - };*/ + function validateDatasources(datasources) { + datasources.forEach(function (datasource) { + if (datasource.type === 'device') { + datasource.type = types.datasourceType.entity; + datasource.entityType = types.entityType.device; + if (datasource.deviceId) { + datasource.entityId = datasource.deviceId; + } else if (datasource.deviceAliasId) { + datasource.entityAliasId = datasource.deviceAliasId; + } + if (datasource.deviceName) { + datasource.entityName = datasource.deviceName; + } + } + if (datasource.type === types.datasourceType.entity && datasource.entityId) { + datasource.name = datasource.entityName; + } + }); + return datasources; + } function createKey(keyInfo, type, datasources) { var dataKey = { @@ -329,115 +346,4 @@ function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) { return dataKey; } - function createDatasourceKeys(keyInfos, type, datasource, datasources) { - for (var i=0;i 0) { - for (var i = 0; i < devices.length; i++) { - var device = devices[i]; - createDatasourceFromSubscription(subscriptionInfo, datasources, device); - } - } - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - }, - function fail() { - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - } - ) - } else { - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - } - } else if (subscriptionInfo.type === types.datasourceType.function) { - createDatasourceFromSubscription(subscriptionInfo, datasources); - index++; - processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); - } - } else { - deferred.resolve(datasources); - } - } - - function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) { - var deferred = $q.defer(); - var datasources = []; - processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred); - return deferred.promise; - } - - function isLocalUrl(url) { - var parser = document.createElement('a'); //eslint-disable-line - parser.href = url; - var host = parser.hostname; - if (host === "localhost" || host === "127.0.0.1") { - return true; - } else { - return false; - } - } - } diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js index afa5f566ae..77e3c1c5bd 100644 --- a/ui/src/app/components/dashboard-autocomplete.directive.js +++ b/ui/src/app/components/dashboard-autocomplete.directive.js @@ -53,7 +53,15 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u promise = $q.when({data: []}); } } else { - promise = dashboardService.getTenantDashboards(pageLink); + if (userService.getAuthority() === 'SYS_ADMIN') { + if (scope.tenantId) { + promise = dashboardService.getTenantDashboardsByTenantId(scope.tenantId, pageLink); + } else { + promise = $q.when({data: []}); + } + } else { + promise = dashboardService.getTenantDashboards(pageLink); + } } promise.then(function success(result) { @@ -76,7 +84,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { - dashboardService.getDashboard(ngModelCtrl.$viewValue).then( + dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then( function success(dashboard) { scope.dashboard = dashboard; }, @@ -117,6 +125,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u link: linker, scope: { dashboardsScope: '@', + tenantId: '=', customerId: '=', theForm: '=?', tbRequired: '=?', diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index 131616be0c..f26121ccea 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -51,7 +51,9 @@ function Dashboard() { scope: true, bindToController: { widgets: '=', + widgetLayouts: '=?', aliasesInfo: '=', + stateController: '=', dashboardTimewindow: '=?', columns: '=', margins: '=', @@ -73,7 +75,8 @@ function Dashboard() { onInit: '&?', onInitFailed: '&?', dashboardStyle: '=?', - dashboardClass: '=?' + dashboardClass: '=?', + ignoreLoading: '=?' }, controller: DashboardController, controllerAs: 'vm', @@ -82,7 +85,7 @@ function Dashboard() { } /*@ngInject*/ -function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) { +function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types, utils) { var highlightedMode = false; var highlightedWidget = null; @@ -132,14 +135,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t updateMobileOpts(); + vm.widgetLayoutInfo = { + }; + vm.widgetItemMap = { + sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX', + sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY', + row: 'vm.widgetLayoutInfo[widget.id].row', + col: 'vm.widgetLayoutInfo[widget.id].col', + minSizeY: 'widget.minSizeY', + maxSizeY: 'widget.maxSizeY' + }; + + /*vm.widgetItemMap = { sizeX: 'vm.widgetSizeX(widget)', sizeY: 'vm.widgetSizeY(widget)', - row: 'widget.row', - col: 'widget.col', + row: 'vm.widgetRow(widget)', + col: 'vm.widgetCol(widget)', minSizeY: 'widget.minSizeY', maxSizeY: 'widget.maxSizeY' - }; + };*/ vm.isWidgetExpanded = false; vm.isHighlighted = isHighlighted; @@ -156,6 +171,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.widgetSizeX = widgetSizeX; vm.widgetSizeY = widgetSizeY; + vm.widgetRow = widgetRow; + vm.widgetCol = widgetCol; vm.widgetColor = widgetColor; vm.widgetBackgroundColor = widgetBackgroundColor; vm.widgetPadding = widgetPadding; @@ -173,6 +190,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.openWidgetContextMenu = openWidgetContextMenu; vm.getEventGridPosition = getEventGridPosition; + vm.reload = reload; vm.contextMenuItems = []; vm.contextMenuEvent = null; @@ -199,6 +217,45 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } }; + $scope.$watchCollection('vm.widgets', function () { + var ids = []; + for (var i=0;i - + ng-style="vm.dashboardStyle" + ng-show="((vm.loading() || vm.dashboardLoading) && !vm.isEdit) || vm.isResizing"> + + @@ -88,6 +90,7 @@ locals="{ visibleRect: vm.visibleRect, widget: widget, aliasesInfo: vm.aliasesInfo, + stateController: vm.stateController, isEdit: vm.isEdit, stDiff: vm.stDiff, dashboardTimewindow: vm.dashboardTimewindow, diff --git a/ui/src/app/components/datakey-config-dialog.controller.js b/ui/src/app/components/datakey-config-dialog.controller.js index 9bdaae2d12..a8741f5e3f 100644 --- a/ui/src/app/components/datakey-config-dialog.controller.js +++ b/ui/src/app/components/datakey-config-dialog.controller.js @@ -20,14 +20,14 @@ export default angular.module('thingsboard.dialogs.datakeyConfigDialog', [things .name; /*@ngInject*/ -function DatakeyConfigDialogController($scope, $mdDialog, deviceService, dataKey, dataKeySettingsSchema, deviceAlias, deviceAliases) { +function DatakeyConfigDialogController($scope, $mdDialog, entityService, dataKey, dataKeySettingsSchema, entityAlias, entityAliases) { var vm = this; vm.dataKey = dataKey; vm.dataKeySettingsSchema = dataKeySettingsSchema; - vm.deviceAlias = deviceAlias; - vm.deviceAliases = deviceAliases; + vm.entityAlias = entityAlias; + vm.entityAliases = entityAliases; vm.hide = function () { $mdDialog.hide(); @@ -37,10 +37,10 @@ function DatakeyConfigDialogController($scope, $mdDialog, deviceService, dataKey $mdDialog.cancel(); }; - vm.fetchDeviceKeys = function (deviceAliasId, query, type) { - var alias = vm.deviceAliases[deviceAliasId]; + vm.fetchEntityKeys = function (entityAliasId, query, type) { + var alias = vm.entityAliases[entityAliasId]; if (alias) { - return deviceService.getDeviceKeys(alias.deviceId, query, type); + return entityService.getEntityKeys(alias.entityType, alias.entityId, query, type); } else { return []; } diff --git a/ui/src/app/components/datakey-config-dialog.tpl.html b/ui/src/app/components/datakey-config-dialog.tpl.html index e827238a00..a55c3161c1 100644 --- a/ui/src/app/components/datakey-config-dialog.tpl.html +++ b/ui/src/app/components/datakey-config-dialog.tpl.html @@ -30,8 +30,8 @@ diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js index 50bbfcbf7e..9b3081ecef 100644 --- a/ui/src/app/components/datakey-config.directive.js +++ b/ui/src/app/components/datakey-config.directive.js @@ -108,9 +108,9 @@ function DatakeyConfig($compile, $templateCache, $q, types) { }, true); scope.keysSearch = function (searchText) { - if (scope.deviceAlias) { + if (scope.entityAlias) { var deferred = $q.defer(); - scope.fetchDeviceKeys({deviceAliasId: scope.deviceAlias.id, query: searchText, type: scope.model.type}) + scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: scope.model.type}) .then(function (keys) { keys.push(searchText); deferred.resolve(keys); @@ -130,8 +130,8 @@ function DatakeyConfig($compile, $templateCache, $q, types) { restrict: 'E', require: '^ngModel', scope: { - deviceAlias: '=', - fetchDeviceKeys: '&', + entityAlias: '=', + fetchEntityKeys: '&', datakeySettingsSchema: '=' }, link: linker diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js new file mode 100644 index 0000000000..97732b9ce5 --- /dev/null +++ b/ui/src/app/components/datasource-entity.directive.js @@ -0,0 +1,249 @@ +/* + * Copyright © 2016-2017 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 './datasource-entity.scss'; + +import 'md-color-picker'; +import tinycolor from 'tinycolor2'; +import $ from 'jquery'; +import thingsboardTypes from '../common/types.constant'; +import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller'; +import thingsboardTruncate from './truncate.filter'; + +/* eslint-disable import/no-unresolved, import/default */ + +import datasourceEntityTemplate from './datasource-entity.tpl.html'; +import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/* eslint-disable angular/angularelement */ + +export default angular.module('thingsboard.directives.datasourceEntity', [thingsboardTruncate, thingsboardTypes, thingsboardDatakeyConfigDialog]) + .directive('tbDatasourceEntity', DatasourceEntity) + .name; + +/*@ngInject*/ +function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $document, $mdColorPicker, $mdConstant, types) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(datasourceEntityTemplate); + element.html(template); + + scope.ngModelCtrl = ngModelCtrl; + scope.types = types; + + scope.selectedTimeseriesDataKey = null; + scope.timeseriesDataKeySearchText = null; + + scope.selectedAttributeDataKey = null; + scope.attributeDataKeySearchText = null; + + scope.updateValidity = function () { + if (ngModelCtrl.$viewValue) { + var value = ngModelCtrl.$viewValue; + var dataValid = angular.isDefined(value) && value != null; + ngModelCtrl.$setValidity('entityData', dataValid); + if (dataValid) { + ngModelCtrl.$setValidity('entityAlias', + angular.isDefined(value.entityAliasId) && + value.entityAliasId != null); + ngModelCtrl.$setValidity('entityKeys', + angular.isDefined(value.dataKeys) && + value.dataKeys != null && + value.dataKeys.length > 0); + } + } + }; + + scope.$watch('entityAlias', function () { + if (ngModelCtrl.$viewValue) { + if (scope.entityAlias) { + ngModelCtrl.$viewValue.entityAliasId = scope.entityAlias.id; + } else { + ngModelCtrl.$viewValue.entityAliasId = null; + } + scope.updateValidity(); + scope.selectedEntityAliasChange(); + } + }); + + scope.$watch('timeseriesDataKeys', function () { + if (ngModelCtrl.$viewValue) { + var dataKeys = []; + dataKeys = dataKeys.concat(scope.timeseriesDataKeys); + dataKeys = dataKeys.concat(scope.attributeDataKeys); + ngModelCtrl.$viewValue.dataKeys = dataKeys; + scope.updateValidity(); + } + }, true); + + scope.$watch('attributeDataKeys', function () { + if (ngModelCtrl.$viewValue) { + var dataKeys = []; + dataKeys = dataKeys.concat(scope.timeseriesDataKeys); + dataKeys = dataKeys.concat(scope.attributeDataKeys); + ngModelCtrl.$viewValue.dataKeys = dataKeys; + scope.updateValidity(); + } + }, true); + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + var entityAliasId = ngModelCtrl.$viewValue.entityAliasId; + if (scope.entityAliases[entityAliasId]) { + scope.entityAlias = {id: entityAliasId, alias: scope.entityAliases[entityAliasId].alias, + entityType: scope.entityAliases[entityAliasId].entityType, + entityId: scope.entityAliases[entityAliasId].entityId}; + } else { + scope.entityAlias = null; + } + var timeseriesDataKeys = []; + var attributeDataKeys = []; + for (var d in ngModelCtrl.$viewValue.dataKeys) { + var dataKey = ngModelCtrl.$viewValue.dataKeys[d]; + if (dataKey.type === types.dataKeyType.timeseries) { + timeseriesDataKeys.push(dataKey); + } else if (dataKey.type === types.dataKeyType.attribute) { + attributeDataKeys.push(dataKey); + } + } + scope.timeseriesDataKeys = timeseriesDataKeys; + scope.attributeDataKeys = attributeDataKeys; + } + }; + + scope.textIsNotEmpty = function(text) { + return (text && text != null && text.length > 0) ? true : false; + } + + scope.selectedEntityAliasChange = function () { + if (!scope.timeseriesDataKeySearchText || scope.timeseriesDataKeySearchText === '') { + scope.timeseriesDataKeySearchText = scope.timeseriesDataKeySearchText === '' ? null : ''; + } + if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') { + scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : ''; + } + }; + + scope.transformTimeseriesDataKeyChip = function (chip) { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries}); + }; + + scope.transformAttributeDataKeyChip = function (chip) { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute}); + }; + + scope.showColorPicker = function (event, dataKey) { + $mdColorPicker.show({ + value: dataKey.color, + defaultValue: '#fff', + random: tinycolor.random(), + clickOutsideToClose: false, + hasBackdrop: false, + skipHide: true, + preserveScope: false, + + mdColorAlphaChannel: true, + mdColorSpectrum: true, + mdColorSliders: true, + mdColorGenericPalette: false, + mdColorMaterialPalette: true, + mdColorHistory: false, + mdColorDefaultTab: 2, + + $event: event + + }).then(function (color) { + dataKey.color = color; + ngModelCtrl.$setDirty(); + }); + } + + scope.editDataKey = function (event, dataKey, index) { + + $mdDialog.show({ + controller: 'DatakeyConfigDialogController', + controllerAs: 'vm', + templateUrl: datakeyConfigDialogTemplate, + locals: { + dataKey: angular.copy(dataKey), + dataKeySettingsSchema: scope.datakeySettingsSchema, + entityAlias: scope.entityAlias, + entityAliases: scope.entityAliases + }, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: event, + skipHide: true, + onComplete: function () { + var w = angular.element($window); + w.triggerHandler('resize'); + } + }).then(function (dataKey) { + if (dataKey.type === types.dataKeyType.timeseries) { + scope.timeseriesDataKeys[index] = dataKey; + } else if (dataKey.type === types.dataKeyType.attribute) { + scope.attributeDataKeys[index] = dataKey; + } + ngModelCtrl.$setDirty(); + }, function () { + }); + }; + + scope.dataKeysSearch = function (searchText, type) { + if (scope.entityAlias) { + var deferred = $q.defer(); + scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type}) + .then(function (dataKeys) { + deferred.resolve(dataKeys); + }, function (e) { + deferred.reject(e); + }); + return deferred.promise; + } else { + return $q.when([]); + } + }; + + scope.createKey = function (event, chipsId) { + var chipsChild = $(chipsId, element)[0].firstElementChild; + var el = angular.element(chipsChild); + var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer(); + event.preventDefault(); + event.stopPropagation(); + el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim()); + el.scope().$mdChipsCtrl.resetChipBuffer(); + } + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + require: "^ngModel", + scope: { + widgetType: '=', + entityAliases: '=', + datakeySettingsSchema: '=', + generateDataKey: '&', + fetchEntityKeys: '&', + onCreateEntityAlias: '&' + }, + link: linker + }; +} + +/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/datasource-entity.scss b/ui/src/app/components/datasource-entity.scss new file mode 100644 index 0000000000..7a87fc700c --- /dev/null +++ b/ui/src/app/components/datasource-entity.scss @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2017 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-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete { + .tb-not-found { + display: block; + line-height: 1.5; + height: 48px; + .tb-no-entries { + line-height: 48px; + } + } + li { + height: auto !important; + white-space: normal !important; + } +} + +tb-datasource-entity { + @media (min-width: $layout-breakpoint-gt-sm) { + padding-left: 4px; + padding-right: 4px; + } + tb-entity-alias-select { + @media (min-width: $layout-breakpoint-gt-sm) { + width: 200px; + max-width: 200px; + } + } +} \ No newline at end of file diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html new file mode 100644 index 0000000000..97ee1b522a --- /dev/null +++ b/ui/src/app/components/datasource-entity.tpl.html @@ -0,0 +1,137 @@ + +
+ + +
+
+ + + {{item}} + +
+
+ entity.no-keys-found +
+
+ entity.no-key-matching + + entity.create-new-key + +
+
+
+
+ +
+
+
+
+
+
+ {{$chip.label}} +
+
:
+
+ {{$chip.name}} + f({{$chip.name}}) +
+
+ + edit + +
+
+
+ + + {{item}} + +
+
+ entity.no-keys-found +
+
+ entity.no-key-matching + + entity.create-new-key + +
+
+
+
+ +
+
+
+
+
+
+ {{$chip.label}} +
+
:
+
+ {{$chip.name}} + f({{$chip.name}}) +
+
+ + edit + +
+
+
+
+ +
+
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js index 5d3c2e7b48..0f66dcaa29 100644 --- a/ui/src/app/components/datasource-func.directive.js +++ b/ui/src/app/components/datasource-func.directive.js @@ -120,8 +120,8 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, locals: { dataKey: angular.copy(dataKey), dataKeySettingsSchema: scope.datakeySettingsSchema, - deviceAlias: null, - deviceAliases: null + entityAlias: null, + entityAliases: null }, parent: angular.element($document[0].body), fullscreen: true, diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js index 82e8cdd7a5..2c06a381ee 100644 --- a/ui/src/app/components/datasource.directive.js +++ b/ui/src/app/components/datasource.directive.js @@ -17,7 +17,7 @@ import './datasource.scss'; import thingsboardTypes from '../common/types.constant'; import thingsboardDatasourceFunc from './datasource-func.directive' -import thingsboardDatasourceDevice from './datasource-device.directive'; +import thingsboardDatasourceEntity from './datasource-entity.directive'; /* eslint-disable import/no-unresolved, import/default */ @@ -25,7 +25,7 @@ import datasourceTemplate from './datasource.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ -export default angular.module('thingsboard.directives.datasource', [thingsboardTypes, thingsboardDatasourceFunc, thingsboardDatasourceDevice]) +export default angular.module('thingsboard.directives.datasource', [thingsboardTypes, thingsboardDatasourceFunc, thingsboardDatasourceEntity]) .directive('tbDatasource', Datasource) .name; @@ -42,7 +42,7 @@ function Datasource($compile, $templateCache, types) { if (scope.functionsOnly) { scope.datasourceTypes = [types.datasourceType.function]; } else{ - scope.datasourceTypes = [types.datasourceType.device, types.datasourceType.function]; + scope.datasourceTypes = [types.datasourceType.entity, types.datasourceType.function]; } scope.updateView = function () { @@ -76,13 +76,13 @@ function Datasource($compile, $templateCache, types) { restrict: "E", require: "^ngModel", scope: { - deviceAliases: '=', + entityAliases: '=', widgetType: '=', functionsOnly: '=', datakeySettingsSchema: '=', generateDataKey: '&', - fetchDeviceKeys: '&', - onCreateDeviceAlias: '&' + fetchEntityKeys: '&', + onCreateEntityAlias: '&' }, link: linker }; diff --git a/ui/src/app/components/datasource.scss b/ui/src/app/components/datasource.scss index 02bbbffe20..f188a04486 100644 --- a/ui/src/app/components/datasource.scss +++ b/ui/src/app/components/datasource.scss @@ -14,7 +14,7 @@ * limitations under the License. */ .tb-datasource { - #device-autocomplete { + #entity-autocomplete { height: 30px; margin-top: 18px; md-autocomplete-wrap { diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html index cbd638c9ee..88ff247fa0 100644 --- a/ui/src/app/components/datasource.tpl.html +++ b/ui/src/app/components/datasource.tpl.html @@ -31,16 +31,16 @@ ng-required="model.type === types.datasourceType.function" generate-data-key="generateDataKey({chip: chip, type: type})"> - - + fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})" + on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"> + diff --git a/ui/src/app/components/entity-alias-select.directive.js b/ui/src/app/components/entity-alias-select.directive.js new file mode 100644 index 0000000000..204b83fe6a --- /dev/null +++ b/ui/src/app/components/entity-alias-select.directive.js @@ -0,0 +1,151 @@ +/* + * Copyright © 2016-2017 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 $ from 'jquery'; + +import './entity-alias-select.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import entityAliasSelectTemplate from './entity-alias-select.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + + +/* eslint-disable angular/angularelement */ + +export default angular.module('thingsboard.directives.entityAliasSelect', []) + .directive('tbEntityAliasSelect', EntityAliasSelect) + .name; + +/*@ngInject*/ +function EntityAliasSelect($compile, $templateCache, $mdConstant) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(entityAliasSelectTemplate); + element.html(template); + + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; + + scope.ngModelCtrl = ngModelCtrl; + scope.entityAliasList = []; + scope.entityAlias = null; + + scope.updateValidity = function () { + var value = ngModelCtrl.$viewValue; + var valid = angular.isDefined(value) && value != null || !scope.tbRequired; + ngModelCtrl.$setValidity('entityAlias', valid); + }; + + scope.$watch('entityAliases', function () { + scope.entityAliasList = []; + for (var aliasId in scope.entityAliases) { + if (scope.allowedEntityTypes) { + if (scope.allowedEntityTypes.indexOf(scope.entityAliases[aliasId].entityType) === -1) { + continue; + } + } + var entityAlias = {id: aliasId, alias: scope.entityAliases[aliasId].alias, + entityType: scope.entityAliases[aliasId].entityType, entityId: scope.entityAliases[aliasId].entityId}; + scope.entityAliasList.push(entityAlias); + } + }, true); + + scope.$watch('entityAlias', function () { + scope.updateView(); + }); + + scope.entityAliasSearch = function (entityAliasSearchText) { + return entityAliasSearchText ? scope.entityAliasList.filter( + scope.createFilterForEntityAlias(entityAliasSearchText)) : scope.entityAliasList; + }; + + scope.createFilterForEntityAlias = function (query) { + var lowercaseQuery = angular.lowercase(query); + return function filterFn(entityAlias) { + return (angular.lowercase(entityAlias.alias).indexOf(lowercaseQuery) === 0); + }; + }; + + scope.updateView = function () { + ngModelCtrl.$setViewValue(scope.entityAlias); + scope.updateValidity(); + } + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + scope.entityAlias = ngModelCtrl.$viewValue; + } + } + + scope.textIsNotEmpty = function(text) { + return (text && text != null && text.length > 0) ? true : false; + } + + scope.entityAliasEnter = function($event) { + if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { + $event.preventDefault(); + if (!scope.entityAlias) { + var found = scope.entityAliasSearch(scope.entityAliasSearchText); + found = found.length > 0; + if (!found) { + scope.createEntityAlias($event, scope.entityAliasSearchText); + } + } + } + } + + scope.createEntityAlias = function (event, alias) { + var autoChild = $('#entity-autocomplete', element)[0].firstElementChild; + var el = angular.element(autoChild); + el.scope().$mdAutocompleteCtrl.hidden = true; + el.scope().$mdAutocompleteCtrl.hasNotFound = false; + event.preventDefault(); + var promise = scope.onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: scope.allowedEntityTypes}); + if (promise) { + promise.then( + function success(newAlias) { + el.scope().$mdAutocompleteCtrl.hasNotFound = true; + if (newAlias) { + scope.entityAliasList.push(newAlias); + scope.entityAlias = newAlias; + } + }, + function fail() { + el.scope().$mdAutocompleteCtrl.hasNotFound = true; + } + ); + } else { + el.scope().$mdAutocompleteCtrl.hasNotFound = true; + } + }; + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + tbRequired: '=?', + entityAliases: '=', + allowedEntityTypes: '=?', + onCreateEntityAlias: '&' + } + }; +} + +/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/entity-alias-select.scss b/ui/src/app/components/entity-alias-select.scss new file mode 100644 index 0000000000..c23982f914 --- /dev/null +++ b/ui/src/app/components/entity-alias-select.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2017 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. + */ +.tb-entity-alias-autocomplete { + .tb-not-found { + display: block; + line-height: 1.5; + height: 48px; + .tb-no-entries { + line-height: 48px; + } + } + li { + height: auto !important; + white-space: normal !important; + } +} diff --git a/ui/src/app/components/entity-alias-select.tpl.html b/ui/src/app/components/entity-alias-select.tpl.html new file mode 100644 index 0000000000..37ca52a4db --- /dev/null +++ b/ui/src/app/components/entity-alias-select.tpl.html @@ -0,0 +1,53 @@ + +
+ + + {{item.alias}} + + +
+
+ entity.no-aliases-found +
+
+ entity.no-alias-matching + + entity.create-new-alias + +
+
+
+
+ +
diff --git a/ui/src/app/components/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js new file mode 100644 index 0000000000..f93ded25ed --- /dev/null +++ b/ui/src/app/components/related-entity-autocomplete.directive.js @@ -0,0 +1,128 @@ +/* + * Copyright © 2016-2017 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 './related-entity-autocomplete.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import relatedEntityAutocompleteTemplate from './related-entity-autocomplete.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + + +export default angular.module('thingsboard.directives.relatedEntityAutocomplete', []) + .directive('tbRelatedEntityAutocomplete', RelatedEntityAutocomplete) + .name; + +/*@ngInject*/ +function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entityService) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(relatedEntityAutocompleteTemplate); + element.html(template); + + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; + scope.entity = null; + scope.entitySearchText = ''; + + scope.allEntities = null; + + scope.fetchEntities = function(searchText) { + var deferred = $q.defer(); + if (!scope.allEntities) { + entityService.getRelatedEntities(scope.rootEntityId, scope.entityType, scope.entitySubtypes, -1, []).then( + function success(entities) { + if (scope.excludeEntityId) { + var result = $filter('filter')(entities, {id: {id: scope.excludeEntityId.id} }, true); + result = $filter('filter')(result, {id: {entityType: scope.excludeEntityId.entityType} }, true); + if (result && result.length) { + var excludeEntity = result[0]; + var index = entities.indexOf(excludeEntity); + if (index > -1) { + entities.splice(index, 1); + } + } + } + scope.allEntities = entities; + filterEntities(searchText, deferred); + }, + function fail() { + deferred.reject(); + } + ); + } else { + filterEntities(searchText, deferred); + } + return deferred.promise; + } + + function filterEntities(searchText, deferred) { + var result = $filter('filter')(scope.allEntities, {name: searchText}); + deferred.resolve(result); + } + + scope.entitySearchTextChanged = function() { + } + + scope.updateView = function () { + if (!scope.disabled) { + ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id : null); + } + } + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + entityService.getRelatedEntity(ngModelCtrl.$viewValue).then( + function success(entity) { + scope.entity = entity; + }, + function fail() { + scope.entity = null; + } + ); + } else { + scope.entity = null; + } + } + + scope.$watch('entity', function () { + scope.updateView(); + }); + + scope.$watch('disabled', function () { + scope.updateView(); + }); + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + rootEntityId: '=', + entityType: '=', + entitySubtypes: '=', + excludeEntityId: '=?', + theForm: '=?', + tbRequired: '=?', + disabled:'=ngDisabled', + placeholderText: '@', + notFoundText: '@', + requiredText: '@' + } + }; +} diff --git a/ui/src/app/components/related-entity-autocomplete.scss b/ui/src/app/components/related-entity-autocomplete.scss new file mode 100644 index 0000000000..32df94f746 --- /dev/null +++ b/ui/src/app/components/related-entity-autocomplete.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2017 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. + */ +.tb-related-entity-autocomplete { + .tb-not-found { + display: block; + line-height: 1.5; + height: 48px; + } + .tb-entity-item { + display: block; + height: 48px; + } + li { + height: auto !important; + white-space: normal !important; + } +} diff --git a/ui/src/app/components/related-entity-autocomplete.tpl.html b/ui/src/app/components/related-entity-autocomplete.tpl.html new file mode 100644 index 0000000000..a5b50e9147 --- /dev/null +++ b/ui/src/app/components/related-entity-autocomplete.tpl.html @@ -0,0 +1,43 @@ + + + +
+ {{item.name}} +
+
+ +
+ {{ notFoundText | translate:{entity: entitySearchText} }} +
+
+
+
{{ requiredText | translate }}
+
+
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index bcffa721cc..122889d4c1 100644 --- a/ui/src/app/components/widget-config.directive.js +++ b/ui/src/app/components/widget-config.directive.js @@ -16,7 +16,7 @@ import jsonSchemaDefaults from 'json-schema-defaults'; import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; -import thingsboardDeviceAliasSelect from './device-alias-select.directive'; +import thingsboardEntityAliasSelect from './entity-alias-select.directive'; import thingsboardDatasource from './datasource.directive'; import thingsboardTimewindow from './timewindow.directive'; import thingsboardLegendConfig from './legend-config.directive'; @@ -34,7 +34,7 @@ import widgetConfigTemplate from './widget-config.tpl.html'; export default angular.module('thingsboard.directives.widgetConfig', [thingsboardTypes, thingsboardUtils, thingsboardJsonForm, - thingsboardDeviceAliasSelect, + thingsboardEntityAliasSelect, thingsboardDatasource, thingsboardTimewindow, thingsboardLegendConfig, @@ -89,58 +89,68 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { - scope.selectedTab = 0; - scope.title = ngModelCtrl.$viewValue.title; - scope.showTitle = ngModelCtrl.$viewValue.showTitle; - scope.dropShadow = angular.isDefined(ngModelCtrl.$viewValue.dropShadow) ? ngModelCtrl.$viewValue.dropShadow : true; - scope.enableFullscreen = angular.isDefined(ngModelCtrl.$viewValue.enableFullscreen) ? ngModelCtrl.$viewValue.enableFullscreen : true; - scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor; - scope.color = ngModelCtrl.$viewValue.color; - scope.padding = ngModelCtrl.$viewValue.padding; - scope.titleStyle = - angular.toJson(angular.isDefined(ngModelCtrl.$viewValue.titleStyle) ? ngModelCtrl.$viewValue.titleStyle : { - fontSize: '16px', - fontWeight: 400 - }, true); - scope.mobileOrder = ngModelCtrl.$viewValue.mobileOrder; - scope.mobileHeight = ngModelCtrl.$viewValue.mobileHeight; - scope.units = ngModelCtrl.$viewValue.units; - scope.decimals = ngModelCtrl.$viewValue.decimals; - scope.useDashboardTimewindow = angular.isDefined(ngModelCtrl.$viewValue.useDashboardTimewindow) ? - ngModelCtrl.$viewValue.useDashboardTimewindow : true; - scope.timewindow = ngModelCtrl.$viewValue.timewindow; - scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ? - ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value; - scope.legendConfig = ngModelCtrl.$viewValue.legendConfig; - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value - && scope.isDataEnabled) { - if (scope.datasources) { - scope.datasources.splice(0, scope.datasources.length); - } else { - scope.datasources = []; - } - if (ngModelCtrl.$viewValue.datasources) { - for (var i in ngModelCtrl.$viewValue.datasources) { - scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); + var config = ngModelCtrl.$viewValue.config; + var layout = ngModelCtrl.$viewValue.layout; + if (config) { + scope.selectedTab = 0; + scope.title = config.title; + scope.showTitle = config.showTitle; + scope.dropShadow = angular.isDefined(config.dropShadow) ? config.dropShadow : true; + scope.enableFullscreen = angular.isDefined(config.enableFullscreen) ? config.enableFullscreen : true; + scope.backgroundColor = config.backgroundColor; + scope.color = config.color; + scope.padding = config.padding; + scope.titleStyle = + angular.toJson(angular.isDefined(config.titleStyle) ? config.titleStyle : { + fontSize: '16px', + fontWeight: 400 + }, true); + scope.units = config.units; + scope.decimals = config.decimals; + scope.useDashboardTimewindow = angular.isDefined(config.useDashboardTimewindow) ? + config.useDashboardTimewindow : true; + scope.timewindow = config.timewindow; + scope.showLegend = angular.isDefined(config.showLegend) ? + config.showLegend : scope.widgetType === types.widgetType.timeseries.value; + scope.legendConfig = config.legendConfig; + if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value + && scope.isDataEnabled) { + if (scope.datasources) { + scope.datasources.splice(0, scope.datasources.length); + } else { + scope.datasources = []; } - } - } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) { - var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0]; - if (scope.deviceAliases[aliasId]) { - scope.targetDeviceAlias.value = {id: aliasId, alias: scope.deviceAliases[aliasId].alias, - deviceId: scope.deviceAliases[aliasId].deviceId}; + if (config.datasources) { + for (var i in config.datasources) { + scope.datasources.push({value: config.datasources[i]}); + } + } + } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { + if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) { + var aliasId = config.targetDeviceAliasIds[0]; + if (scope.entityAliases[aliasId]) { + scope.targetDeviceAlias.value = { + id: aliasId, + alias: scope.entityAliases[aliasId].alias, + entityType: scope.entityAliases[aliasId].entityType, + entityId: scope.entityAliases[aliasId].entityId + }; + } else { + scope.targetDeviceAlias.value = null; + } } else { scope.targetDeviceAlias.value = null; } - } else { - scope.targetDeviceAlias.value = null; } - } - scope.settings = ngModelCtrl.$viewValue.settings; + scope.settings = config.settings; - scope.updateSchemaForm(); + scope.updateSchemaForm(); + } + if (layout) { + scope.mobileOrder = layout.mobileOrder; + scope.mobileHeight = layout.mobileHeight; + } } }; @@ -163,19 +173,22 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.updateValidity = function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - var valid; - if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { - valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; - ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); - } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { - valid = value && value.datasources && value.datasources.length > 0; - ngModelCtrl.$setValidity('datasources', valid); - } - try { - angular.fromJson(scope.titleStyle); - ngModelCtrl.$setValidity('titleStyle', true); - } catch (e) { - ngModelCtrl.$setValidity('titleStyle', false); + var config = value.config; + if (config) { + var valid; + if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { + valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0; + ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); + } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { + valid = config && config.datasources && config.datasources.length > 0; + ngModelCtrl.$setValidity('datasources', valid); + } + try { + angular.fromJson(scope.titleStyle); + ngModelCtrl.$setValidity('titleStyle', true); + } catch (e) { + ngModelCtrl.$setValidity('titleStyle', false); + } } } }; @@ -184,24 +197,30 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti 'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.title = scope.title; - value.showTitle = scope.showTitle; - value.dropShadow = scope.dropShadow; - value.enableFullscreen = scope.enableFullscreen; - value.backgroundColor = scope.backgroundColor; - value.color = scope.color; - value.padding = scope.padding; - try { - value.titleStyle = angular.fromJson(scope.titleStyle); - } catch (e) { - value.titleStyle = {}; + if (value.config) { + var config = value.config; + config.title = scope.title; + config.showTitle = scope.showTitle; + config.dropShadow = scope.dropShadow; + config.enableFullscreen = scope.enableFullscreen; + config.backgroundColor = scope.backgroundColor; + config.color = scope.color; + config.padding = scope.padding; + try { + config.titleStyle = angular.fromJson(scope.titleStyle); + } catch (e) { + config.titleStyle = {}; + } + config.units = scope.units; + config.decimals = scope.decimals; + config.useDashboardTimewindow = scope.useDashboardTimewindow; + config.showLegend = scope.showLegend; + } + if (value.layout) { + var layout = value.layout; + layout.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined; + layout.mobileHeight = scope.mobileHeight; } - value.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined; - value.mobileHeight = scope.mobileHeight; - value.units = scope.units; - value.decimals = scope.decimals; - value.useDashboardTimewindow = scope.useDashboardTimewindow; - value.showLegend = scope.showLegend; ngModelCtrl.$setViewValue(value); scope.updateValidity(); } @@ -210,39 +229,46 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.$watch('currentSettings', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.settings = scope.currentSettings; - ngModelCtrl.$setViewValue(value); + if (value.config) { + value.config.settings = scope.currentSettings; + ngModelCtrl.$setViewValue(value); + } } }, true); scope.$watch('timewindow', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.timewindow = scope.timewindow; - ngModelCtrl.$setViewValue(value); + if (value.config) { + value.config.timewindow = scope.timewindow; + ngModelCtrl.$setViewValue(value); + } } }, true); scope.$watch('legendConfig', function () { if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; - value.legendConfig = scope.legendConfig; - ngModelCtrl.$setViewValue(value); + if (value.config) { + value.config.legendConfig = scope.legendConfig; + ngModelCtrl.$setViewValue(value); + } } }, true); scope.$watch('datasources', function () { - if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; - if (value.datasources) { - value.datasources.splice(0, value.datasources.length); + var config = value.config; + if (config.datasources) { + config.datasources.splice(0, config.datasources.length); } else { - value.datasources = []; + config.datasources = []; } if (scope.datasources) { for (var i in scope.datasources) { - value.datasources.push(scope.datasources[i].value); + config.datasources.push(scope.datasources[i].value); } } ngModelCtrl.$setViewValue(value); @@ -251,12 +277,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti }, true); scope.$watch('targetDeviceAlias.value', function () { - if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; + var config = value.config; if (scope.targetDeviceAlias.value) { - value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; + config.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; } else { - value.targetDeviceAliasIds = []; + config.targetDeviceAliasIds = []; } ngModelCtrl.$setViewValue(value); scope.updateValidity(); @@ -269,7 +296,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti newDatasource = angular.copy(utils.getDefaultDatasource(scope.datakeySettingsSchema.schema)); newDatasource.dataKeys = [scope.generateDataKey('Sin', types.dataKeyType.function)]; } else { - newDatasource = { type: types.datasourceType.device, + newDatasource = { type: types.datasourceType.entity, dataKeys: [] }; } @@ -368,10 +395,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti widgetType: '=', widgetSettingsSchema: '=', datakeySettingsSchema: '=', - deviceAliases: '=', + entityAliases: '=', functionsOnly: '=', - fetchDeviceKeys: '&', - onCreateDeviceAlias: '&', + fetchEntityKeys: '&', + onCreateEntityAlias: '&', theForm: '=' }, link: linker diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html index e7c03e3f25..808e07b487 100644 --- a/ui/src/app/components/widget-config.tpl.html +++ b/ui/src/app/components/widget-config.tpl.html @@ -60,12 +60,12 @@ style="padding: 0 0 0 10px; margin: 5px;"> + fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})" + on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"> - - + on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: allowedEntityTypes})"> + diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index 7c7caa9e2b..6fc4bba646 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -21,8 +21,8 @@ import Subscription from '../api/subscription'; /*@ngInject*/ export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService, - datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, - dashboardTimewindowApi, widget, aliasesInfo, widgetType) { + datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, + dashboardTimewindowApi, widget, aliasesInfo, stateController, widgetType) { var vm = this; @@ -83,10 +83,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q // type: "timeseries" or "latest" or "rpc" - /* devicesSubscriptionInfo = [ + /* subscriptionInfo = [ { - deviceId: "" - deviceName: "" + entityType: "" + entityId: "" + entityName: "" timeseries: [{ name: "", label: "" }, ..] attributes: [{ name: "", label: "" }, ..] } @@ -130,7 +131,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q }, utils: { formatValue: formatValue - } + }, + stateController: stateController }; var subscriptionContext = { @@ -231,7 +233,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } } - utils.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then( + entityService.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then( function (datasources) { options.datasources = datasources; var subscription = createSubscription(options, subscribe); @@ -341,6 +343,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q }, onRpcSuccess: function(subscription) { $scope.executingRpcRequest = subscription.executingRpcRequest; + $scope.rpcErrorText = subscription.rpcErrorText; + $scope.rpcRejection = subscription.rpcRejection; }, onRpcFailed: function(subscription) { $scope.executingRpcRequest = subscription.executingRpcRequest; @@ -394,6 +398,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q onMobileModeChanged(newIsMobile); }); + $scope.$on('entityAliasListChanged', function (event, aliasesInfo) { + subscriptionContext.aliasesInfo = aliasesInfo; + for (var id in widgetContext.subscriptions) { + var subscription = widgetContext.subscriptions[id]; + subscription.onAliasesChanged(); + } + }); + $scope.$on("$destroy", function () { removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef onDestroy(); diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html index a2165712e6..3facd0f7a8 100644 --- a/ui/src/app/customer/customer-fieldset.tpl.html +++ b/ui/src/app/customer/customer-fieldset.tpl.html @@ -16,10 +16,21 @@ --> {{ 'customer.manage-users' | translate }} +{{ 'customer.manage-assets' | translate }} {{ 'customer.manage-devices' | translate }} {{ 'customer.manage-dashboards' | translate }} {{ 'customer.delete' | translate }} +
+ + + customer.copyId + +
+
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js index 4182588555..5175052050 100644 --- a/ui/src/app/customer/customer.controller.js +++ b/ui/src/app/customer/customer.controller.js @@ -21,7 +21,7 @@ import customerCard from './customer-card.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function CustomerController(customerService, $state, $stateParams, $translate) { +export default function CustomerController(customerService, $state, $stateParams, $translate, types) { var customerActionsList = [ { @@ -35,6 +35,20 @@ export default function CustomerController(customerService, $state, $stateParams return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); } }, + { + onAction: function ($event, item) { + openCustomerAssets($event, item); + }, + name: function() { return $translate.instant('asset.assets') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-assets') + } else { + return $translate.instant('customer.manage-customer-assets') + } + }, + icon: "domain" + }, { onAction: function ($event, item) { openCustomerDevices($event, item); @@ -78,6 +92,8 @@ export default function CustomerController(customerService, $state, $stateParams var vm = this; + vm.types = types; + vm.customerGridConfig = { refreshParamsFunc: null, @@ -128,6 +144,7 @@ export default function CustomerController(customerService, $state, $stateParams } vm.openCustomerUsers = openCustomerUsers; + vm.openCustomerAssets = openCustomerAssets; vm.openCustomerDevices = openCustomerDevices; vm.openCustomerDashboards = openCustomerDashboards; @@ -178,6 +195,13 @@ export default function CustomerController(customerService, $state, $stateParams $state.go('home.customers.users', {customerId: customer.id.id}); } + function openCustomerAssets($event, customer) { + if ($event) { + $event.stopPropagation(); + } + $state.go('home.customers.assets', {customerId: customer.id.id}); + } + function openCustomerDevices($event, customer) { if ($event) { $event.stopPropagation(); diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js index 4a4e09ca0b..e48934b1e7 100644 --- a/ui/src/app/customer/customer.directive.js +++ b/ui/src/app/customer/customer.directive.js @@ -20,13 +20,17 @@ import customerFieldsetTemplate from './customer-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function CustomerDirective($compile, $templateCache) { +export default function CustomerDirective($compile, $templateCache, $translate, toast) { var linker = function (scope, element) { var template = $templateCache.get(customerFieldsetTemplate); element.html(template); scope.isPublic = false; + scope.onCustomerIdCopied = function() { + toast.showSuccess($translate.instant('customer.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + scope.$watch('customer', function(newVal) { if (newVal) { if (scope.customer.additionalInfo) { @@ -48,6 +52,7 @@ export default function CustomerDirective($compile, $templateCache) { isEdit: '=', theForm: '=', onManageUsers: '&', + onManageAssets: '&', onManageDevices: '&', onManageDashboards: '&', onDeleteCustomer: '&' diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html index 7d706be889..92219844ee 100644 --- a/ui/src/app/customer/customers.tpl.html +++ b/ui/src/app/customer/customers.tpl.html @@ -19,11 +19,41 @@
- + + + + + + + + + + + + + + + + + diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js index e9ceaadfae..2f23cf9f38 100644 --- a/ui/src/app/dashboard/add-widget.controller.js +++ b/ui/src/app/dashboard/add-widget.controller.js @@ -15,12 +15,12 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import deviceAliasesTemplate from './device-aliases.tpl.html'; +import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) { +export default function AddWidgetController($scope, widgetService, entityService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) { var vm = this; @@ -34,10 +34,16 @@ export default function AddWidgetController($scope, widgetService, deviceService vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType; vm.add = add; vm.cancel = cancel; - vm.fetchDeviceKeys = fetchDeviceKeys; - vm.createDeviceAlias = createDeviceAlias; + vm.fetchEntityKeys = fetchEntityKeys; + vm.createEntityAlias = createEntityAlias; - vm.widgetConfig = vm.widget.config; + vm.widgetConfig = { + config: vm.widget.config, + layout: {} + }; + + vm.widgetConfig.layout.mobileOrder = vm.widget.config.mobileOrder; + vm.widgetConfig.layout.mobileHeight = vm.widget.config.mobileHeight; var settingsSchema = vm.widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; var dataKeySettingsSchema = vm.widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; @@ -85,50 +91,53 @@ export default function AddWidgetController($scope, widgetService, deviceService function add () { if ($scope.theForm.$valid) { $scope.theForm.$setPristine(); - vm.widget.config = vm.widgetConfig; + vm.widget.config = vm.widgetConfig.config; + vm.widget.config.mobileOrder = vm.widgetConfig.layout.mobileOrder; + vm.widget.config.mobileHeight = vm.widgetConfig.layout.mobileHeight; $mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo}); } } - function fetchDeviceKeys (deviceAliasId, query, type) { - var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId]; - if (deviceAlias && deviceAlias.deviceId) { - return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type); + function fetchEntityKeys (entityAliasId, query, type) { + var entityAlias = vm.aliasesInfo.entityAliases[entityAliasId]; + if (entityAlias && entityAlias.entityId) { + return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type); } else { return $q.when([]); } } - function createDeviceAlias (event, alias) { + function createEntityAlias (event, alias, allowedEntityTypes) { var deferred = $q.defer(); - var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null}; + var singleEntityAlias = {id: null, alias: alias, entityType: types.entityType.device, entityFilter: null}; $mdDialog.show({ - controller: 'DeviceAliasesController', + controller: 'EntityAliasesController', controllerAs: 'vm', - templateUrl: deviceAliasesTemplate, + templateUrl: entityAliasesTemplate, locals: { config: { - deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases), + entityAliases: angular.copy(vm.dashboard.configuration.entityAliases), widgets: null, - isSingleDeviceAlias: true, - singleDeviceAlias: singleDeviceAlias + isSingleEntityAlias: true, + singleEntityAlias: singleEntityAlias, + allowedEntityTypes: allowedEntityTypes } }, parent: angular.element($document[0].body), fullscreen: true, skipHide: true, targetEvent: event - }).then(function (singleDeviceAlias) { - vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] = - { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter }; - deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then( + }).then(function (singleEntityAlias) { + vm.dashboard.configuration.entityAliases[singleEntityAlias.id] = + { alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter }; + entityService.processEntityAliases(vm.dashboard.configuration.entityAliases).then( function(resolution) { if (!resolution.error) { vm.aliasesInfo = resolution.aliasesInfo; } - deferred.resolve(singleDeviceAlias); + deferred.resolve(singleEntityAlias); } ); }, function () { diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html index 4e6db50b5f..870e20346a 100644 --- a/ui/src/app/dashboard/add-widget.tpl.html +++ b/ui/src/app/dashboard/add-widget.tpl.html @@ -37,10 +37,10 @@ ng-model="vm.widgetConfig" widget-settings-schema="vm.settingsSchema" datakey-settings-schema="vm.dataKeySettingsSchema" - device-aliases="vm.aliasesInfo.deviceAliases" + entity-aliases="vm.aliasesInfo.entityAliases" functions-only="vm.functionsOnly" - fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)" - on-create-device-alias="vm.createDeviceAlias(event, alias)" + fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)" + on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)" the-form="theForm">
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js index 06c64eb757..dbe4cbe992 100644 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ b/ui/src/app/dashboard/dashboard-settings.controller.js @@ -16,7 +16,7 @@ import './dashboard-settings.scss'; /*@ngInject*/ -export default function DashboardSettingsController($scope, $mdDialog, gridSettings) { +export default function DashboardSettingsController($scope, $mdDialog, statesControllerService, settings, gridSettings) { var vm = this; @@ -25,32 +25,49 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti vm.imageAdded = imageAdded; vm.clearImage = clearImage; - vm.gridSettings = gridSettings || {}; + vm.settings = settings; + vm.gridSettings = gridSettings; + vm.stateControllers = statesControllerService.getStateControllers(); - if (angular.isUndefined(vm.gridSettings.showTitle)) { - vm.gridSettings.showTitle = true; - } + if (vm.settings) { + if (angular.isUndefined(vm.settings.stateControllerId)) { + vm.settings.stateControllerId = 'default'; + } - if (angular.isUndefined(vm.gridSettings.showDevicesSelect)) { - vm.gridSettings.showDevicesSelect = true; - } + if (angular.isUndefined(vm.settings.showTitle)) { + vm.settings.showTitle = true; + } - if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) { - vm.gridSettings.showDashboardTimewindow = true; - } + if (angular.isUndefined(vm.settings.titleColor)) { + vm.settings.titleColor = 'rgba(0,0,0,0.870588)'; + } - if (angular.isUndefined(vm.gridSettings.showDashboardExport)) { - vm.gridSettings.showDashboardExport = true; - } + if (angular.isUndefined(vm.settings.showDashboardsSelect)) { + vm.settings.showDashboardsSelect = true; + } - vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; - vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)'; - vm.gridSettings.columns = vm.gridSettings.columns || 24; - vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; - vm.hMargin = vm.gridSettings.margins[0]; - vm.vMargin = vm.gridSettings.margins[1]; + if (angular.isUndefined(vm.settings.showEntitiesSelect)) { + vm.settings.showEntitiesSelect = true; + } - vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; + if (angular.isUndefined(vm.settings.showDashboardTimewindow)) { + vm.settings.showDashboardTimewindow = true; + } + + if (angular.isUndefined(vm.settings.showDashboardExport)) { + vm.settings.showDashboardExport = true; + } + } + + if (vm.gridSettings) { + vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; + vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)'; + vm.gridSettings.columns = vm.gridSettings.columns || 24; + vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; + vm.hMargin = vm.gridSettings.margins[0]; + vm.vMargin = vm.gridSettings.margins[1]; + vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%'; + } function cancel() { $mdDialog.cancel(); @@ -76,7 +93,14 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti function save() { $scope.theForm.$setPristine(); - vm.gridSettings.margins = [vm.hMargin, vm.vMargin]; - $mdDialog.hide(vm.gridSettings); + if (vm.gridSettings) { + vm.gridSettings.margins = [vm.hMargin, vm.vMargin]; + } + $mdDialog.hide( + { + settings: vm.settings, + gridSettings: vm.gridSettings + } + ); } } diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html index ec6f28b369..88fc66d651 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -19,7 +19,7 @@
-

dashboard.settings

+

{{vm.settings ? 'dashboard.settings' : 'layout.settings'}}

@@ -31,15 +31,53 @@
-
- {{ 'dashboard.display-title' | translate }} - +
+ + + + + {{stateControllerId}} + + + +
+ {{ 'dashboard.display-title' | translate }} + +
+
+
+ {{ 'dashboard.display-dashboards-selection' | translate }} + + {{ 'dashboard.display-entities-selection' | translate }} + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + {{ 'dashboard.display-dashboard-export' | translate }} + +
+
+
-
-
- {{ 'dashboard.display-device-selection' | translate }} - - {{ 'dashboard.display-dashboard-timewindow' | translate }} - - {{ 'dashboard.display-dashboard-export' | translate }} - -
- - - -
-
dashboard.columns-count-required
-
dashboard.min-columns-count-message
-
dashboard.max-columns-count-message
-
-
- dashboard.widgets-margins -
- - - -
-
dashboard.horizontal-margin-required
-
dashboard.min-horizontal-margin-message
-
dashboard.max-horizontal-margin-message
-
-
- - - -
-
dashboard.vertical-margin-required
-
dashboard.min-vertical-margin-message
-
dashboard.max-vertical-margin-message
+ + + +
+
dashboard.columns-count-required
+
dashboard.min-columns-count-message
+
dashboard.max-columns-count-message
-
-
-
- -
-
-
dashboard.no-image
- -
-
- - - {{ 'action.remove' | translate }} - - - close - - -
-
- - + dashboard.widgets-margins +
+ + + +
+
dashboard.horizontal-margin-required
+
dashboard.min-horizontal-margin-message
+
dashboard.max-horizontal-margin-message
+
+
+ + + +
+
dashboard.vertical-margin-required
+
dashboard.min-vertical-margin-message
+
dashboard.max-vertical-margin-message
+
+
+
+
+
+ +
+
+
dashboard.no-image
+ +
+
+ + + {{ 'action.remove' | translate }} + + + close + + +
+
+ + +
+ + + + Fit width + Fit height + Cover + Contain + Original size + +
- - - - Fit width - Fit height - Cover - Contain - Original size - -
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index 9121f6aed8..e18ebb8859 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -15,23 +15,28 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import deviceAliasesTemplate from './device-aliases.tpl.html'; -import dashboardBackgroundTemplate from './dashboard-settings.tpl.html'; +import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; +import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; +import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; +import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; import addWidgetTemplate from './add-widget.tpl.html'; +import selectTargetLayoutTemplate from './layouts/select-target-layout.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function DashboardController(types, widgetService, userService, - dashboardService, timeService, deviceService, itembuffer, importExport, hotkeys, $window, $rootScope, - $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { +export default function DashboardController(types, dashboardUtils, widgetService, userService, + dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope, + $scope, $element, $state, $stateParams, $mdDialog, $mdMedia, $timeout, $document, $q, $translate, $filter) { var vm = this; vm.user = userService.getCurrentUser(); vm.dashboard = null; vm.editingWidget = null; + vm.editingWidgetLayout = null; vm.editingWidgetOriginal = null; + vm.editingWidgetLayoutOriginal = null; vm.editingWidgetSubtitle = null; vm.forceDashboardMobileMode = false; vm.isAddingWidget = false; @@ -43,8 +48,6 @@ export default function DashboardController(types, widgetService, userService, vm.staticWidgetTypes = []; vm.widgetEditMode = $state.$current.data.widgetEditMode; vm.iframeMode = $rootScope.iframeMode; - vm.widgets = []; - vm.dashboardInitComplete = false; vm.isToolbarOpened = false; @@ -60,10 +63,33 @@ export default function DashboardController(types, widgetService, userService, } Object.defineProperty(vm, 'toolbarOpened', { - get: function() { return vm.isToolbarOpened || vm.isEdit; }, + get: function() { return !vm.widgetEditMode && ($scope.forceFullscreen || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); }, set: function() { } }); + vm.layouts = { + main: { + show: false, + layoutCtx: { + id: 'main', + widgets: [], + widgetLayouts: {}, + gridSettings: {}, + ignoreLoading: false + } + }, + right: { + show: false, + layoutCtx: { + id: 'right', + widgets: [], + widgetLayouts: {}, + gridSettings: {}, + ignoreLoading: false + } + } + }; + vm.openToolbar = function() { $timeout(function() { vm.isToolbarOpened = true; @@ -76,31 +102,78 @@ export default function DashboardController(types, widgetService, userService, }); } + vm.showCloseToolbar = function() { + return !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch(); + } + + vm.showRightLayoutSwitch = function() { + return vm.isMobile && vm.layouts.right.show; + } + + vm.toggleLayouts = function() { + vm.isRightLayoutOpened = !vm.isRightLayoutOpened; + } + + vm.openRightLayout = function() { + vm.isRightLayoutOpened = true; + } + + vm.isRightLayoutOpened = false; + vm.isMobile = !$mdMedia('gt-sm'); + + $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) { + vm.isMobile = !isGtSm; + }); + + vm.mainLayoutWidth = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') { + return '100%'; + } else { + return vm.layouts.right.show && !vm.isMobile ? '50%' : '100%'; + } + } + + vm.mainLayoutHeight = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') { + return '100%'; + } else { + return 'auto'; + } + } + + vm.rightLayoutWidth = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') { + return '100%'; + } else { + return vm.isMobile ? '100%' : '50%'; + } + } + + vm.rightLayoutHeight = function() { + if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') { + return '100%'; + } else { + return 'auto'; + } + } + + vm.getServerTimeDiff = getServerTimeDiff; vm.addWidget = addWidget; vm.addWidgetFromType = addWidgetFromType; - vm.dashboardInited = dashboardInited; - vm.dashboardInitFailed = dashboardInitFailed; - vm.widgetMouseDown = widgetMouseDown; - vm.widgetClicked = widgetClicked; - vm.prepareDashboardContextMenu = prepareDashboardContextMenu; - vm.prepareWidgetContextMenu = prepareWidgetContextMenu; - vm.editWidget = editWidget; vm.exportDashboard = exportDashboard; - vm.exportWidget = exportWidget; vm.importWidget = importWidget; vm.isPublicUser = isPublicUser; vm.isTenantAdmin = isTenantAdmin; vm.isSystemAdmin = isSystemAdmin; - vm.loadDashboard = loadDashboard; - vm.getServerTimeDiff = getServerTimeDiff; - vm.noData = noData; vm.dashboardConfigurationError = dashboardConfigurationError; vm.showDashboardToolbar = showDashboardToolbar; vm.onAddWidgetClosed = onAddWidgetClosed; vm.onEditWidgetClosed = onEditWidgetClosed; - vm.openDeviceAliases = openDeviceAliases; + vm.openDashboardState = openDashboardState; + vm.openEntityAliases = openEntityAliases; vm.openDashboardSettings = openDashboardSettings; - vm.removeWidget = removeWidget; + vm.manageDashboardLayouts = manageDashboardLayouts; + vm.manageDashboardStates = manageDashboardStates; vm.saveDashboard = saveDashboard; vm.saveWidget = saveWidget; vm.toggleDashboardEditMode = toggleDashboardEditMode; @@ -109,10 +182,56 @@ export default function DashboardController(types, widgetService, userService, vm.displayTitle = displayTitle; vm.displayExport = displayExport; vm.displayDashboardTimewindow = displayDashboardTimewindow; - vm.displayDevicesSelect = displayDevicesSelect; + vm.displayDashboardsSelect = displayDashboardsSelect; + vm.displayEntitiesSelect = displayEntitiesSelect; vm.widgetsBundle; + vm.dashboardCtx = { + state: null, + stateController: { + openRightLayout: function() { + vm.openRightLayout(); + } + }, + onAddWidget: function(event, layoutCtx) { + addWidget(event, layoutCtx); + }, + onEditWidget: function(event, layoutCtx, widget) { + editWidget(event, layoutCtx, widget); + }, + onExportWidget: function(event, layoutCtx, widget) { + exportWidget(event, layoutCtx, widget); + }, + onWidgetMouseDown: function(event, layoutCtx, widget) { + widgetMouseDown(event, layoutCtx, widget); + }, + onWidgetClicked: function(event, layoutCtx, widget) { + widgetClicked(event, layoutCtx, widget); + }, + prepareDashboardContextMenu: function(layoutCtx) { + return prepareDashboardContextMenu(layoutCtx); + }, + prepareWidgetContextMenu: function(layoutCtx, widget) { + return prepareWidgetContextMenu(layoutCtx, widget); + }, + onRemoveWidget: function(event, layoutCtx, widget) { + removeWidget(event, layoutCtx, widget); + }, + copyWidget: function($event, layoutCtx, widget) { + copyWidget($event, layoutCtx, widget); + }, + copyWidgetReference: function($event, layoutCtx, widget) { + copyWidgetReference($event, layoutCtx, widget); + }, + pasteWidget: function($event, layoutCtx, pos) { + pasteWidget($event, layoutCtx, pos); + }, + pasteWidgetReference: function($event, layoutCtx, pos) { + pasteWidgetReference($event, layoutCtx, pos); + } + }; + $scope.$watch('vm.widgetsBundle', function (newVal, prevVal) { if (newVal !== prevVal && !vm.widgetEditMode) { loadWidgetLibrary(); @@ -132,6 +251,7 @@ export default function DashboardController(types, widgetService, userService, } }); + loadDashboard(); function loadWidgetLibrary() { vm.latestWidgetTypes = []; @@ -199,84 +319,100 @@ export default function DashboardController(types, widgetService, userService, } function loadDashboard() { - - var deferred = $q.defer(); - if (vm.widgetEditMode) { - $timeout(function () { - vm.dashboardConfiguration = { - timewindow: timeService.defaultTimewindow() - }; - vm.widgets = [{ - isSystemType: true, - bundleAlias: 'customWidgetBundle', - typeAlias: 'customWidget', - type: $rootScope.editWidgetInfo.type, - title: 'My widget', - sizeX: $rootScope.editWidgetInfo.sizeX * 2, - sizeY: $rootScope.editWidgetInfo.sizeY * 2, - row: 2, - col: 4, - config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig) - }]; - vm.widgets[0].config.title = vm.widgets[0].config.title || $rootScope.editWidgetInfo.widgetName; - deferred.resolve(); - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - parentScope.$root.$broadcast('widgetEditModeInited'); - parentScope.$root.$apply(); - }); + var widget = { + isSystemType: true, + bundleAlias: 'customWidgetBundle', + typeAlias: 'customWidget', + type: $rootScope.editWidgetInfo.type, + title: 'My widget', + sizeX: $rootScope.editWidgetInfo.sizeX * 2, + sizeY: $rootScope.editWidgetInfo.sizeY * 2, + row: 2, + col: 4, + config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig) + }; + widget.config.title = widget.config.title || $rootScope.editWidgetInfo.widgetName; + + vm.dashboard = dashboardUtils.createSingleWidgetDashboard(widget); + vm.dashboardConfiguration = vm.dashboard.configuration; + vm.dashboardCtx.dashboard = vm.dashboard; + vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; + var parentScope = $window.parent.angular.element($window.frameElement).scope(); + parentScope.$root.$broadcast('widgetEditModeInited'); + parentScope.$root.$apply(); } else { - dashboardService.getDashboard($stateParams.dashboardId) .then(function success(dashboard) { - vm.dashboard = dashboard; - if (vm.dashboard.configuration == null) { - vm.dashboard.configuration = {widgets: [], deviceAliases: {}}; - } - if (angular.isUndefined(vm.dashboard.configuration.widgets)) { - vm.dashboard.configuration.widgets = []; - } - if (angular.isUndefined(vm.dashboard.configuration.deviceAliases)) { - vm.dashboard.configuration.deviceAliases = {}; - } - - if (angular.isUndefined(vm.dashboard.configuration.timewindow)) { - vm.dashboard.configuration.timewindow = timeService.defaultTimewindow(); - } - deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases) + vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); + entityService.processEntityAliases(vm.dashboard.configuration.entityAliases) .then( function(resolution) { if (resolution.error && !isTenantAdmin()) { vm.configurationError = true; showAliasesResolutionError(resolution.error); - deferred.reject(); } else { - vm.aliasesInfo = resolution.aliasesInfo; vm.dashboardConfiguration = vm.dashboard.configuration; - vm.widgets = vm.dashboard.configuration.widgets; - deferred.resolve(); + vm.dashboardCtx.dashboard = vm.dashboard; + vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo; + vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow; } } ); - }, function fail(e) { - deferred.reject(e); + }, function fail() { + vm.configurationError = true; }); - } - return deferred.promise; } - function dashboardInitFailed() { - var parentScope = $window.parent.angular.element($window.frameElement).scope(); - parentScope.$emit('widgetEditModeInited'); - parentScope.$apply(); - vm.dashboardInitComplete = true; + function openDashboardState(state) { + var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state); + if (layoutsData) { + vm.dashboardCtx.state = state; + var layoutVisibilityChanged = false; + for (var l in vm.layouts) { + var layout = vm.layouts[l]; + var showLayout; + if (layoutsData[l]) { + showLayout = true; + } else { + showLayout = false; + } + if (layout.show != showLayout) { + layout.show = showLayout; + layoutVisibilityChanged = !vm.isMobile; + } + } + vm.isRightLayoutOpened = false; + updateLayouts(layoutVisibilityChanged); + } + + function updateLayouts(layoutVisibilityChanged) { + for (l in vm.layouts) { + layout = vm.layouts[l]; + if (layoutsData[l]) { + var layoutInfo = layoutsData[l]; + if (layout.layoutCtx.id === 'main') { + layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged); + } + updateLayout(layout, layoutInfo.widgets, layoutInfo.widgetLayouts, layoutInfo.gridSettings); + } else { + updateLayout(layout, [], {}, null); + } + } + } } - function dashboardInited(dashboard) { - vm.dashboardContainer = dashboard; - initHotKeys(); - vm.dashboardInitComplete = true; + function updateLayout(layout, widgets, widgetLayouts, gridSettings) { + if (gridSettings) { + layout.layoutCtx.gridSettings = gridSettings; + } + layout.layoutCtx.widgets = widgets; + layout.layoutCtx.widgetLayouts = widgetLayouts; + if (layout.show && layout.layoutCtx.ctrl) { + layout.layoutCtx.ctrl.reload(); + } + layout.layoutCtx.ignoreLoading = true; } function isPublicUser() { @@ -291,91 +427,148 @@ export default function DashboardController(types, widgetService, userService, return vm.user.authority === 'SYS_ADMIN'; } - function noData() { - return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0; - } - function dashboardConfigurationError() { - return vm.dashboardInitComplete && vm.configurationError; + return vm.configurationError; } function showDashboardToolbar() { - return vm.dashboardInitComplete; + return true; } - function openDeviceAliases($event) { + function openEntityAliases($event) { $mdDialog.show({ - controller: 'DeviceAliasesController', + controller: 'EntityAliasesController', controllerAs: 'vm', - templateUrl: deviceAliasesTemplate, + templateUrl: entityAliasesTemplate, locals: { config: { - deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases), - widgets: vm.widgets, - isSingleDeviceAlias: false, - singleDeviceAlias: null + entityAliases: angular.copy(vm.dashboard.configuration.entityAliases), + widgets: dashboardUtils.getWidgetsArray(vm.dashboard), + isSingleEntityAlias: false, + singleEntityAlias: null } }, parent: angular.element($document[0].body), skipHide: true, fullscreen: true, targetEvent: $event - }).then(function (deviceAliases) { - vm.dashboard.configuration.deviceAliases = deviceAliases; - deviceAliasesUpdated(); + }).then(function (entityAliases) { + vm.dashboard.configuration.entityAliases = entityAliases; + entityAliasesUpdated(); }, function () { }); } function openDashboardSettings($event) { + var gridSettings = null; + var layoutKeys = dashboardUtils.isSingleLayoutDashboard(vm.dashboard); + if (layoutKeys) { + gridSettings = angular.copy(vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings) + } $mdDialog.show({ controller: 'DashboardSettingsController', controllerAs: 'vm', - templateUrl: dashboardBackgroundTemplate, + templateUrl: dashboardSettingsTemplate, locals: { - gridSettings: angular.copy(vm.dashboard.configuration.gridSettings) + settings: angular.copy(vm.dashboard.configuration.settings), + gridSettings: gridSettings }, parent: angular.element($document[0].body), skipHide: true, fullscreen: true, targetEvent: $event - }).then(function (gridSettings) { - var prevGridSettings = vm.dashboard.configuration.gridSettings; - var prevColumns = prevGridSettings ? prevGridSettings.columns : 24; - var ratio = gridSettings.columns / prevColumns; - var currentWidgets = angular.copy(vm.widgets); - vm.widgets = []; - vm.dashboard.configuration.gridSettings = gridSettings; - for (var w in currentWidgets) { - var widget = currentWidgets[w]; - widget.sizeX = Math.round(widget.sizeX * ratio); - widget.sizeY = Math.round(widget.sizeY * ratio); - widget.col = Math.round(widget.col * ratio); - widget.row = Math.round(widget.row * ratio); + }).then(function (data) { + vm.dashboard.configuration.settings = data.settings; + var gridSettings = data.gridSettings; + if (gridSettings) { + updateLayoutGrid(layoutKeys, gridSettings); } - vm.dashboard.configuration.widgets = currentWidgets; - vm.widgets = vm.dashboard.configuration.widgets; }, function () { }); } - function editWidget($event, widget) { + function manageDashboardLayouts($event) { + $mdDialog.show({ + controller: 'ManageDashboardLayoutsController', + controllerAs: 'vm', + templateUrl: manageDashboardLayoutsTemplate, + locals: { + layouts: angular.copy(vm.dashboard.configuration.states[vm.dashboardCtx.state].layouts) + }, + parent: angular.element($document[0].body), + skipHide: true, + fullscreen: true, + targetEvent: $event + }).then(function (layouts) { + updateLayouts(layouts); + }, function () { + }); + } + + function manageDashboardStates($event) { + var dashboardConfiguration = vm.dashboard.configuration; + var states = angular.copy(dashboardConfiguration.states); + + $mdDialog.show({ + controller: 'ManageDashboardStatesController', + controllerAs: 'vm', + templateUrl: manageDashboardStatesTemplate, + locals: { + states: states + }, + parent: angular.element($document[0].body), + skipHide: true, + fullscreen: true, + targetEvent: $event + }).then(function (states) { + updateStates(states); + }, function () { + }); + } + + function updateLayoutGrid(layoutKeys, gridSettings) { + var layout = vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; + var layoutCtx = vm.layouts[layoutKeys.layout]; + layoutCtx.widgets = []; + dashboardUtils.updateLayoutSettings(layout, gridSettings); + var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, layoutKeys.state); + layoutCtx.widgets = layoutsData[layoutKeys.layout].widgets; + } + + function updateLayouts(layouts) { + dashboardUtils.setLayouts(vm.dashboard, vm.dashboardCtx.state, layouts); + openDashboardState(vm.dashboardCtx.state); + } + + function updateStates(states) { + vm.dashboard.configuration.states = states; + dashboardUtils.removeUnusedWidgets(vm.dashboard); + var targetState = vm.dashboardCtx.state; + if (!vm.dashboard.configuration.states[targetState]) { + targetState = dashboardUtils.getRootStateId(vm.dashboardConfiguration.states); + } + openDashboardState(targetState); + } + + function editWidget($event, layoutCtx, widget) { $event.stopPropagation(); if (vm.editingWidgetOriginal === widget) { $timeout(onEditWidgetClosed()); } else { var transition = !vm.forceDashboardMobileMode; vm.editingWidgetOriginal = widget; + vm.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id]; vm.editingWidget = angular.copy(vm.editingWidgetOriginal); + vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal); + vm.editingLayoutCtx = layoutCtx; vm.editingWidgetSubtitle = widgetService.getInstantWidgetInfo(vm.editingWidget).widgetName; vm.forceDashboardMobileMode = true; vm.isEditingWidget = true; - - if (vm.dashboardContainer) { + if (layoutCtx) { var delayOffset = transition ? 350 : 0; var delay = transition ? 400 : 300; $timeout(function () { - vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, delay); + layoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, delay); }, delayOffset, false); } } @@ -385,82 +578,36 @@ export default function DashboardController(types, widgetService, userService, importExport.exportDashboard(vm.currentDashboardId); } - function exportWidget($event, widget) { + function exportWidget($event, layoutCtx, widget) { $event.stopPropagation(); - importExport.exportWidget(vm.dashboard, widget); + importExport.exportWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); } function importWidget($event) { $event.stopPropagation(); - importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated); + importExport.importWidget($event, vm.dashboard, vm.dashboardCtx.state, + selectTargetLayout, entityAliasesUpdated).then( + function success(importData) { + var widget = importData.widget; + var layoutId = importData.layoutId; + vm.layouts[layoutId].layoutCtx.widgets.push(widget); + } + ); } - function widgetMouseDown($event, widget) { + function widgetMouseDown($event, layoutCtx, widget) { if (vm.isEdit && !vm.isEditingWidget) { - vm.dashboardContainer.selectWidget(widget, 0); + layoutCtx.ctrl.selectWidget(widget, 0); } } - function widgetClicked($event, widget) { + function widgetClicked($event, layoutCtx, widget) { if (vm.isEditingWidget) { - editWidget($event, widget); + editWidget($event, layoutCtx, widget); } } - function isHotKeyAllowed(event) { - var target = event.target || event.srcElement; - var scope = angular.element(target).scope(); - return scope && scope.$parent !== $rootScope; - } - - function initHotKeys() { - $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) { - hotkeys.bindTo($scope) - .add({ - combo: 'ctrl+c', - description: translations['action.copy'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - copyWidget(event, widget); - } - } - } - }) - .add({ - combo: 'ctrl+v', - description: translations['action.paste'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - if (itembuffer.hasWidget()) { - event.preventDefault(); - pasteWidget(event); - } - } - } - }) - .add({ - combo: 'ctrl+x', - description: translations['action.delete'], - callback: function (event) { - if (isHotKeyAllowed(event) && - vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { - var widget = vm.dashboardContainer.getSelectedWidget(); - if (widget) { - event.preventDefault(); - removeWidget(event, widget); - } - } - } - }); - }); - } - - function prepareDashboardContextMenu() { + function prepareDashboardContextMenu(layoutCtx) { var dashboardContextActions = []; if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { dashboardContextActions.push( @@ -473,36 +620,47 @@ export default function DashboardController(types, widgetService, userService, ); dashboardContextActions.push( { - action: openDeviceAliases, + action: openEntityAliases, enabled: true, - value: "device.aliases", + value: "entity.aliases", icon: "devices_other" } ); dashboardContextActions.push( { - action: pasteWidget, + action: function ($event) { + layoutCtx.ctrl.pasteWidget($event); + }, enabled: itembuffer.hasWidget(), value: "action.paste", icon: "content_paste", shortcut: "M-V" } ); + dashboardContextActions.push( + { + action: function ($event) { + layoutCtx.ctrl.pasteWidgetReference($event); + }, + enabled: itembuffer.canPasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id), + value: "action.paste-reference", + icon: "content_paste", + shortcut: "M-I" + } + ); + } return dashboardContextActions; } - function pasteWidget($event) { - var pos = vm.dashboardContainer.getEventGridPosition($event); - itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated); - } - - function prepareWidgetContextMenu() { + function prepareWidgetContextMenu(layoutCtx) { var widgetContextActions = []; if (vm.isEdit && !vm.isEditingWidget) { widgetContextActions.push( { - action: editWidget, + action: function (event, widget) { + editWidget(event, layoutCtx, widget); + }, enabled: true, value: "action.edit", icon: "edit" @@ -511,7 +669,9 @@ export default function DashboardController(types, widgetService, userService, if (!vm.widgetEditMode) { widgetContextActions.push( { - action: copyWidget, + action: function (event, widget) { + copyWidget(event, layoutCtx, widget); + }, enabled: true, value: "action.copy", icon: "content_copy", @@ -520,7 +680,20 @@ export default function DashboardController(types, widgetService, userService, ); widgetContextActions.push( { - action: removeWidget, + action: function (event, widget) { + copyWidgetReference(event, layoutCtx, widget); + }, + enabled: true, + value: "action.copy-reference", + icon: "content_copy", + shortcut: "M-R" + } + ); + widgetContextActions.push( + { + action: function (event, widget) { + removeWidget(event, layoutCtx, widget); + }, enabled: true, value: "action.delete", icon: "clear", @@ -532,8 +705,12 @@ export default function DashboardController(types, widgetService, userService, return widgetContextActions; } - function copyWidget($event, widget) { - itembuffer.copyWidget(vm.dashboard, widget); + function copyWidget($event, layoutCtx, widget) { + itembuffer.copyWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); + } + + function copyWidgetReference($event, layoutCtx, widget) { + itembuffer.copyWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget); } function helpLinkIdForWidgetType() { @@ -562,36 +739,45 @@ export default function DashboardController(types, widgetService, userService, } function displayTitle() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) { - return vm.dashboard.configuration.gridSettings.showTitle; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showTitle)) { + return vm.dashboard.configuration.settings.showTitle; } else { return true; } } function displayExport() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardExport)) { - return vm.dashboard.configuration.gridSettings.showDashboardExport; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showDashboardExport)) { + return vm.dashboard.configuration.settings.showDashboardExport; } else { return true; } } function displayDashboardTimewindow() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardTimewindow)) { - return vm.dashboard.configuration.gridSettings.showDashboardTimewindow; + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showDashboardTimewindow)) { + return vm.dashboard.configuration.settings.showDashboardTimewindow; } else { return true; } } - function displayDevicesSelect() { - if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showDevicesSelect)) { - return vm.dashboard.configuration.gridSettings.showDevicesSelect; + function displayDashboardsSelect() { + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showDashboardsSelect)) { + return vm.dashboard.configuration.settings.showDashboardsSelect; + } else { + return true; + } + } + + function displayEntitiesSelect() { + if (vm.dashboard && vm.dashboard.configuration.settings && + angular.isDefined(vm.dashboard.configuration.settings.showEntitiesSelect)) { + return vm.dashboard.configuration.settings.showEntitiesSelect; } else { return true; } @@ -601,32 +787,40 @@ export default function DashboardController(types, widgetService, userService, if (widgetForm.$dirty) { widgetForm.$setPristine(); vm.editingWidget = angular.copy(vm.editingWidgetOriginal); + vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal); } } function saveWidget(widgetForm) { widgetForm.$setPristine(); var widget = angular.copy(vm.editingWidget); - var index = vm.widgets.indexOf(vm.editingWidgetOriginal); - vm.widgets[index] = widget; + var widgetLayout = angular.copy(vm.editingWidgetLayout); + var id = vm.editingWidgetOriginal.id; + var index = vm.editingLayoutCtx.widgets.indexOf(vm.editingWidgetOriginal); + vm.dashboardConfiguration.widgets[id] = widget; vm.editingWidgetOriginal = widget; - vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, 0); + vm.editingWidgetLayoutOriginal = widgetLayout; + vm.editingLayoutCtx.widgets[index] = widget; + vm.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; + vm.editingLayoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, 0); } function onEditWidgetClosed() { vm.editingWidgetOriginal = null; vm.editingWidget = null; + vm.editingWidgetLayoutOriginal = null; + vm.editingWidgetLayout = null; + vm.editingLayoutCtx = null; vm.editingWidgetSubtitle = null; vm.isEditingWidget = false; - if (vm.dashboardContainer) { - vm.dashboardContainer.resetHighlight(); - } + resetHighlight(); vm.forceDashboardMobileMode = false; } - function addWidget() { + function addWidget(event, layoutCtx) { loadWidgetLibrary(); vm.isAddingWidget = true; + vm.addingLayoutCtx = layoutCtx; } function onAddWidgetClosed() { @@ -636,6 +830,33 @@ export default function DashboardController(types, widgetService, userService, vm.staticWidgetTypes = []; } + function selectTargetLayout($event) { + var deferred = $q.defer(); + var layouts = vm.dashboardConfiguration.states[vm.dashboardCtx.state].layouts; + var layoutIds = Object.keys(layouts); + if (layoutIds.length > 1) { + $mdDialog.show({ + controller: 'SelectTargetLayoutController', + controllerAs: 'vm', + templateUrl: selectTargetLayoutTemplate, + parent: angular.element($document[0].body), + fullscreen: true, + skipHide: true, + targetEvent: $event + }).then( + function success(layoutId) { + deferred.resolve(layoutId); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(layoutIds[0]); + } + return deferred.promise; + } + function addWidgetFromType(event, widget) { vm.onAddWidgetClosed(); vm.isAddingWidget = false; @@ -655,17 +876,22 @@ export default function DashboardController(types, widgetService, userService, config: config }; + function addWidgetToLayout(widget, layoutId) { + dashboardUtils.addWidgetToLayout(vm.dashboard, vm.dashboardCtx.state, layoutId, widget); + vm.layouts[layoutId].layoutCtx.widgets.push(widget); + } + function addWidget(widget) { - var columns = 24; - if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { - columns = vm.dashboard.configuration.gridSettings.columns; - } - if (columns != 24) { - var ratio = columns / 24; - widget.sizeX *= ratio; - widget.sizeY *= ratio; + if (vm.addingLayoutCtx) { + addWidgetToLayout(widget, vm.addingLayoutCtx.id); + vm.addingLayoutCtx = null; + } else { + selectTargetLayout(event).then( + function success(layoutId) { + addWidgetToLayout(widget, layoutId); + } + ); } - vm.widgets.push(widget); } if (widgetTypeInfo.useCustomDatasources) { @@ -677,7 +903,7 @@ export default function DashboardController(types, widgetService, userService, templateUrl: addWidgetTemplate, locals: { dashboard: vm.dashboard, - aliasesInfo: vm.aliasesInfo, + aliasesInfo: vm.dashboardCtx.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo }, @@ -691,17 +917,17 @@ export default function DashboardController(types, widgetService, userService, } }).then(function (result) { var widget = result.widget; - vm.aliasesInfo = result.aliasesInfo; + vm.dashboardCtx.aliasesInfo = result.aliasesInfo; addWidget(widget); }, function (rejection) { - vm.aliasesInfo = rejection.aliasesInfo; + vm.dashboardCtx.aliasesInfo = rejection.aliasesInfo; }); } } ); } - function removeWidget(event, widget) { + function removeWidget(event, layoutCtx, widget) { var title = widget.config.title; if (!title || title.length === 0) { title = widgetService.getInstantWidgetInfo(widget).widgetName; @@ -714,32 +940,61 @@ export default function DashboardController(types, widgetService, userService, .cancel($translate.instant('action.no')) .ok($translate.instant('action.yes')); $mdDialog.show(confirm).then(function () { - vm.widgets.splice(vm.widgets.indexOf(widget), 1); + var index = layoutCtx.widgets.indexOf(widget); + if (index > -1) { + layoutCtx.widgets.splice(index, 1); + dashboardUtils.removeWidgetFromLayout(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget.id); + } }); } + function pasteWidget(event, layoutCtx, pos) { + itembuffer.pasteWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos, entityAliasesUpdated).then( + function (widget) { + if (widget) { + layoutCtx.widgets.push(widget); + } + } + ); + } + + function pasteWidgetReference(event, layoutCtx, pos) { + itembuffer.pasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos).then( + function (widget) { + if (widget) { + layoutCtx.widgets.push(widget); + } + } + ); + } + function setEditMode(isEdit, revert) { vm.isEdit = isEdit; if (vm.isEdit) { - if (vm.widgetEditMode) { - vm.prevWidgets = angular.copy(vm.widgets); - } else { - vm.prevDashboard = angular.copy(vm.dashboard); - } + vm.prevDashboard = angular.copy(vm.dashboard); + vm.prevDashboardState = vm.dashboardCtx.state; } else { if (vm.widgetEditMode) { if (revert) { - vm.widgets = vm.prevWidgets; + vm.dashboard = vm.prevDashboard; } } else { - if (vm.dashboardContainer) { - vm.dashboardContainer.resetHighlight(); - } + resetHighlight(); if (revert) { vm.dashboard = vm.prevDashboard; - vm.widgets = vm.dashboard.configuration.widgets; vm.dashboardConfiguration = vm.dashboard.configuration; - deviceAliasesUpdated(); + openDashboardState(vm.prevDashboardState); + entityAliasesUpdated(); + } + } + } + } + + function resetHighlight() { + for (var l in vm.layouts) { + if (vm.layouts[l].layoutCtx) { + if (vm.layouts[l].layoutCtx.ctrl) { + vm.layouts[l].layoutCtx.ctrl.resetHighlight(); } } } @@ -768,21 +1023,27 @@ export default function DashboardController(types, widgetService, userService, $mdDialog.show(alert); } - function deviceAliasesUpdated() { - deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases) + function entityAliasesUpdated() { + var deferred = $q.defer(); + entityService.processEntityAliases(vm.dashboard.configuration.entityAliases) .then( function(resolution) { if (resolution.aliasesInfo) { - vm.aliasesInfo = resolution.aliasesInfo; + vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo; } + deferred.resolve(); } ); + return deferred.promise; } function notifyDashboardUpdated() { if (vm.widgetEditMode) { var parentScope = $window.parent.angular.element($window.frameElement).scope(); - var widget = vm.widgets[0]; + var widget = vm.layouts.main.layoutCtx.widgets[0]; + var layout = vm.layouts.main.layoutCtx.widgetLayouts[widget.id]; + widget.sizeX = layout.sizeX; + widget.sizeY = layout.sizeY; parentScope.$root.$broadcast('widgetEditUpdated', widget); parentScope.$root.$apply(); } else { diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js index e9fe1f24ea..92bb36220f 100644 --- a/ui/src/app/dashboard/dashboard.routes.js +++ b/ui/src/app/dashboard/dashboard.routes.js @@ -66,7 +66,8 @@ export default function DashboardRoutes($stateProvider) { } }) .state('home.dashboards.dashboard', { - url: '/:dashboardId', + url: '/:dashboardId?state', + reloadOnSearch: false, module: 'private', auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], views: { @@ -86,7 +87,8 @@ export default function DashboardRoutes($stateProvider) { } }) .state('home.customers.dashboards.dashboard', { - url: '/:dashboardId', + url: '/:dashboardId?state', + reloadOnSearch: false, module: 'private', auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], views: { diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss index 8f50ca295f..bc5ec56a33 100644 --- a/ui/src/app/dashboard/dashboard.scss +++ b/ui/src/app/dashboard/dashboard.scss @@ -63,7 +63,7 @@ tb-details-sidenav.tb-widget-details-sidenav { section.tb-dashboard-toolbar { position: absolute; top: 0px; - left: -100%; + left: 0px; z-index: 3; pointer-events: none; &.tb-dashboard-toolbar-opened { @@ -118,6 +118,27 @@ section.tb-dashboard-toolbar { .close-action { margin-right: -18px; } + .md-fab-action-item { + width: 100%; + height: 46px; + .tb-dashboard-action-panels { + height: 46px; + flex-direction: row-reverse; + .tb-dashboard-action-panel { + height: 46px; + flex-direction: row-reverse; + div { + height: 46px; + } + md-select { + pointer-events: all; + } + tb-states-component { + pointer-events: all; + } + } + } + } } } } @@ -133,6 +154,19 @@ section.tb-dashboard-toolbar { margin-top: 0px; @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2) .2s); } + .tb-dashboard-layouts { + md-backdrop { + z-index: 1; + } + #tb-main-layout { + + } + #tb-right-layout { + md-sidenav { + z-index: 1; + } + } + } } /***************************** diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index 9276e0f17c..078daabd52 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -16,16 +16,10 @@ --> + hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom">
- @@ -37,77 +31,100 @@ - - - {{ 'dashboard.close-toolbar' | translate }} - - arrow_forward - - - - - - - - {{ 'dashboard.export' | translate }} - - file_download - - - - - - - - {{ 'device.aliases' | translate }} - - devices_other - - - - {{ 'dashboard.settings' | translate }} - - settings - - - +
+
+ + + {{ 'dashboard.close-toolbar' | translate }} + + arrow_forward + + + + + {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }} + + + + + + + + + {{ 'dashboard.export' | translate }} + + file_download + + + + + + + + {{ 'entity.aliases' | translate }} + + devices_other + + + + {{ 'dashboard.settings' | translate }} + + settings + + + +
+
+
+ + + {{ 'dashboard.manage-states' | translate }} + + layers + + + + {{ 'layout.manage' | translate }} + + view_compact + +
+
+ + + + +
+
+
-
- - dashboard.no-widgets - - - add - {{ 'dashboard.add-widget' | translate }} - -
@@ -116,46 +133,47 @@
+ ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">

{{ vm.dashboard.title }}

- - + +
-
- - +
+ + +
+ + + +
@@ -286,7 +305,7 @@ - @@ -296,7 +315,7 @@ @@ -308,7 +327,7 @@
-