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..9b00682d02 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); @@ -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); @@ -387,6 +449,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 +484,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/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/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..f6c62bee1f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -0,0 +1,51 @@ +/** + * 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.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +/** + * Created by ashvayka on 11.05.17. + */ +@Data +public class Alarm extends BaseData { + + private TenantId tenantId; + private String type; + private EntityId originator; + private AlarmSeverity severity; + private AlarmStatus status; + private long startTs; + private long endTs; + private long ackTs; + private long clearTs; + private JsonNode details; + private boolean propagate; + + public Alarm() { + super(); + } + + public Alarm(AlarmId id) { + super(id); + } + +} 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..2dc0189ed0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -0,0 +1,34 @@ +/** + * 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.id.TenantId; +import org.thingsboard.server.common.data.page.TimePageLink; + +/** + * Created by ashvayka on 11.05.17. + */ +@Data +public class AlarmQuery { + + private TenantId tenantId; + 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..0f1b346f2c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java @@ -0,0 +1,33 @@ +/** + * 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; + + public boolean isAck() { + return this == ACTIVE_ACK || this == CLEARED_ACK; + } + + public boolean isCleared() { + return this == CLEARED_ACK || this == CLEARED_UNACK; + } + +} 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..83286ac54a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -0,0 +1,34 @@ +/** + * 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.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.model.AlarmEntity; + +/** + * Created by ashvayka on 11.05.17. + */ +public interface AlarmDao extends Dao { + + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + + AlarmEntity save(Alarm alarm); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java new file mode 100644 index 0000000000..a6f6ee7109 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java @@ -0,0 +1,53 @@ +/** + * 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 lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.AbstractModelDao; +import org.thingsboard.server.dao.model.AlarmEntity; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COLUMN_FAMILY_NAME; + +@Component +@Slf4j +public class AlarmDaoImpl extends AbstractModelDao implements AlarmDao { + + @Override + protected Class getColumnFamilyClass() { + return AlarmEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return ALARM_COLUMN_FAMILY_NAME; + } + + @Override + public AlarmEntity save(Alarm alarm) { + log.debug("Save asset [{}] ", alarm); + return save(new AlarmEntity(alarm)); + } + + @Override + public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return null; + } +} 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..a3daafca6e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -0,0 +1,42 @@ +/** + * 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.id.EntityId; +import org.thingsboard.server.common.data.page.TimePageData; + +import java.util.Optional; + +/** + * Created by ashvayka on 11.05.17. + */ +public interface AlarmService { + + Alarm createOrUpdateAlarm(Alarm alarm); + + ListenableFuture updateAlarm(Alarm alarm); + + ListenableFuture ackAlarm(AlarmId alarmId, long ackTs); + + ListenableFuture clearAlarm(AlarmId alarmId, long ackTs); + + ListenableFuture> findAlarms(AlarmQuery query); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java new file mode 100644 index 0000000000..b9df342018 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -0,0 +1,266 @@ +/** + * 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.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.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmId; +import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +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.page.TimePageData; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.asset.AssetDao; +import org.thingsboard.server.dao.asset.AssetSearchQuery; +import org.thingsboard.server.dao.asset.AssetService; +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.AlarmEntity; +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.relation.RelationService; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; +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.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +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 BaseAlarmService extends BaseEntityService implements AlarmService { + + private static final String ALARM_RELATION_PREFIX = "ALARM_"; + private static final String ALARM_RELATION = "ALARM_ANY"; + + @Autowired + private AlarmDao alarmDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private RelationService relationService; + + @Override + public Alarm createOrUpdateAlarm(Alarm alarm) { + alarmDataValidator.validate(alarm); + try { + if (alarm.getStartTs() == 0L) { + alarm.setStartTs(System.currentTimeMillis()); + } + Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get(); + if (existing == null || existing.getStatus().isCleared()) { + log.debug("New Alarm : {}", alarm); + Alarm saved = getData(alarmDao.save(new AlarmEntity(alarm))); + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(saved.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE)); + List parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList()); + for (EntityId parentId : parentEntities) { + createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION)); + createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name())); + } + return saved; + } else { + log.debug("Alarm before merge: {}", alarm); + alarm = merge(existing, alarm); + log.debug("Alarm after merge: {}", alarm); + return getData(alarmDao.save(new AlarmEntity(alarm))); + } + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public ListenableFuture updateAlarm(Alarm update) { + alarmDataValidator.validate(update); + return getAndUpdate(update.getId(), new Function() { + @Nullable + @Override + public Boolean apply(@Nullable AlarmEntity entity) { + Alarm alarm = getData(entity); + if (alarm == null) { + return false; + } else { + AlarmStatus oldStatus = alarm.getStatus(); + AlarmStatus newStatus = update.getStatus(); + alarmDao.save(new AlarmEntity(merge(alarm, update))); + if (oldStatus != newStatus) { + updateRelations(alarm, oldStatus, newStatus); + } + return true; + } + } + }); + } + + @Override + public ListenableFuture ackAlarm(AlarmId alarmId, long ackTime) { + return getAndUpdate(alarmId, new Function() { + @Nullable + @Override + public Boolean apply(@Nullable AlarmEntity entity) { + Alarm alarm = getData(entity); + if (alarm == null || alarm.getStatus().isAck()) { + return false; + } else { + AlarmStatus oldStatus = alarm.getStatus(); + AlarmStatus newStatus = oldStatus.isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK; + alarm.setStatus(newStatus); + alarm.setAckTs(ackTime); + alarmDao.save(new AlarmEntity(alarm)); + updateRelations(alarm, oldStatus, newStatus); + return true; + } + } + }); + } + + @Override + public ListenableFuture clearAlarm(AlarmId alarmId, long clearTime) { + return getAndUpdate(alarmId, new Function() { + @Nullable + @Override + public Boolean apply(@Nullable AlarmEntity entity) { + Alarm alarm = getData(entity); + if (alarm == null || alarm.getStatus().isCleared()) { + return false; + } else { + AlarmStatus oldStatus = alarm.getStatus(); + AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; + alarm.setStatus(newStatus); + alarm.setClearTs(clearTime); + alarmDao.save(new AlarmEntity(alarm)); + updateRelations(alarm, oldStatus, newStatus); + return true; + } + } + }); + } + + @Override + public ListenableFuture> findAlarms(AlarmQuery query) { + return null; + } + + private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException { + log.debug("Deleting Alarm relation: {}", alarmRelation); + relationService.deleteRelation(alarmRelation).get(); + } + + private void createRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException { + log.debug("Creating Alarm relation: {}", alarmRelation); + relationService.saveRelation(alarmRelation).get(); + } + + private Alarm merge(Alarm existing, Alarm alarm) { + if (alarm.getStartTs() > existing.getEndTs()) { + existing.setEndTs(alarm.getStartTs()); + } + if (alarm.getEndTs() > existing.getEndTs()) { + existing.setEndTs(alarm.getEndTs()); + } + if (alarm.getClearTs() > existing.getClearTs()) { + existing.setClearTs(alarm.getClearTs()); + } + if (alarm.getAckTs() > existing.getAckTs()) { + existing.setAckTs(alarm.getAckTs()); + } + existing.setStatus(alarm.getStatus()); + existing.setSeverity(alarm.getSeverity()); + existing.setDetails(alarm.getDetails()); + return existing; + } + + private void updateRelations(Alarm alarm, AlarmStatus oldStatus, AlarmStatus newStatus) { + try { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE)); + List parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList()); + for (EntityId parentId : parentEntities) { + deleteRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name())); + createRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name())); + } + } catch (ExecutionException | InterruptedException e) { + log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus); + throw new RuntimeException(e); + } + } + + private ListenableFuture getAndUpdate(AlarmId alarmId, Function function) { + validateId(alarmId, "Alarm id should be specified!"); + ListenableFuture entity = alarmDao.findByIdAsync(alarmId.getId()); + return Futures.transform(entity, function); + } + + private DataValidator alarmDataValidator = + new DataValidator() { + + @Override + protected void validateDataImpl(Alarm alarm) { + if (StringUtils.isEmpty(alarm.getType())) { + throw new DataValidationException("Alarm type should be specified!"); + } + if (alarm.getOriginator() == null) { + throw new DataValidationException("Alarm originator should be specified!"); + } + if (alarm.getSeverity() == null) { + throw new DataValidationException("Alarm severity should be specified!"); + } + if (alarm.getStatus() == null) { + throw new DataValidationException("Alarm status should be specified!"); + } + if (alarm.getTenantId() == null) { + throw new DataValidationException("Alarm should be assigned to tenant!"); + } else { + TenantEntity tenant = tenantDao.findById(alarm.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Alarm is referencing to non-existent tenant!"); + } + } + } + }; +} 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/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 2e0abfbdfd..826404241b 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; @@ -91,6 +90,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/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 3d1ce31349..43949017f4 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 @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.page.TextPageLink; 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; @@ -53,7 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @Slf4j -public class DeviceServiceImpl implements DeviceService { +public class DeviceServiceImpl extends BaseEntityService implements DeviceService { @Autowired private DeviceDao deviceDao; @@ -132,6 +133,7 @@ public class DeviceServiceImpl implements DeviceService { if (deviceCredentials != null) { deviceCredentialsService.deleteDeviceCredentials(deviceCredentials); } + deleteEntityRelations(deviceId); deviceDao.removeById(deviceId.getId()); } 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/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/AlarmEntity.java new file mode 100644 index 0000000000..7dba381d65 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/AlarmEntity.java @@ -0,0 +1,148 @@ +/** + * 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.*; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmId; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.type.AlarmSeverityCodec; +import org.thingsboard.server.dao.model.type.AlarmStatusCodec; +import org.thingsboard.server.dao.model.type.EntityTypeCodec; +import org.thingsboard.server.dao.model.type.JsonCodec; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Table(name = ALARM_COLUMN_FAMILY_NAME) +public final class AlarmEntity implements BaseEntity { + + @Transient + private static final long serialVersionUID = -1265181166886910152L; + + @ClusteringColumn(value = 1) + @Column(name = ID_PROPERTY) + private UUID id; + + @PartitionKey(value = 0) + @Column(name = ALARM_TENANT_ID_PROPERTY) + private UUID tenantId; + + @PartitionKey(value = 1) + @Column(name = ALARM_ORIGINATOR_ID_PROPERTY) + private UUID originatorId; + + @PartitionKey(value = 2) + @Column(name = ALARM_ORIGINATOR_TYPE_PROPERTY, codec = EntityTypeCodec.class) + private EntityType originatorType; + + @ClusteringColumn(value = 0) + @Column(name = ALARM_TYPE_PROPERTY) + private String type; + + @Column(name = ALARM_SEVERITY_PROPERTY, codec = AlarmSeverityCodec.class) + private AlarmSeverity severity; + + @Column(name = ALARM_STATUS_PROPERTY, codec = AlarmStatusCodec.class) + private AlarmStatus status; + + @Column(name = ALARM_START_TS_PROPERTY) + private Long startTs; + + @Column(name = ALARM_END_TS_PROPERTY) + private Long endTs; + + @Column(name = ALARM_ACK_TS_PROPERTY) + private Long ackTs; + + @Column(name = ALARM_CLEAR_TS_PROPERTY) + private Long clearTs; + + @Column(name = ALARM_DETAILS_PROPERTY, codec = JsonCodec.class) + private JsonNode details; + + @Column(name = ALARM_PROPAGATE_PROPERTY) + private Boolean propagate; + + public AlarmEntity() { + super(); + } + + public AlarmEntity(Alarm alarm) { + if (alarm.getId() != null) { + this.id = alarm.getId().getId(); + } + if (alarm.getTenantId() != null) { + this.tenantId = alarm.getTenantId().getId(); + } + this.type = alarm.getType(); + this.originatorId = alarm.getOriginator().getId(); + this.originatorType = alarm.getOriginator().getEntityType(); + this.type = alarm.getType(); + this.severity = alarm.getSeverity(); + this.status = alarm.getStatus(); + this.propagate = alarm.isPropagate(); + this.startTs = alarm.getStartTs(); + this.endTs = alarm.getEndTs(); + this.ackTs = alarm.getAckTs(); + this.clearTs = alarm.getClearTs(); + this.details = alarm.getDetails(); + } + + 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; + } + + @Override + public Alarm toData() { + Alarm alarm = new Alarm(new AlarmId(id)); + alarm.setCreatedTime(UUIDs.unixTimestamp(id)); + if (tenantId != null) { + alarm.setTenantId(new TenantId(tenantId)); + } + alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); + alarm.setType(type); + alarm.setSeverity(severity); + alarm.setStatus(status); + alarm.setPropagate(propagate); + alarm.setStartTs(startTs); + alarm.setEndTs(endTs); + alarm.setAckTs(ackTs); + alarm.setClearTs(clearTs); + alarm.setDetails(details); + return alarm; + } + +} \ No newline at end of file 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/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..6a002b0c12 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 @@ -126,6 +126,49 @@ public class ModelConstants { 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 alarm constants. + */ + public static final String ALARM_COLUMN_FAMILY_NAME = "alarm"; + public static final String ALARM_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY; + public static final String ALARM_TYPE_PROPERTY = "type"; + public static final String ALARM_DETAILS_PROPERTY = "details"; + public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id"; + public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; + public static final String ALARM_SEVERITY_PROPERTY = "severity"; + public static final String ALARM_STATUS_PROPERTY = "status"; + public static final String ALARM_START_TS_PROPERTY = "start_ts"; + public static final String ALARM_END_TS_PROPERTY = "end_ts"; + public static final String ALARM_ACK_TS_PROPERTY = "ack_ts"; + public static final String ALARM_CLEAR_TS_PROPERTY = "clear_ts"; + public static final String ALARM_PROPAGATE_PROPERTY = "propagate"; + + /** + * 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/model/type/AlarmSeverityCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java new file mode 100644 index 0000000000..2f8e8400af --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.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.dao.model.type; + +import com.datastax.driver.extras.codecs.enums.EnumNameCodec; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.dao.alarm.AlarmService; + +public class AlarmSeverityCodec extends EnumNameCodec { + + public AlarmSeverityCodec() { + super(AlarmSeverity.class); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java new file mode 100644 index 0000000000..7ba7d7b7f8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java @@ -0,0 +1,27 @@ +/** + * 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.type; + +import com.datastax.driver.extras.codecs.enums.EnumNameCodec; +import org.thingsboard.server.common.data.alarm.AlarmStatus; + +public class AlarmStatusCodec extends EnumNameCodec { + + public AlarmStatusCodec() { + super(AlarmStatus.class); + } + +} 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..998f2ef5a0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -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. + */ +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.debug("Filters are not set [{}]", query); + } + + 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) { + if (filters == null || filters.isEmpty()) { + relations.add(relation); + } else { + 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..e3e2a1fc57 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -0,0 +1,52 @@ +/** + * 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); + +// TODO: This method may be useful for some validations in the future +// ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); + +} 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..797e4a1f19 100644 --- a/dao/src/main/resources/schema.cql +++ b/dao/src/main/resources/schema.cql @@ -202,6 +202,73 @@ 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.alarm ( + id timeuuid, + tenant_id timeuuid, + type text, + originator_id timeuuid, + originator_type text, + severity text, + status text, + start_ts bigint, + end_ts bigint, + ack_ts bigint, + clear_ts bigint, + details text, + propagate boolean, + PRIMARY KEY ((tenant_id, originator_id, originator_type), type, id) +); + +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 9ccc087af2..ab5975f19b 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..35c14fb26f --- /dev/null +++ b/ui/src/app/api/attribute.service.js @@ -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. + */ +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, + 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 = {}; + 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/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..6f153634d4 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, @@ -159,174 +151,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 +228,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) { 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..4904a6a3c2 --- /dev/null +++ b/ui/src/app/api/entity.service.js @@ -0,0 +1,477 @@ +/* + * 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, userService, deviceService, + assetService, tenantService, customerService, + ruleService, pluginService, types, utils) { + var service = { + getEntity: getEntity, + getEntities: getEntities, + getEntitiesByNameFilter: getEntitiesByNameFilter, + entityName: entityName, + processEntityAliases: processEntityAliases, + getEntityKeys: getEntityKeys, + checkEntityAlias: checkEntityAlias, + createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo + }; + + 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); + promise.then( + function success(result) { + deferred.resolve(result); + }, + function fail() { + 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 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/widget.service.js b/ui/src/app/api/widget.service.js index 21c38e254c..50a739722c 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -20,6 +20,8 @@ import tinycolor from 'tinycolor2'; import thingsboardLedLight from '../components/led-light.directive'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; +import agrologWidgets from '../widget/lib/agrolog'; + import TbFlot from '../widget/lib/flot-widget'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge'; @@ -32,7 +34,7 @@ import cssjs from '../../vendor/css.js/css'; import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; -export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, +export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, agrologWidgets, thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 290e9cdf1c..f67be331a9 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -53,6 +53,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller'; import thingsboardMenu from './services/menu.service'; import thingsboardRaf from './common/raf.provider'; import thingsboardUtils from './common/utils.service'; +import thingsboardDashboardUtils from './common/dashboard-utils.service'; import thingsboardTypes from './common/types.constant'; import thingsboardApiTime from './api/time.service'; import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; @@ -62,6 +63,10 @@ import thingsboardHome from './layout'; import thingsboardApiLogin from './api/login.service'; import thingsboardApiDevice from './api/device.service'; import thingsboardApiUser from './api/user.service'; +import thingsboardApiEntityRelation from './api/entity-relation.service'; +import thingsboardApiAsset from './api/asset.service'; +import thingsboardApiAttribute from './api/attribute.service'; +import thingsboardApiEntity from './api/entity.service'; import 'typeface-roboto'; import 'font-awesome/css/font-awesome.min.css'; @@ -105,6 +110,7 @@ angular.module('thingsboard', [ thingsboardMenu, thingsboardRaf, thingsboardUtils, + thingsboardDashboardUtils, thingsboardTypes, thingsboardApiTime, thingsboardKeyboardShortcut, @@ -114,6 +120,10 @@ angular.module('thingsboard', [ thingsboardApiLogin, thingsboardApiDevice, thingsboardApiUser, + thingsboardApiEntityRelation, + thingsboardApiAsset, + thingsboardApiAttribute, + thingsboardApiEntity, uiRouter]) .config(AppConfig) .factory('globalInterceptor', GlobalInterceptor) diff --git a/ui/src/app/asset/add-asset.tpl.html b/ui/src/app/asset/add-asset.tpl.html new file mode 100644 index 0000000000..ce22e4e674 --- /dev/null +++ b/ui/src/app/asset/add-asset.tpl.html @@ -0,0 +1,45 @@ + + +
+ +
+

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..0bc73d1343 --- /dev/null +++ b/ui/src/app/common/dashboard-utils.service.js @@ -0,0 +1,107 @@ +/* + * 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, timeService) { + + var service = { + validateAndUpdateDashboard: validateAndUpdateDashboard + }; + + 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; + } + }); + } + + function validateAndUpdateDashboard(dashboard) { + if (!dashboard.configuration) { + dashboard.configuration = { + widgets: [], + entityAliases: {} + }; + } + if (angular.isUndefined(dashboard.configuration.widgets)) { + dashboard.configuration.widgets = []; + } + dashboard.configuration.widgets.forEach(function(widget) { + validateAndUpdateWidget(widget); + }); + if (angular.isUndefined(dashboard.configuration.timewindow)) { + dashboard.configuration.timewindow = timeService.defaultTimewindow(); + } + if (angular.isDefined(dashboard.configuration.gridSettings)) { + if (angular.isDefined(dashboard.configuration.gridSettings.showDevicesSelect)) { + dashboard.configuration.gridSettings.showEntitiesSelect = dashboard.configuration.gridSettings.showDevicesSelect; + delete dashboard.configuration.gridSettings.showDevicesSelect; + } + } + dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration); + return dashboard; + } +} diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index df321f795b..ae3556fea0 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,12 @@ 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" }, eventType: { alarm: { @@ -122,7 +123,7 @@ export default angular.module('thingsboard.types', []) name: "attribute.scope-latest-telemetry", clientSide: true }, - deviceAttributesScope: { + attributesScope: { client: { value: "CLIENT_SCOPE", name: "attribute.scope-client", 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.directive.js b/ui/src/app/components/dashboard.directive.js index 131616be0c..b20c03949f 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -272,8 +272,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t $scope.$broadcast('toggleDashboardEditMode', vm.isEdit); }); - $scope.$watch('vm.aliasesInfo.deviceAliases', function () { - $scope.$broadcast('deviceAliasListChanged', vm.aliasesInfo); + $scope.$watch('vm.aliasesInfo.entityAliases', function () { + $scope.$broadcast('entityAliasListChanged', vm.aliasesInfo); }, true); $scope.$on('gridster-resized', function (event, sizes, theGridster) { 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/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index bcffa721cc..6c7c472e99 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, @@ -127,9 +127,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti } 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 (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; } @@ -269,7 +269,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 +368,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..ec5e7ed931 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -21,7 +21,7 @@ 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, + datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow, dashboardTimewindowApi, widget, aliasesInfo, 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: "" }, ..] } @@ -231,7 +232,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 +342,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 +397,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..de3bf959d0 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,8 +34,8 @@ 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; @@ -90,45 +90,46 @@ export default function AddWidgetController($scope, widgetService, deviceService } } - 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..ee107cc4be 100644 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ b/ui/src/app/dashboard/dashboard-settings.controller.js @@ -31,8 +31,8 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti vm.gridSettings.showTitle = true; } - if (angular.isUndefined(vm.gridSettings.showDevicesSelect)) { - vm.gridSettings.showDevicesSelect = true; + if (angular.isUndefined(vm.gridSettings.showEntitiesSelect)) { + vm.gridSettings.showEntitiesSelect = true; } if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) { diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html index ec6f28b369..6ae47465d9 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -49,8 +49,8 @@ >
- {{ 'dashboard.display-device-selection' | translate }} + {{ 'dashboard.display-entities-selection' | translate }} {{ 'dashboard.display-dashboard-timewindow' | translate }} diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index 9121f6aed8..79ee6aeeb4 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -15,15 +15,15 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import deviceAliasesTemplate from './device-aliases.tpl.html'; +import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; import dashboardBackgroundTemplate from './dashboard-settings.tpl.html'; import addWidgetTemplate from './add-widget.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, +export default function DashboardController(types, dashboardUtils, widgetService, userService, + dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope, $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { var vm = this; @@ -98,7 +98,7 @@ export default function DashboardController(types, widgetService, userService, vm.showDashboardToolbar = showDashboardToolbar; vm.onAddWidgetClosed = onAddWidgetClosed; vm.onEditWidgetClosed = onEditWidgetClosed; - vm.openDeviceAliases = openDeviceAliases; + vm.openEntityAliases = openEntityAliases; vm.openDashboardSettings = openDashboardSettings; vm.removeWidget = removeWidget; vm.saveDashboard = saveDashboard; @@ -109,7 +109,7 @@ export default function DashboardController(types, widgetService, userService, vm.displayTitle = displayTitle; vm.displayExport = displayExport; vm.displayDashboardTimewindow = displayDashboardTimewindow; - vm.displayDevicesSelect = displayDevicesSelect; + vm.displayEntitiesSelect = displayEntitiesSelect; vm.widgetsBundle; @@ -229,21 +229,8 @@ export default function DashboardController(types, widgetService, userService, 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()) { @@ -303,26 +290,26 @@ export default function DashboardController(types, widgetService, userService, return vm.dashboardInitComplete; } - 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), + entityAliases: angular.copy(vm.dashboard.configuration.entityAliases), widgets: vm.widgets, - isSingleDeviceAlias: false, - singleDeviceAlias: null + 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 () { }); } @@ -392,7 +379,7 @@ export default function DashboardController(types, widgetService, userService, function importWidget($event) { $event.stopPropagation(); - importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated); + importExport.importWidget($event, vm.dashboard, entityAliasesUpdated); } function widgetMouseDown($event, widget) { @@ -473,9 +460,9 @@ export default function DashboardController(types, widgetService, userService, ); dashboardContextActions.push( { - action: openDeviceAliases, + action: openEntityAliases, enabled: true, - value: "device.aliases", + value: "entity.aliases", icon: "devices_other" } ); @@ -494,7 +481,7 @@ export default function DashboardController(types, widgetService, userService, function pasteWidget($event) { var pos = vm.dashboardContainer.getEventGridPosition($event); - itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated); + itembuffer.pasteWidget(vm.dashboard, pos, entityAliasesUpdated); } function prepareWidgetContextMenu() { @@ -588,10 +575,10 @@ export default function DashboardController(types, widgetService, userService, } } - function displayDevicesSelect() { + function displayEntitiesSelect() { if (vm.dashboard && vm.dashboard.configuration.gridSettings && - angular.isDefined(vm.dashboard.configuration.gridSettings.showDevicesSelect)) { - return vm.dashboard.configuration.gridSettings.showDevicesSelect; + angular.isDefined(vm.dashboard.configuration.gridSettings.showEntitiesSelect)) { + return vm.dashboard.configuration.gridSettings.showEntitiesSelect; } else { return true; } @@ -739,7 +726,7 @@ export default function DashboardController(types, widgetService, userService, vm.dashboard = vm.prevDashboard; vm.widgets = vm.dashboard.configuration.widgets; vm.dashboardConfiguration = vm.dashboard.configuration; - deviceAliasesUpdated(); + entityAliasesUpdated(); } } } @@ -768,8 +755,8 @@ export default function DashboardController(types, widgetService, userService, $mdDialog.show(alert); } - function deviceAliasesUpdated() { - deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases) + function entityAliasesUpdated() { + entityService.processEntityAliases(vm.dashboard.configuration.entityAliases) .then( function(resolution) { if (resolution.aliasesInfo) { diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index 9276e0f17c..3286f8d867 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -63,17 +63,17 @@ tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow"> - - - + ng-model="vm.aliasesInfo.entityAliases" + entity-aliases-info="vm.aliasesInfo.entityAliasesInfo"> + + - {{ 'device.aliases' | translate }} + {{ 'entity.aliases' | translate }} - devices_other + devices_other diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index ced39f57d6..1256065860 100644 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ b/ui/src/app/dashboard/edit-widget.directive.js @@ -15,13 +15,13 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import deviceAliasesTemplate from './device-aliases.tpl.html'; +import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; import editWidgetTemplate from './edit-widget.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function EditWidgetDirective($compile, $templateCache, widgetService, deviceService, $q, $document, $mdDialog) { +export default function EditWidgetDirective($compile, $templateCache, types, widgetService, entityService, $q, $document, $mdDialog) { var linker = function (scope, element) { var template = $templateCache.get(editWidgetTemplate); @@ -58,45 +58,46 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ } }); - scope.fetchDeviceKeys = function (deviceAliasId, query, type) { - var deviceAlias = scope.aliasesInfo.deviceAliases[deviceAliasId]; - if (deviceAlias && deviceAlias.deviceId) { - return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type); + scope.fetchEntityKeys = function (entityAliasId, query, type) { + var entityAlias = scope.aliasesInfo.entityAliases[entityAliasId]; + if (entityAlias && entityAlias.entityId) { + return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type); } else { return $q.when([]); } }; - scope.createDeviceAlias = function (event, alias) { + scope.createEntityAlias = function (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(scope.dashboard.configuration.deviceAliases), + entityAliases: angular.copy(scope.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) { - scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] = - { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter }; - deviceService.processDeviceAliases(scope.dashboard.configuration.deviceAliases).then( + }).then(function (singleEntityAlias) { + scope.dashboard.configuration.entityAliases[singleEntityAlias.id] = + { alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter }; + entityService.processEntityAliases(scope.dashboard.configuration.entityAliases).then( function(resolution) { if (!resolution.error) { scope.aliasesInfo = resolution.aliasesInfo; } - deferred.resolve(singleDeviceAlias); + deferred.resolve(singleEntityAlias); } ); }, function () { diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html index 1aaf861d0e..279d3113a8 100644 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ b/ui/src/app/dashboard/edit-widget.tpl.html @@ -21,9 +21,9 @@ is-data-enabled="isDataEnabled" widget-settings-schema="settingsSchema" datakey-settings-schema="dataKeySettingsSchema" - device-aliases="aliasesInfo.deviceAliases" + entity-aliases="aliasesInfo.entityAliases" functions-only="functionsOnly" - fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)" - on-create-device-alias="createDeviceAlias(event, alias)" + fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" + on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" the-form="theForm"> diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js index 88d71fb0e2..c8ba734a75 100644 --- a/ui/src/app/dashboard/index.js +++ b/ui/src/app/dashboard/index.js @@ -24,7 +24,6 @@ import thingsboardApiUser from '../api/user.service'; import thingsboardApiDashboard from '../api/dashboard.service'; import thingsboardApiCustomer from '../api/customer.service'; import thingsboardDetailsSidenav from '../components/details-sidenav.directive'; -import thingsboardDeviceFilter from '../components/device-filter.directive'; import thingsboardWidgetConfig from '../components/widget-config.directive'; import thingsboardDashboardSelect from '../components/dashboard-select.directive'; import thingsboardDashboard from '../components/dashboard.directive'; @@ -38,15 +37,12 @@ import thingsboardImportExport from '../import-export'; import DashboardRoutes from './dashboard.routes'; import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller'; import DashboardController from './dashboard.controller'; -import DeviceAliasesController from './device-aliases.controller'; -import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller'; import DashboardSettingsController from './dashboard-settings.controller'; import AssignDashboardToCustomerController from './assign-to-customer.controller'; import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller'; import AddWidgetController from './add-widget.controller'; import DashboardDirective from './dashboard.directive'; import EditWidgetDirective from './edit-widget.directive'; -import AliasesDeviceSelectDirective from './aliases-device-select.directive'; export default angular.module('thingsboard.dashboard', [ uiRouter, @@ -60,7 +56,6 @@ export default angular.module('thingsboard.dashboard', [ thingsboardApiDashboard, thingsboardApiCustomer, thingsboardDetailsSidenav, - thingsboardDeviceFilter, thingsboardWidgetConfig, thingsboardDashboardSelect, thingsboardDashboard, @@ -73,13 +68,10 @@ export default angular.module('thingsboard.dashboard', [ .controller('DashboardCardController', DashboardCardController) .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController) .controller('DashboardController', DashboardController) - .controller('DeviceAliasesController', DeviceAliasesController) - .controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController) .controller('DashboardSettingsController', DashboardSettingsController) .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController) .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController) .controller('AddWidgetController', AddWidgetController) .directive('tbDashboardDetails', DashboardDirective) .directive('tbEditWidget', EditWidgetDirective) - .directive('tbAliasesDeviceSelect', AliasesDeviceSelectDirective) .name; diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index 90909e31aa..2f90db814e 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -34,15 +34,17 @@ + entity-id="vm.grid.operatingItem().id.id" + entity-type="{{vm.types.entityType.device}}" + entity-name="vm.grid.operatingItem().name" + default-attribute-scope="{{vm.types.attributesScope.client.value}}"> diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js index b84497aa21..ea42ee8e95 100644 --- a/ui/src/app/device/index.js +++ b/ui/src/app/device/index.js @@ -25,10 +25,7 @@ import {DeviceController, DeviceCardController} from './device.controller'; import AssignDeviceToCustomerController from './assign-to-customer.controller'; import AddDevicesToCustomerController from './add-devices-to-customer.controller'; import ManageDeviceCredentialsController from './device-credentials.controller'; -import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; -import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; import DeviceDirective from './device.directive'; -import AttributeTableDirective from './attribute/attribute-table.directive'; export default angular.module('thingsboard.device', [ uiRouter, @@ -44,8 +41,5 @@ export default angular.module('thingsboard.device', [ .controller('AssignDeviceToCustomerController', AssignDeviceToCustomerController) .controller('AddDevicesToCustomerController', AddDevicesToCustomerController) .controller('ManageDeviceCredentialsController', ManageDeviceCredentialsController) - .controller('AddAttributeDialogController', AddAttributeDialogController) - .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) .directive('tbDevice', DeviceDirective) - .directive('tbAttributeTable', AttributeTableDirective) .name; diff --git a/ui/src/app/entity/aliases-entity-select-button.tpl.html b/ui/src/app/entity/aliases-entity-select-button.tpl.html new file mode 100644 index 0000000000..b0b69f5774 --- /dev/null +++ b/ui/src/app/entity/aliases-entity-select-button.tpl.html @@ -0,0 +1,32 @@ + + +
+ + + {{ 'entity.select-entities' | translate }} + + devices_other + + + + {{ 'entity.select-entities' | translate }} + + {{displayValue}} + +
diff --git a/ui/src/app/entity/aliases-entity-select-panel.controller.js b/ui/src/app/entity/aliases-entity-select-panel.controller.js new file mode 100644 index 0000000000..4128f948d5 --- /dev/null +++ b/ui/src/app/entity/aliases-entity-select-panel.controller.js @@ -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. + */ + +/*@ngInject*/ +export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, types, entityAliases, entityAliasesInfo, onEntityAliasesUpdate) { + + var vm = this; + vm._mdPanelRef = mdPanelRef; + vm.entityAliases = entityAliases; + vm.entityAliasesInfo = entityAliasesInfo; + vm.onEntityAliasesUpdate = onEntityAliasesUpdate; + + $scope.$watch('vm.entityAliases', function () { + if (onEntityAliasesUpdate) { + onEntityAliasesUpdate(vm.entityAliases); + } + }, true); +} diff --git a/ui/src/app/entity/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/aliases-entity-select-panel.tpl.html new file mode 100644 index 0000000000..d9c5f2ff11 --- /dev/null +++ b/ui/src/app/entity/aliases-entity-select-panel.tpl.html @@ -0,0 +1,33 @@ + + +
+ +
+ + + + + {{entityInfo.name}} + + + +
+
+
+
diff --git a/ui/src/app/entity/aliases-entity-select.directive.js b/ui/src/app/entity/aliases-entity-select.directive.js new file mode 100644 index 0000000000..7512409f12 --- /dev/null +++ b/ui/src/app/entity/aliases-entity-select.directive.js @@ -0,0 +1,154 @@ +/* + * 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 './aliases-entity-select.scss'; + +import $ from 'jquery'; + +/* eslint-disable import/no-unresolved, import/default */ + +import aliasesEntitySelectButtonTemplate from './aliases-entity-select-button.tpl.html'; +import aliasesEntitySelectPanelTemplate from './aliases-entity-select-panel.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/* eslint-disable angular/angularelement */ +/*@ngInject*/ +export default function AliasesEntitySelectDirective($compile, $templateCache, $mdMedia, types, $mdPanel, $document, $translate) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + + /* tbAliasesEntitySelect (ng-model) + * { + * "aliasId": { + * alias: alias, + * entityType: entityType, + * entityId: entityId + * } + * } + */ + + var template = $templateCache.get(aliasesEntitySelectButtonTemplate); + + scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top'; + + element.html(template); + + scope.openEditMode = function (event) { + if (scope.disabled) { + return; + } + var position; + var panelHeight = $mdMedia('min-height: 350px') ? 250 : 150; + var panelWidth = 300; + var offset = element[0].getBoundingClientRect(); + var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line + var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line + var yPosition; + var xPosition; + if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line + yPosition = $mdPanel.yPosition.ABOVE; + } else { + yPosition = $mdPanel.yPosition.BELOW; + } + if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line + xPosition = $mdPanel.xPosition.CENTER; + } else { + xPosition = $mdPanel.xPosition.ALIGN_START; + } + position = $mdPanel.newPanelPosition() + .relativeTo(element) + .addPanelPosition(xPosition, yPosition); + var config = { + attachTo: angular.element($document[0].body), + controller: 'AliasesEntitySelectPanelController', + controllerAs: 'vm', + templateUrl: aliasesEntitySelectPanelTemplate, + panelClass: 'tb-aliases-entity-select-panel', + position: position, + fullscreen: false, + locals: { + 'entityAliases': angular.copy(scope.model), + 'entityAliasesInfo': scope.entityAliasesInfo, + 'onEntityAliasesUpdate': function (entityAliases) { + scope.model = entityAliases; + scope.updateView(); + } + }, + openFrom: event, + clickOutsideToClose: true, + escapeToClose: true, + focusOnOpen: false + }; + $mdPanel.open(config); + } + + scope.updateView = function () { + var value = angular.copy(scope.model); + ngModelCtrl.$setViewValue(value); + updateDisplayValue(); + } + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + var value = ngModelCtrl.$viewValue; + scope.model = angular.copy(value); + updateDisplayValue(); + } + } + + function updateDisplayValue() { + var displayValue; + var singleValue = true; + var currentAliasId; + for (var aliasId in scope.model) { + if (!currentAliasId) { + currentAliasId = aliasId; + } else { + singleValue = false; + break; + } + } + if (singleValue && currentAliasId) { + var entityId = scope.model[currentAliasId].entityId; + var entitiesInfo = scope.entityAliasesInfo[currentAliasId]; + for (var i=0;i + +
+ +
+

attribute.add

+ + + + +
+
+ + + +
+ +
+ + + +
+
attribute.key-required
+
+
+
+ + + + + + {{type.name | translate}} + + + + + + +
+
attribute.value-required
+
+
+ + + +
+
attribute.value-required
+
value.invalid-integer-value
+
+
+ + + +
+
attribute.value-required
+
+
+
+ + {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }} + +
+
+
+
+
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js new file mode 100644 index 0000000000..0e07cb4957 --- /dev/null +++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js @@ -0,0 +1,74 @@ +/* + * 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 AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, entityId, entityType, entityName, widget) { + + var vm = this; + + vm.widget = widget; + vm.dashboardId = null; + vm.addToDashboardType = 0; + vm.newDashboard = {}; + vm.openDashboard = false; + + vm.add = add; + vm.cancel = cancel; + + function cancel() { + $mdDialog.cancel(); + } + + function add() { + $scope.theForm.$setPristine(); + if (vm.addToDashboardType === 0) { + dashboardService.getDashboard(vm.dashboardId).then( + function success(dashboard) { + addWidgetToDashboard(dashboard); + }, + function fail() {} + ); + } else { + addWidgetToDashboard(vm.newDashboard); + } + + } + + function addWidgetToDashboard(theDashboard) { + var aliasesInfo = { + datasourceAliases: {}, + targetDeviceAliases: {} + }; + aliasesInfo.datasourceAliases[0] = { + aliasName: entityName, + entityType: entityType, + entityFilter: { + useFilter: false, + entityNameFilter: '', + entityList: [entityId] + } + }; + theDashboard = itembuffer.addWidgetToDashboard(theDashboard, vm.widget, aliasesInfo, null, 48, -1, -1); + dashboardService.saveDashboard(theDashboard).then( + function success(dashboard) { + $mdDialog.hide(); + if (vm.openDashboard) { + $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id}); + } + } + ); + } + +} diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html new file mode 100644 index 0000000000..eb16077422 --- /dev/null +++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html @@ -0,0 +1,81 @@ + + +
+ +
+

attribute.add-widget-to-dashboard

+ + + + +
+
+ + + +
+ +
+ + +
+ dashboard.select-existing + + +
+
+ +
+ dashboard.create-new + + + +
+
dashboard.title-required
+
+
+
+
+
+
+
+
+
+ + + + {{ 'dashboard.open-dashboard' | translate }} + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js new file mode 100644 index 0000000000..e05fcb64af --- /dev/null +++ b/ui/src/app/entity/attribute/attribute-table.directive.js @@ -0,0 +1,433 @@ +/* + * 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 'angular-material-data-table/dist/md-data-table.min.css'; +import './attribute-table.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import attributeTableTemplate from './attribute-table.tpl.html'; +import addAttributeDialogTemplate from './add-attribute-dialog.tpl.html'; +import addWidgetToDashboardDialogTemplate from './add-widget-to-dashboard-dialog.tpl.html'; +import editAttributeValueTemplate from './edit-attribute-value.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import EditAttributeValueController from './edit-attribute-value.controller'; + +/*@ngInject*/ +export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, + $document, $translate, $filter, utils, types, dashboardService, attributeService, widgetService) { + + var linker = function (scope, element, attrs) { + + var template = $templateCache.get(attributeTableTemplate); + + element.html(template); + + var getAttributeScopeByValue = function(attributeScopeValue) { + if (scope.types.latestTelemetry.value === attributeScopeValue) { + return scope.types.latestTelemetry; + } + for (var attrScope in scope.attributeScopes) { + if (scope.attributeScopes[attrScope].value === attributeScopeValue) { + return scope.attributeScopes[attrScope]; + } + } + } + + scope.types = types; + + scope.entityType = attrs.entityType; + scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope); + + if (scope.entityType === types.entityType.device) { + scope.attributeScopes = types.attributesScope; + scope.attributeScopeSelectionReadonly = false; + } else { + scope.attributeScopes = {}; + scope.attributeScopes.server = types.attributesScope.server; + scope.attributeScopeSelectionReadonly = true; + if (scope.attributeScope != types.latestTelemetry) { + scope.attributeScope = scope.attributeScopes.server.value; + } + } + + scope.attributes = { + count: 0, + data: [] + }; + + scope.selectedAttributes = []; + scope.mode = 'default'; // 'widget' + scope.subscriptionId = null; + + scope.query = { + order: 'key', + limit: 5, + page: 1, + search: null + }; + + scope.$watch("entityId", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + scope.resetFilter(); + scope.getEntityAttributes(false, true); + } + }); + + scope.$watch("attributeScope", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + scope.mode = 'default'; + scope.query.search = null; + scope.selectedAttributes = []; + scope.getEntityAttributes(false, true); + } + }); + + scope.resetFilter = function() { + scope.mode = 'default'; + scope.query.search = null; + scope.selectedAttributes = []; + scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope); + } + + scope.enterFilterMode = function() { + scope.query.search = ''; + } + + scope.exitFilterMode = function() { + scope.query.search = null; + scope.getEntityAttributes(); + } + + scope.$watch("query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && scope.query.search != null) { + scope.getEntityAttributes(); + } + }); + + function success(attributes, update, apply) { + scope.attributes = attributes; + if (!update) { + scope.selectedAttributes = []; + } + if (apply) { + scope.$digest(); + } + } + + scope.onReorder = function() { + scope.getEntityAttributes(false, false); + } + + scope.onPaginate = function() { + scope.getEntityAttributes(false, false); + } + + scope.getEntityAttributes = function(forceUpdate, reset) { + if (scope.attributesDeferred) { + scope.attributesDeferred.resolve(); + } + if (scope.entityId && scope.entityType && scope.attributeScope) { + if (reset) { + scope.attributes = { + count: 0, + data: [] + }; + } + scope.checkSubscription(); + scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, + scope.query, function(attributes, update, apply) { + success(attributes, update || forceUpdate, apply); + } + ); + } else { + var deferred = $q.defer(); + scope.attributesDeferred = deferred; + success({ + count: 0, + data: [] + }); + deferred.resolve(); + } + } + + scope.checkSubscription = function() { + var newSubscriptionId = null; + if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') { + newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value); + } + if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) { + attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); + } + scope.subscriptionId = newSubscriptionId; + } + + scope.$on('$destroy', function() { + if (scope.subscriptionId) { + attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); + } + }); + + scope.editAttribute = function($event, attribute) { + if (!scope.attributeScope.clientSide) { + $event.stopPropagation(); + $mdEditDialog.show({ + controller: EditAttributeValueController, + templateUrl: editAttributeValueTemplate, + locals: {attributeValue: attribute.value, + save: function (model) { + var updatedAttribute = angular.copy(attribute); + updatedAttribute.value = model.value; + attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then( + function success() { + scope.getEntityAttributes(); + } + ); + }}, + targetEvent: $event + }); + } + } + + scope.addAttribute = function($event) { + if (!scope.attributeScope.clientSide) { + $event.stopPropagation(); + $mdDialog.show({ + controller: 'AddAttributeDialogController', + controllerAs: 'vm', + templateUrl: addAttributeDialogTemplate, + parent: angular.element($document[0].body), + locals: {entityType: scope.entityType, entityId: scope.entityId, attributeScope: scope.attributeScope.value}, + fullscreen: true, + targetEvent: $event + }).then(function () { + scope.getEntityAttributes(); + }); + } + } + + scope.deleteAttributes = function($event) { + if (!scope.attributeScope.clientSide) { + $event.stopPropagation(); + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('attribute.delete-attributes-title', {count: scope.selectedAttributes.length}, 'messageformat')) + .htmlContent($translate.instant('attribute.delete-attributes-text')) + .ariaLabel($translate.instant('attribute.delete-attributes')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then( + function success() { + scope.selectedAttributes = []; + scope.getEntityAttributes(); + } + ) + }); + } + } + + scope.nextWidget = function() { + if (scope.widgetsCarousel.index < scope.widgetsList.length-1) { + scope.widgetsCarousel.index++; + } + } + + scope.prevWidget = function() { + if (scope.widgetsCarousel.index > 0) { + scope.widgetsCarousel.index--; + } + } + + scope.enterWidgetMode = function() { + + if (scope.widgetsIndexWatch) { + scope.widgetsIndexWatch(); + scope.widgetsIndexWatch = null; + } + + if (scope.widgetsBundleWatch) { + scope.widgetsBundleWatch(); + scope.widgetsBundleWatch = null; + } + + scope.mode = 'widget'; + scope.checkSubscription(); + scope.widgetsList = []; + scope.widgetsListCache = []; + scope.widgetsLoaded = false; + scope.widgetsCarousel = { + index: 0 + } + scope.widgetsBundle = null; + scope.firstBundle = true; + scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards; + + scope.aliasesInfo = { + entityAliases: { + '1': {alias: scope.entityName, entityType: scope.entityType, entityId: scope.entityId} + }, + entityAliasesInfo: { + '1': [ + {name: scope.entityName, entityType: scope.entityType, id: scope.entityId} + ] + } + }; + + var dataKeyType = scope.attributeScope === types.latestTelemetry ? + types.dataKeyType.timeseries : types.dataKeyType.attribute; + + var datasource = { + type: types.datasourceType.entity, + entityAliasId: '1', + dataKeys: [] + } + var i = 0; + for (var attr =0; attr < scope.selectedAttributes.length;attr++) { + var attribute = scope.selectedAttributes[attr]; + var dataKey = { + name: attribute.key, + label: attribute.key, + type: dataKeyType, + color: utils.getMaterialColor(i), + settings: {}, + _hash: Math.random() + } + datasource.dataKeys.push(dataKey); + i++; + } + + scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) { + if (scope.mode === 'widget' && (newVal != prevVal)) { + var index = scope.widgetsCarousel.index; + for (var i = 0; i < scope.widgetsList.length; i++) { + scope.widgetsList[i].splice(0, scope.widgetsList[i].length); + if (i === index) { + scope.widgetsList[i].push(scope.widgetsListCache[i][0]); + } + } + } + }); + + scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) { + if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) { + scope.widgetsList = []; + scope.widgetsListCache = []; + scope.widgetsCarousel.index = 0; + scope.firstBundle = false; + if (scope.widgetsBundle) { + scope.widgetsLoaded = false; + var bundleAlias = scope.widgetsBundle.alias; + var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid; + widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( + function success(widgetTypes) { + + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']); + + for (var i = 0; i < widgetTypes.length; i++) { + var widgetType = widgetTypes[i]; + var widgetInfo = widgetService.toWidgetInfo(widgetType); + if (widgetInfo.type !== types.widgetType.static.value) { + var sizeX = widgetInfo.sizeX * 2; + var sizeY = widgetInfo.sizeY * 2; + var col = Math.floor(Math.max(0, (20 - sizeX) / 2)); + var widget = { + isSystemType: isSystem, + bundleAlias: bundleAlias, + typeAlias: widgetInfo.alias, + type: widgetInfo.type, + title: widgetInfo.widgetName, + sizeX: sizeX, + sizeY: sizeY, + row: 0, + col: col, + config: angular.fromJson(widgetInfo.defaultConfig) + }; + + widget.config.title = widgetInfo.widgetName; + widget.config.datasources = [datasource]; + var length; + if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) { + length = scope.widgetsListCache.push([widget]); + scope.widgetsList.push(length === 1 ? [widget] : []); + } else if (widgetInfo.type === types.widgetType.latest.value) { + length = scope.widgetsListCache.push([widget]); + scope.widgetsList.push(length === 1 ? [widget] : []); + } + } + } + scope.widgetsLoaded = true; + } + ); + } + } + }); + } + + scope.exitWidgetMode = function() { + if (scope.widgetsBundleWatch) { + scope.widgetsBundleWatch(); + scope.widgetsBundleWatch = null; + } + if (scope.widgetsIndexWatch) { + scope.widgetsIndexWatch(); + scope.widgetsIndexWatch = null; + } + scope.selectedWidgetsBundleAlias = null; + scope.mode = 'default'; + scope.getEntityAttributes(true); + } + + scope.getServerTimeDiff = function() { + return dashboardService.getServerTimeDiff(); + } + + scope.addWidgetToDashboard = function($event) { + if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) { + var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0]; + $event.stopPropagation(); + $mdDialog.show({ + controller: 'AddWidgetToDashboardDialogController', + controllerAs: 'vm', + templateUrl: addWidgetToDashboardDialogTemplate, + parent: angular.element($document[0].body), + locals: {entityId: scope.entityId, entityType: scope.entityType, entityName: scope.entityName, widget: angular.copy(widget)}, + fullscreen: true, + targetEvent: $event + }).then(function () { + + }); + } + } + + scope.loading = function() { + return $rootScope.loading; + } + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + link: linker, + scope: { + entityId: '=', + entityName: '=', + disableAttributeScopeSelection: '@?' + } + }; +} diff --git a/ui/src/app/entity/attribute/attribute-table.scss b/ui/src/app/entity/attribute/attribute-table.scss new file mode 100644 index 0000000000..3358230654 --- /dev/null +++ b/ui/src/app/entity/attribute/attribute-table.scss @@ -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. + */ +@import '../../../scss/constants'; + +$md-light: rgba(255, 255, 255, 100%); +$md-edit-icon-fill: #757575; + +md-toolbar.md-table-toolbar.alternate { + .md-toolbar-tools { + md-icon { + color: $md-light; + } + } +} + +.md-table { + .md-cell { + ng-md-icon { + fill: $md-edit-icon-fill; + float: right; + height: 16px; + } + } +} + +.widgets-carousel { + position: relative; + margin: 0px; + + min-height: 150px !important; + + tb-dashboard { + #gridster-parent { + padding: 0 7px; + } + } +} \ No newline at end of file diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html new file mode 100644 index 0000000000..bf3b8cd22c --- /dev/null +++ b/ui/src/app/entity/attribute/attribute-table.tpl.html @@ -0,0 +1,210 @@ + + +
+ + + + + {{scope.name | translate}} + + + +
+
+ +
+ {{ attributeScope.name }} + + + add + + {{ 'action.add' | translate }} + + + + search + + {{ 'action.search' | translate }} + + + + refresh + + {{ 'action.refresh' | translate }} + + +
+
+ +
+ + search + + {{ 'action.search' | translate }} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ +
+ + + + delete + + {{ 'action.delete' | translate }} + + + + + {{ 'attribute.show-on-widget' | translate }} + + now_widgets + attribute.show-on-widget + +
+
+ +
+
+ {{ 'widgets-bundle.current' | translate }} + + +
+ + + {{ 'attribute.add-to-dashboard' | translate }} + + dashboard + attribute.add-to-dashboard + + + close + + {{ 'action.close' | translate }} + + +
+
+ + + + + + + + + + + + + + + + +
Last update timeKeyValue
{{attribute.lastUpdateTs | date : 'yyyy-MM-dd HH:mm:ss'}}{{attribute.key}} + {{attribute.value}} + +
+
+ + + +
+
diff --git a/ui/src/app/entity/attribute/edit-attribute-value.controller.js b/ui/src/app/entity/attribute/edit-attribute-value.controller.js new file mode 100644 index 0000000000..fba5b4d0b9 --- /dev/null +++ b/ui/src/app/entity/attribute/edit-attribute-value.controller.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. + */ +/*@ngInject*/ +export default function EditAttributeValueController($scope, $q, $element, types, attributeValue, save) { + + $scope.valueTypes = types.valueType; + + $scope.model = {}; + + $scope.model.value = attributeValue; + + if ($scope.model.value === true || $scope.model.value === false) { + $scope.valueType = types.valueType.boolean; + } else if (angular.isNumber($scope.model.value)) { + if ($scope.model.value.toString().indexOf('.') == -1) { + $scope.valueType = types.valueType.integer; + } else { + $scope.valueType = types.valueType.double; + } + } else { + $scope.valueType = types.valueType.string; + } + + $scope.submit = submit; + $scope.dismiss = dismiss; + + function dismiss() { + $element.remove(); + } + + function update() { + if($scope.editDialog.$invalid) { + return $q.reject(); + } + + if(angular.isFunction(save)) { + return $q.when(save($scope.model)); + } + + return $q.resolve(); + } + + function submit() { + update().then(function () { + $scope.dismiss(); + }); + } + + $scope.$watch('valueType', function(newVal, prevVal) { + if (newVal != prevVal) { + if ($scope.valueType === types.valueType.boolean) { + $scope.model.value = false; + } else { + $scope.model.value = null; + } + } + }); +} diff --git a/ui/src/app/entity/attribute/edit-attribute-value.tpl.html b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html new file mode 100644 index 0000000000..8ce8d81f73 --- /dev/null +++ b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html @@ -0,0 +1,72 @@ + + +
+
+
+
+ + + + + + {{type.name | translate}} + + + + + + +
+
attribute.value-required
+
+
+ + + +
+
attribute.value-required
+
value.invalid-integer-value
+
+
+ + + +
+
attribute.value-required
+
+
+
+ + {{ (model.value ? 'value.true' : 'value.false') | translate }} + +
+
+
+
+
+ {{ 'action.cancel' | + translate }} + + + {{ 'action.update' | translate }} + +
+
+
\ No newline at end of file diff --git a/ui/src/app/entity/entity-aliases.controller.js b/ui/src/app/entity/entity-aliases.controller.js new file mode 100644 index 0000000000..4eb1737c0a --- /dev/null +++ b/ui/src/app/entity/entity-aliases.controller.js @@ -0,0 +1,221 @@ +/* + * 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 './entity-aliases.scss'; + +/*@ngInject*/ +export default function EntityAliasesController(utils, entityService, toast, $scope, $mdDialog, $document, $q, $translate, + types, config) { + + var vm = this; + + vm.isSingleEntityAlias = config.isSingleEntityAlias; + vm.singleEntityAlias = config.singleEntityAlias; + vm.entityAliases = []; + vm.title = config.customTitle ? config.customTitle : 'entity.aliases'; + vm.disableAdd = config.disableAdd; + vm.aliasToWidgetsMap = {}; + vm.allowedEntityTypes = config.allowedEntityTypes; + + vm.onFilterEntityChanged = onFilterEntityChanged; + vm.addAlias = addAlias; + vm.removeAlias = removeAlias; + + vm.cancel = cancel; + vm.save = save; + + initController(); + + function initController() { + var aliasId; + if (config.widgets) { + var widgetsTitleList, widget; + if (config.isSingleWidget && config.widgets.length == 1) { + widget = config.widgets[0]; + widgetsTitleList = [widget.config.title]; + for (aliasId in config.entityAliases) { + vm.aliasToWidgetsMap[aliasId] = widgetsTitleList; + } + } else { + for (var w in config.widgets) { + widget = config.widgets[w]; + if (widget.type === types.widgetType.rpc.value) { + if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { + var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; + widgetsTitleList = vm.aliasToWidgetsMap[targetDeviceAliasId]; + if (!widgetsTitleList) { + widgetsTitleList = []; + vm.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; + } + widgetsTitleList.push(widget.config.title); + } + } else { + var datasources = utils.validateDatasources(widget.config.datasources); + for (var i=0;i -1) { + var widgetsTitleList = vm.aliasToWidgetsMap[entityAlias.id]; + if (widgetsTitleList) { + var widgetsListHtml = ''; + for (var t in widgetsTitleList) { + widgetsListHtml += '
\'' + widgetsTitleList[t] + '\''; + } + var alert = $mdDialog.alert() + .parent(angular.element($document[0].body)) + .clickOutsideToClose(true) + .title($translate.instant('entity.unable-delete-entity-alias-title')) + .htmlContent($translate.instant('entity.unable-delete-entity-alias-text', {entityAlias: entityAlias.alias, widgetsList: widgetsListHtml})) + .ariaLabel($translate.instant('entity.unable-delete-entity-alias-title')) + .ok($translate.instant('action.close')) + .targetEvent($event); + alert._options.skipHide = true; + alert._options.fullscreen = true; + + $mdDialog.show(alert); + } else { + vm.entityAliases.splice(index, 1); + if ($scope.theForm) { + $scope.theForm.$setDirty(); + } + } + } + } + + function cancel() { + $mdDialog.cancel(); + } + + function cleanupEntityFilter(entityFilter) { + if (entityFilter.useFilter) { + entityFilter.entityList = []; + } else { + entityFilter.entityNameFilter = ''; + } + return entityFilter; + } + + function save() { + + var entityAliases = {}; + var uniqueAliasList = {}; + + var valid = true; + var aliasId, maxAliasId; + var alias; + var i; + + if (vm.isSingleEntityAlias) { + maxAliasId = 0; + vm.singleEntityAlias.entityFilter = cleanupEntityFilter(vm.singleEntityAlias.entityFilter); + for (i = 0; i < vm.entityAliases.length; i ++) { + aliasId = vm.entityAliases[i].id; + alias = vm.entityAliases[i].alias; + if (alias === vm.singleEntityAlias.alias) { + valid = false; + break; + } + maxAliasId = Math.max(aliasId, maxAliasId); + } + maxAliasId++; + vm.singleEntityAlias.id = maxAliasId; + } else { + for (i = 0; i < vm.entityAliases.length; i++) { + aliasId = vm.entityAliases[i].id; + alias = vm.entityAliases[i].alias; + if (!uniqueAliasList[alias]) { + uniqueAliasList[alias] = alias; + entityAliases[aliasId] = {alias: alias, entityType: vm.entityAliases[i].entityType, entityFilter: cleanupEntityFilter(vm.entityAliases[i].entityFilter)}; + } else { + valid = false; + break; + } + } + } + if (valid) { + $scope.theForm.$setPristine(); + if (vm.isSingleEntityAlias) { + $mdDialog.hide(vm.singleEntityAlias); + } else { + $mdDialog.hide(entityAliases); + } + } else { + toast.showError($translate.instant('entity.duplicate-alias-error', {alias: alias})); + } + } + +} diff --git a/ui/src/app/entity/entity-aliases.scss b/ui/src/app/entity/entity-aliases.scss new file mode 100644 index 0000000000..b108dc074e --- /dev/null +++ b/ui/src/app/entity/entity-aliases.scss @@ -0,0 +1,28 @@ +/** + * 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-aliases-dialog { + .md-dialog-content { + padding-bottom: 0px; + } + .tb-alias { + padding: 10px 0 0 10px; + margin: 5px; + md-select.tb-entity-type-select { + padding-bottom: 24px; + } + } +} diff --git a/ui/src/app/entity/entity-aliases.tpl.html b/ui/src/app/entity/entity-aliases.tpl.html new file mode 100644 index 0000000000..bc9e269498 --- /dev/null +++ b/ui/src/app/entity/entity-aliases.tpl.html @@ -0,0 +1,106 @@ + + +
+ +
+

{{ vm.isSingleEntityAlias ? ('entity.configure-alias' | translate:vm.singleEntityAlias ) : (vm.title | translate) }}

+ + + + +
+
+ + + +
+
+
+ + + + +
+
+ +
+ entity.alias + entity.type + entity.entities + +
+
+
+
+ {{$index + 1}}. +
+ + +
+
entity.alias-required
+
+
+
+ + +
+
+ + +
+ + + {{ 'entity.remove-alias' | translate }} + + + close + + +
+
+
+
+ + + {{ 'entity.add-alias' | translate }} + + action.add + +
+
+
+
+ + + + {{ 'action.save' | translate }} + + {{ 'action.cancel' | translate }} + +
+
\ No newline at end of file diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js new file mode 100644 index 0000000000..777dbb8996 --- /dev/null +++ b/ui/src/app/entity/entity-filter.directive.js @@ -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. + */ + +/* eslint-disable import/no-unresolved, import/default */ + +import entityFilterTemplate from './entity-filter.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import './entity-filter.scss'; + +/*@ngInject*/ +export default function EntityFilterDirective($compile, $templateCache, $q, entityService) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + + var template = $templateCache.get(entityFilterTemplate); + element.html(template); + + scope.ngModelCtrl = ngModelCtrl; + + scope.itemName = function(item) { + if (item) { + return entityService.entityName(scope.entityType, item); + } else { + return ''; + } + } + + scope.fetchEntities = function(searchText, limit) { + var deferred = $q.defer(); + entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit).then(function success(result) { + if (result) { + deferred.resolve(result); + } else { + deferred.resolve([]); + } + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + scope.updateValidity = function() { + if (ngModelCtrl.$viewValue) { + var value = ngModelCtrl.$viewValue; + var valid; + if (value.useFilter) { + ngModelCtrl.$setValidity('entityList', true); + if (angular.isDefined(value.entityNameFilter) && value.entityNameFilter.length > 0) { + ngModelCtrl.$setValidity('entityNameFilter', true); + valid = angular.isDefined(scope.model.matchingFilterEntity) && scope.model.matchingFilterEntity != null; + ngModelCtrl.$setValidity('entityNameFilterEntityMatch', valid); + } else { + ngModelCtrl.$setValidity('entityNameFilter', false); + } + } else { + ngModelCtrl.$setValidity('entityNameFilter', true); + ngModelCtrl.$setValidity('entityNameFilterDeviceMatch', true); + valid = angular.isDefined(value.entityList) && value.entityList.length > 0; + ngModelCtrl.$setValidity('entityList', valid); + } + } + } + + ngModelCtrl.$render = function () { + destroyWatchers(); + scope.model = { + useFilter: false, + entityList: [], + entityNameFilter: '' + } + if (ngModelCtrl.$viewValue) { + var value = ngModelCtrl.$viewValue; + var model = scope.model; + model.useFilter = value.useFilter === true ? true: false; + model.entityList = []; + model.entityNameFilter = value.entityNameFilter || ''; + processEntityNameFilter(model.entityNameFilter).then( + function(entity) { + scope.model.matchingFilterEntity = entity; + if (value.entityList && value.entityList.length > 0) { + entityService.getEntities(scope.entityType, value.entityList).then(function (entities) { + model.entityList = entities; + updateMatchingEntity(); + initWatchers(); + }); + } else { + updateMatchingEntity(); + initWatchers(); + } + } + ) + } + } + + function updateMatchingEntity() { + if (scope.model.useFilter) { + scope.model.matchingEntity = scope.model.matchingFilterEntity; + } else { + if (scope.model.entityList && scope.model.entityList.length > 0) { + scope.model.matchingEntity = scope.model.entityList[0]; + } else { + scope.model.matchingEntity = null; + } + } + } + + function processEntityNameFilter(entityNameFilter) { + var deferred = $q.defer(); + if (angular.isDefined(entityNameFilter) && entityNameFilter.length > 0) { + scope.fetchEntities(entityNameFilter, 1).then(function (entities) { + if (entities && entities.length > 0) { + deferred.resolve(entities[0]); + } else { + deferred.resolve(null); + } + }); + } else { + deferred.resolve(null); + } + return deferred.promise; + } + + function destroyWatchers() { + if (scope.entityTypeDeregistration) { + scope.entityTypeDeregistration(); + scope.entityTypeDeregistration = null; + } + if (scope.entityListDeregistration) { + scope.entityListDeregistration(); + scope.entityListDeregistration = null; + } + if (scope.useFilterDeregistration) { + scope.useFilterDeregistration(); + scope.useFilterDeregistration = null; + } + if (scope.entityNameFilterDeregistration) { + scope.entityNameFilterDeregistration(); + scope.entityNameFilterDeregistration = null; + } + if (scope.matchingEntityDeregistration) { + scope.matchingEntityDeregistration(); + scope.matchingEntityDeregistration = null; + } + } + + function initWatchers() { + + scope.entityTypeDeregistration = scope.$watch('entityType', function (newEntityType, prevEntityType) { + if (!angular.equals(newEntityType, prevEntityType)) { + scope.model.entityList = []; + scope.model.entityNameFilter = ''; + } + }); + + scope.entityListDeregistration = scope.$watch('model.entityList', function () { + if (ngModelCtrl.$viewValue) { + var value = ngModelCtrl.$viewValue; + value.entityList = []; + if (scope.model.entityList && scope.model.entityList.length > 0) { + for (var i=0;i +
+
+
+ + + + {{itemName(item)}} + + + entity.no-entities-matching + + + + + {{itemName($chip)}} + + + +
+
+ + + + +
+
+ + + +
+
+ +
\ No newline at end of file diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js new file mode 100644 index 0000000000..6a5a0457d6 --- /dev/null +++ b/ui/src/app/entity/entity-type-select.directive.js @@ -0,0 +1,120 @@ +/* + * 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 './entity-type-select.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import entityTypeSelectTemplate from './entity-type-select.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EntityTypeSelect($compile, $templateCache, userService, types) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(entityTypeSelectTemplate); + element.html(template); + + if (angular.isDefined(attrs.hideLabel)) { + scope.showLabel = false; + } else { + scope.showLabel = true; + } + + scope.ngModelCtrl = ngModelCtrl; + + var authority = userService.getAuthority(); + scope.entityTypes = {}; + switch(authority) { + case 'SYS_ADMIN': + scope.entityTypes.tenant = types.entityType.tenant; + scope.entityTypes.rule = types.entityType.rule; + scope.entityTypes.plugin = types.entityType.plugin; + break; + case 'TENANT_ADMIN': + scope.entityTypes.device = types.entityType.device; + scope.entityTypes.asset = types.entityType.asset; + scope.entityTypes.customer = types.entityType.customer; + scope.entityTypes.rule = types.entityType.rule; + scope.entityTypes.plugin = types.entityType.plugin; + break; + case 'CUSTOMER_USER': + scope.entityTypes.device = types.entityType.device; + scope.entityTypes.asset = types.entityType.asset; + break; + } + + if (scope.allowedEntityTypes) { + for (var entityType in scope.entityTypes) { + if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) { + delete scope.entityTypes[entityType]; + } + } + } + + scope.typeName = function(type) { + switch (type) { + case types.entityType.device: + return 'entity.type-device'; + case types.entityType.asset: + return 'entity.type-asset'; + case types.entityType.rule: + return 'entity.type-rule'; + case types.entityType.plugin: + return 'entity.type-plugin'; + case types.entityType.tenant: + return 'entity.type-tenant'; + case types.entityType.customer: + return 'entity.type-customer'; + } + } + + scope.updateValidity = function () { + var value = ngModelCtrl.$viewValue; + var valid = angular.isDefined(value) && value != null; + ngModelCtrl.$setValidity('entityType', valid); + }; + + scope.$watch('entityType', function (newValue, prevValue) { + if (!angular.equals(newValue, prevValue)) { + scope.updateView(); + } + }); + + scope.updateView = function () { + ngModelCtrl.$setViewValue(scope.entityType); + scope.updateValidity(); + }; + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + scope.entityType = ngModelCtrl.$viewValue; + } + }; + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + allowedEntityTypes: "=?" + } + }; +} diff --git a/ui/src/app/entity/entity-type-select.scss b/ui/src/app/entity/entity-type-select.scss new file mode 100644 index 0000000000..58ff6ccdd3 --- /dev/null +++ b/ui/src/app/entity/entity-type-select.scss @@ -0,0 +1,18 @@ +/** + * 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. + */ + +md-select.tb-entity-type-select { +} diff --git a/ui/src/app/entity/entity-type-select.tpl.html b/ui/src/app/entity/entity-type-select.tpl.html new file mode 100644 index 0000000000..e31cbf5d12 --- /dev/null +++ b/ui/src/app/entity/entity-type-select.tpl.html @@ -0,0 +1,25 @@ + + + + + + {{typeName(type) | translate}} + + + \ No newline at end of file diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js new file mode 100644 index 0000000000..07c0862242 --- /dev/null +++ b/ui/src/app/entity/index.js @@ -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. + */ + +import EntityAliasesController from './entity-aliases.controller'; +import EntityTypeSelectDirective from './entity-type-select.directive'; +import EntityFilterDirective from './entity-filter.directive'; +import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller'; +import AliasesEntitySelectDirective from './aliases-entity-select.directive'; +import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; +import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; +import AttributeTableDirective from './attribute/attribute-table.directive'; + +export default angular.module('thingsboard.entity', []) + .controller('EntityAliasesController', EntityAliasesController) + .controller('AliasesEntitySelectPanelController', AliasesEntitySelectPanelController) + .controller('AddAttributeDialogController', AddAttributeDialogController) + .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) + .directive('tbEntityTypeSelect', EntityTypeSelectDirective) + .directive('tbEntityFilter', EntityFilterDirective) + .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) + .directive('tbAttributeTable', AttributeTableDirective) + .name; diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js index 62d6c2ddc9..22f9d75dbb 100644 --- a/ui/src/app/help/help-links.constant.js +++ b/ui/src/app/help/help-links.constant.js @@ -78,6 +78,7 @@ export default angular.module('thingsboard.help', []) pluginActionRestApiCall: helpBaseUrl + "/docs/reference/actions/rest-api-call-plugin-action", tenants: helpBaseUrl + "/docs/user-guide/ui/tenants", customers: helpBaseUrl + "/docs/user-guide/ui/customers", + assets: helpBaseUrl + "/docs/user-guide/ui/assets", devices: helpBaseUrl + "/docs/user-guide/ui/devices", dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards", users: helpBaseUrl + "/docs/user-guide/ui/users", diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index 0c0429101f..4b89a99799 100644 --- a/ui/src/app/import-export/import-export.service.js +++ b/ui/src/app/import-export/import-export.service.js @@ -16,7 +16,7 @@ /* eslint-disable import/no-unresolved, import/default */ import importDialogTemplate from './import-dialog.tpl.html'; -import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html'; +import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -24,8 +24,8 @@ import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html'; /* eslint-disable no-undef, angular/window-service, angular/document-service */ /*@ngInject*/ -export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, types, - deviceService, dashboardService, pluginService, ruleService, widgetService, toast) { +export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, types, dashboardUtils, + entityService, dashboardService, pluginService, ruleService, widgetService, toast) { var service = { @@ -339,21 +339,49 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, exportToPc(prepareExport(widgetItem), name + '.json'); } - function prepareDeviceAlias(aliasInfo) { - var deviceFilter; + function prepareAliasesInfo(aliasesInfo) { + var datasourceAliases = aliasesInfo.datasourceAliases; + var targetDeviceAliases = aliasesInfo.targetDeviceAliases; + var datasourceIndex; + if (datasourceAliases || targetDeviceAliases) { + if (datasourceAliases) { + for (datasourceIndex in datasourceAliases) { + datasourceAliases[datasourceIndex] = prepareEntityAlias(datasourceAliases[datasourceIndex]); + } + } + if (targetDeviceAliases) { + for (datasourceIndex in targetDeviceAliases) { + targetDeviceAliases[datasourceIndex] = prepareEntityAlias(targetDeviceAliases[datasourceIndex]); + } + } + } + } + + function prepareEntityAlias(aliasInfo) { + var entityFilter; + var entityType; if (aliasInfo.deviceId) { - deviceFilter = { + entityFilter = { useFilter: false, - deviceNameFilter: '', - deviceList: [aliasInfo.deviceId] + entityNameFilter: '', + entityList: [aliasInfo.deviceId] + } + entityType = types.entityType.device; + } else if (aliasInfo.deviceFilter) { + entityFilter = { + useFilter: aliasInfo.deviceFilter.useFilter, + entityNameFilter: aliasInfo.deviceFilter.deviceNameFilter, + entityList: aliasInfo.deviceFilter.deviceList } - delete aliasInfo.deviceId; + entityType = types.entityType.device; } else { - deviceFilter = aliasInfo.deviceFilter; + entityFilter = aliasInfo.entityFilter; + entityType = aliasInfo.entityType; } return { alias: aliasInfo.aliasName, - deviceFilter: deviceFilter + entityType: entityType, + entityFilter: entityFilter }; } @@ -364,13 +392,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, toast.showError($translate.instant('dashboard.invalid-widget-file-error')); } else { var widget = widgetItem.widget; - var aliasesInfo = widgetItem.aliasesInfo; + var aliasesInfo = prepareAliasesInfo(widgetItem.aliasesInfo); var originalColumns = widgetItem.originalColumns; var datasourceAliases = aliasesInfo.datasourceAliases; var targetDeviceAliases = aliasesInfo.targetDeviceAliases; if (datasourceAliases || targetDeviceAliases) { - var deviceAliases = {}; + var entityAliases = {}; var datasourceAliasesMap = {}; var targetDeviceAliasesMap = {}; var aliasId = 1; @@ -378,35 +406,37 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, if (datasourceAliases) { for (datasourceIndex in datasourceAliases) { datasourceAliasesMap[aliasId] = datasourceIndex; - deviceAliases[aliasId] = prepareDeviceAlias(datasourceAliases[datasourceIndex]); + entityAliases[aliasId] = datasourceAliases[datasourceIndex]; aliasId++; } } if (targetDeviceAliases) { for (datasourceIndex in targetDeviceAliases) { targetDeviceAliasesMap[aliasId] = datasourceIndex; - deviceAliases[aliasId] = prepareDeviceAlias(targetDeviceAliases[datasourceIndex]); + entityAliases[aliasId] = targetDeviceAliases[datasourceIndex]; aliasId++; } } - var aliasIds = Object.keys(deviceAliases); + var aliasIds = Object.keys(entityAliases); if (aliasIds.length > 0) { - processDeviceAliases(deviceAliases, aliasIds).then( - function(missingDeviceAliases) { - if (Object.keys(missingDeviceAliases).length > 0) { + processEntityAliases(entityAliases, aliasIds).then( + function(missingEntityAliases) { + if (Object.keys(missingEntityAliases).length > 0) { editMissingAliases($event, [ widget ], - true, 'dashboard.widget-import-missing-aliases-title', missingDeviceAliases).then( - function success(updatedDeviceAliases) { - for (var aliasId in updatedDeviceAliases) { - var deviceAlias = updatedDeviceAliases[aliasId]; + true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then( + function success(updatedEntityAliases) { + for (var aliasId in updatedEntityAliases) { + var entityAlias = updatedEntityAliases[aliasId]; var datasourceIndex; if (datasourceAliasesMap[aliasId]) { datasourceIndex = datasourceAliasesMap[aliasId]; - datasourceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter; + datasourceAliases[datasourceIndex].entityType = entityAlias.entityType; + datasourceAliases[datasourceIndex].entityFilter = entityAlias.entityFilter; } else if (targetDeviceAliasesMap[aliasId]) { datasourceIndex = targetDeviceAliasesMap[aliasId]; - targetDeviceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter; + targetDeviceAliases[datasourceIndex].entityType = entityAlias.entityType; + targetDeviceAliases[datasourceIndex].entityFilter = entityAlias.entityFilter; } } addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns); @@ -477,18 +507,19 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, toast.showError($translate.instant('dashboard.invalid-dashboard-file-error')); deferred.reject(); } else { - var deviceAliases = dashboard.configuration.deviceAliases; - if (deviceAliases) { - var aliasIds = Object.keys( deviceAliases ); + dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); + var entityAliases = dashboard.configuration.entityAliases; + if (entityAliases) { + var aliasIds = Object.keys( entityAliases ); if (aliasIds.length > 0) { - processDeviceAliases(deviceAliases, aliasIds).then( - function(missingDeviceAliases) { - if (Object.keys( missingDeviceAliases ).length > 0) { + processEntityAliases(entityAliases, aliasIds).then( + function(missingEntityAliases) { + if (Object.keys( missingEntityAliases ).length > 0) { editMissingAliases($event, dashboard.configuration.widgets, - false, 'dashboard.dashboard-import-missing-aliases-title', missingDeviceAliases).then( - function success(updatedDeviceAliases) { - for (var aliasId in updatedDeviceAliases) { - deviceAliases[aliasId] = updatedDeviceAliases[aliasId]; + false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then( + function success(updatedEntityAliases) { + for (var aliasId in updatedEntityAliases) { + entityAliases[aliasId] = updatedEntityAliases[aliasId]; } saveImportedDashboard(dashboard, deferred); }, @@ -534,53 +565,53 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return true; } - function processDeviceAliases(deviceAliases, aliasIds) { + function processEntityAliases(entityAliases, aliasIds) { var deferred = $q.defer(); - var missingDeviceAliases = {}; + var missingEntityAliases = {}; var index = -1; - checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred); + checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); return deferred.promise; } - function checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) { + function checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred) { index++; if (index == aliasIds.length) { - deferred.resolve(missingDeviceAliases); + deferred.resolve(missingEntityAliases); } else { - checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred); + checkEntityAlias(index, aliasIds, entityAliases, missingEntityAliases, deferred); } } - function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) { + function checkEntityAlias(index, aliasIds, entityAliases, missingEntityAliases, deferred) { var aliasId = aliasIds[index]; - var deviceAlias = deviceAliases[aliasId]; - deviceService.checkDeviceAlias(deviceAlias).then( + var entityAlias = entityAliases[aliasId]; + entityService.checkEntityAlias(entityAlias).then( function(result) { if (result) { - checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred); + checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); } else { - var missingDeviceAlias = angular.copy(deviceAlias); - missingDeviceAlias.deviceFilter = null; - missingDeviceAliases[aliasId] = missingDeviceAlias; - checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred); + var missingEntityAlias = angular.copy(entityAlias); + missingEntityAlias.entityFilter = null; + missingEntityAliases[aliasId] = missingEntityAlias; + checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); } } ); } - function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) { + function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingEntityAliases) { var deferred = $q.defer(); $mdDialog.show({ - controller: 'DeviceAliasesController', + controller: 'EntityAliasesController', controllerAs: 'vm', - templateUrl: deviceAliasesTemplate, + templateUrl: entityAliasesTemplate, locals: { config: { - deviceAliases: missingDeviceAliases, + entityAliases: missingEntityAliases, widgets: widgets, isSingleWidget: isSingleWidget, - isSingleDeviceAlias: false, - singleDeviceAlias: null, + isSingleEntityAlias: false, + singleEntityAlias: null, customTitle: customTitle, disableAdd: true } @@ -589,8 +620,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, skipHide: true, fullscreen: true, targetEvent: $event - }).then(function (updatedDeviceAliases) { - deferred.resolve(updatedDeviceAliases); + }).then(function (updatedEntityAliases) { + deferred.resolve(updatedEntityAliases); }, function () { deferred.reject(); }); diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 32395a64fd..be63558bfb 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -31,12 +31,14 @@ import thingsboardDashboardAutocomplete from '../components/dashboard-autocomple import thingsboardUserMenu from './user-menu.directive'; +import thingsboardEntity from '../entity'; import thingsboardTenant from '../tenant'; import thingsboardCustomer from '../customer'; import thingsboardUser from '../user'; import thingsboardHomeLinks from '../home'; import thingsboardAdmin from '../admin'; import thingsboardProfile from '../profile'; +import thingsboardAsset from '../asset'; import thingsboardDevice from '../device'; import thingsboardWidgetLibrary from '../widget'; import thingsboardDashboard from '../dashboard'; @@ -58,11 +60,13 @@ export default angular.module('thingsboard.home', [ thingsboardMenu, thingsboardHomeLinks, thingsboardUserMenu, + thingsboardEntity, thingsboardTenant, thingsboardCustomer, thingsboardUser, thingsboardAdmin, thingsboardProfile, + thingsboardAsset, thingsboardDevice, thingsboardWidgetLibrary, thingsboardDashboard, diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 549f4c8a20..1fc45f7681 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -101,6 +101,56 @@ export default angular.module('thingsboard.locale', []) "enable-tls": "Enable TLS", "send-test-mail": "Send test mail" }, + "asset": { + "asset": "Asset", + "assets": "Assets", + "management": "Asset management", + "view-assets": "View Assets", + "add": "Add Asset", + "assign-to-customer": "Assign to customer", + "assign-asset-to-customer": "Assign Asset(s) To Customer", + "assign-asset-to-customer-text": "Please select the assets to assign to the customer", + "no-assets-text": "No assets found", + "assign-to-customer-text": "Please select the customer to assign the asset(s)", + "public": "Public", + "assignedToCustomer": "Assigned to customer", + "make-public": "Make asset public", + "make-private": "Make asset private", + "unassign-from-customer": "Unassign from customer", + "delete": "Delete asset", + "asset-public": "Asset is public", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "type": "Type", + "type-required": "Type is required.", + "details": "Details", + "events": "Events", + "add-asset-text": "Add new asset", + "asset-details": "Asset details", + "assign-assets": "Assign assets", + "assign-assets-text": "Assign { count, select, 1 {1 asset} other {# assets} } to customer", + "delete-assets": "Delete assets", + "unassign-assets": "Unassign assets", + "unassign-assets-action-title": "Unassign { count, select, 1 {1 asset} other {# assets} } from customer", + "assign-new-asset": "Assign new asset", + "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", + "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", + "delete-assets-title": "Are you sure you want to delete { count, select, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": "Delete { count, select, 1 {1 asset} other {# assets} }", + "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.", + "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?", + "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.", + "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?", + "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.", + "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?", + "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.", + "unassign-asset": "Unassign asset", + "unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", + "copyId": "Copy asset Id", + "idCopiedMessage": "Asset Id has been copied to clipboard" + }, "attribute": { "attributes": "Attributes", "latest-telemetry": "Latest telemetry", @@ -157,8 +207,10 @@ export default angular.module('thingsboard.locale', []) "dashboard": "Customer Dashboard", "dashboards": "Customer Dashboards", "devices": "Customer Devices", + "assets": "Customer Assets", "public-dashboards": "Public Dashboards", "public-devices": "Public Devices", + "public-assets": "Public Assets", "add": "Add Customer", "delete": "Delete customer", "manage-customer-users": "Manage customer users", @@ -166,6 +218,8 @@ export default angular.module('thingsboard.locale', []) "manage-customer-dashboards": "Manage customer dashboards", "manage-public-devices": "Manage public devices", "manage-public-dashboards": "Manage public dashboards", + "manage-customer-assets": "Manage customer assets", + "manage-public-assets": "Manage public assets", "add-customer-text": "Add new customer", "no-customers-text": "No customers found", "customer-details": "Customer details", @@ -175,11 +229,16 @@ export default angular.module('thingsboard.locale', []) "delete-customers-action-title": "Delete { count, select, 1 {1 customer} other {# customers} }", "delete-customers-text": "Be careful, after the confirmation all selected customers will be removed and all related data will become unrecoverable.", "manage-users": "Manage users", + "manage-assets": "Manage assets", "manage-devices": "Manage devices", "manage-dashboards": "Manage dashboards", "title": "Title", "title-required": "Title is required.", - "description": "Description" + "description": "Description", + "details": "Details", + "events": "Events", + "copyId": "Copy customer Id", + "idCopiedMessage": "Customer Id has been copied to clipboard" }, "datetime": { "date-from": "Date from", @@ -265,7 +324,7 @@ export default angular.module('thingsboard.locale', []) "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", "display-title": "Display dashboard title", "title-color": "Title color", - "display-device-selection": "Display device selection", + "display-entities-selection": "Display entities selection", "display-dashboard-timewindow": "Display timewindow", "display-dashboard-export": "Display export", "import": "Import dashboard", @@ -303,10 +362,10 @@ export default angular.module('thingsboard.locale', []) "configuration": "Data key configuration", "timeseries": "Timeseries", "attributes": "Attributes", - "timeseries-required": "Device timeseries is required.", - "timeseries-or-attributes-required": "Device timeseries/attributes is required.", + "timeseries-required": "Entity timeseries are required.", + "timeseries-or-attributes-required": "Entity timeseries/attributes are required.", "function-types": "Function types", - "function-types-required": "Function types is required." + "function-types-required": "Function types are required." }, "datasource": { "type": "Datasource type", @@ -411,6 +470,41 @@ export default angular.module('thingsboard.locale', []) "unhandled-error-code": "Unhandled error code: {{errorCode}}", "unknown-error": "Unknown error" }, + "entity": { + "entity": "Entity", + "entities": "Entities", + "aliases": "Entity aliases", + "entity-alias": "Entity alias", + "unable-delete-entity-alias-title": "Unable to delete entity alias", + "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
Entity aliases must be unique whithin the dashboard.", + "configure-alias": "Configure '{{alias}}' alias", + "alias": "Alias", + "alias-required": "Entity alias is required.", + "remove-alias": "Remove entity alias", + "add-alias": "Add entity alias", + "entity-list": "Entity list", + "no-entities-matching": "No entities matching '{{entity}}' were found.", + "name-starts-with": "Name starts with", + "use-entity-name-filter": "Use filter", + "entity-list-empty": "No entities selected.", + "entity-name-filter-required": "Entity name filter is required.", + "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", + "type": "Type", + "type-device": "Device", + "type-asset": "Asset", + "type-rule": "Rule", + "type-plugin": "Plugin", + "type-tenant": "Tenant", + "type-customer": "Customer", + "select-entities": "Select entities", + "no-aliases-found": "No aliases found.", + "no-alias-matching": "'{{alias}}' not found.", + "create-new-alias": "Create a new one!", + "no-keys-found": "No keys found.", + "no-key-matching": "'{{key}}' not found.", + "create-new-key": "Create a new one!" + }, "event": { "event-type": "Event type", "type-alarm": "Alarm", @@ -541,7 +635,9 @@ export default angular.module('thingsboard.locale', []) "export-failed-error": "Unable to export plugin: {{error}}", "create-new-plugin": "Create new plugin", "plugin-file": "Plugin file", - "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure." + "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.", + "copyId": "Copy plugin Id", + "idCopiedMessage": "Plugin Id has been copied to clipboard" }, "position": { "top": "Top", @@ -604,7 +700,9 @@ export default angular.module('thingsboard.locale', []) "export-failed-error": "Unable to export rule: {{error}}", "create-new-rule": "Create new rule", "rule-file": "Rule file", - "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure." + "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.", + "copyId": "Copy rule Id", + "idCopiedMessage": "Rule Id has been copied to clipboard" }, "rule-plugin": { "management": "Rules and plugins management" @@ -626,7 +724,11 @@ export default angular.module('thingsboard.locale', []) "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.", "title": "Title", "title-required": "Title is required.", - "description": "Description" + "description": "Description", + "details": "Details", + "events": "Events", + "copyId": "Copy tenant Id", + "idCopiedMessage": "Tenant Id has been copied to clipboard" }, "timeinterval": { "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }", diff --git a/ui/src/app/plugin/plugin-fieldset.tpl.html b/ui/src/app/plugin/plugin-fieldset.tpl.html index 86dfeb5b25..b5f87429a0 100644 --- a/ui/src/app/plugin/plugin-fieldset.tpl.html +++ b/ui/src/app/plugin/plugin-fieldset.tpl.html @@ -28,6 +28,16 @@ ng-show="!isEdit && !isReadOnly" class="md-raised md-primary">{{ 'plugin.delete' | translate }}
+
+ + + plugin.copyId + +
+
diff --git a/ui/src/app/plugin/plugin.directive.js b/ui/src/app/plugin/plugin.directive.js index d1fdeeaf56..caaa7fad8f 100644 --- a/ui/src/app/plugin/plugin.directive.js +++ b/ui/src/app/plugin/plugin.directive.js @@ -22,7 +22,7 @@ import pluginFieldsetTemplate from './plugin-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function PluginDirective($compile, $templateCache, types, utils, userService, componentDescriptorService) { +export default function PluginDirective($compile, $templateCache, $translate, types, toast, utils, userService, componentDescriptorService) { var linker = function (scope, element) { var template = $templateCache.get(pluginFieldsetTemplate); element.html(template); @@ -63,6 +63,10 @@ export default function PluginDirective($compile, $templateCache, types, utils, } }, true); + scope.onPluginIdCopied = function() { + toast.showSuccess($translate.instant('plugin.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + componentDescriptorService.getComponentDescriptorsByType(types.componentType.plugin).then( function success(components) { scope.pluginComponents = components; diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html index 04a7a09ee3..bd8cf74cb5 100644 --- a/ui/src/app/plugin/plugins.tpl.html +++ b/ui/src/app/plugin/plugins.tpl.html @@ -31,6 +31,23 @@ on-export-plugin="vm.exportPlugin(event, vm.grid.detailsConfig.currentItem)" on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> + + + + + + + + {{ 'rule.delete' | translate }} +
+ + + rule.copyId + +
+
diff --git a/ui/src/app/rule/rule.directive.js b/ui/src/app/rule/rule.directive.js index bd1ab406d2..bfd6b4c695 100644 --- a/ui/src/app/rule/rule.directive.js +++ b/ui/src/app/rule/rule.directive.js @@ -22,7 +22,8 @@ import ruleFieldsetTemplate from './rule-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function RuleDirective($compile, $templateCache, $mdDialog, $document, $q, pluginService, componentDialogService, componentDescriptorService, types) { +export default function RuleDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, pluginService, + componentDialogService, componentDescriptorService, types, toast) { var linker = function (scope, element) { var template = $templateCache.get(ruleFieldsetTemplate); element.html(template); @@ -91,6 +92,10 @@ export default function RuleDirective($compile, $templateCache, $mdDialog, $docu } }; + scope.onRuleIdCopied = function() { + toast.showSuccess($translate.instant('rule.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + scope.$watch('rule', function(newVal, prevVal) { if (newVal) { if (!scope.rule.filters) { diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html index 8aca1e3889..bd7ea4829a 100644 --- a/ui/src/app/rule/rules.tpl.html +++ b/ui/src/app/rule/rules.tpl.html @@ -31,6 +31,23 @@ on-export-rule="vm.exportRule(event, vm.grid.detailsConfig.currentItem)" on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> + + + + + + + + {{ 'tenant.manage-tenant-admins' | translate }} {{ 'tenant.delete' | translate }} +
+ + + tenant.copyId + +
+
diff --git a/ui/src/app/tenant/tenant.controller.js b/ui/src/app/tenant/tenant.controller.js index 3ba3b4baec..93fc98240c 100644 --- a/ui/src/app/tenant/tenant.controller.js +++ b/ui/src/app/tenant/tenant.controller.js @@ -21,7 +21,7 @@ import tenantCard from './tenant-card.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function TenantController(tenantService, $state, $stateParams, $translate) { +export default function TenantController(tenantService, $state, $stateParams, $translate, types) { var tenantActionsList = [ { @@ -44,6 +44,8 @@ export default function TenantController(tenantService, $state, $stateParams, $t var vm = this; + vm.types = types; + vm.tenantGridConfig = { refreshParamsFunc: null, diff --git a/ui/src/app/tenant/tenant.directive.js b/ui/src/app/tenant/tenant.directive.js index 13a7eba263..7876061e33 100644 --- a/ui/src/app/tenant/tenant.directive.js +++ b/ui/src/app/tenant/tenant.directive.js @@ -20,10 +20,15 @@ import tenantFieldsetTemplate from './tenant-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function TenantDirective($compile, $templateCache) { +export default function TenantDirective($compile, $templateCache, $translate, toast) { var linker = function (scope, element) { var template = $templateCache.get(tenantFieldsetTemplate); element.html(template); + + scope.onTenantIdCopied = function() { + toast.showSuccess($translate.instant('tenant.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + $compile(element.contents())(scope); } return { diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html index 704ec0be18..ada2a3fb7e 100644 --- a/ui/src/app/tenant/tenants.tpl.html +++ b/ui/src/app/tenant/tenants.tpl.html @@ -15,13 +15,43 @@ limitations under the License. --> +
- + + + + + + + + + + + + + + + + +